Компиляция. 1: лексер

    Меня всегда завораживало таинство рождения программой программы. К сожалению, российские вузы уделяют мало внимания сей интереснейшей теме. Рассчитываю написать серию постов, в которых поэтапно создадим маленький работоспособный компилятор.

    Первые посты серии уже подготовлены, и бета-тестировались в одном маленьком и наглухо закрытом сообществе. Тем не менее, я буду продолжать их править с учётом пожеланий почтенной хабрапублики.

    Далее в посте:

    1. С какой стати писать компиляторы?
    2. Общий план
    3. Анализ текста
    4. Практический пример
    5. Как это работает?

    С какой стати писать компиляторы?


    Разве не все нужные языки программирования уже написаны? Кому в наш просвещённый век может понадобиться писать собственный компилятор?
    Everything that can be invented has been invented.
    --Charles H. Duell, Director of U.S. Patent Office, 1899 (attributed)
    Что было, то и будет; и что делалось, то и будет делаться, и нет ничего нового под солнцем.
    --Екклесиаст 1:9 (ок. 3 в. до н.э.)

    Во-первых, языки постоянно создаются и развиваются. Из ныне популярных, Ruby, PHP, Java и C# были созданы на нашей памяти, а бурно применяться начали несколько лет назад. Прямо сейчас Майкрософт проталкивает новый язык F#, и он — учитывая мощь Майкрософт — наверняка также войдёт в число общеупотребимых.
    До сих пор остаются ниши для новых языков: например, не прекращаются попытки придумать удобный императивный язык для параллельного программирования.

    Во-вторых, используемые в компиляции приёмы (в первую очередь, парсинг по грамматике) имеют массу других приложений. Часто есть потребность в преобразованиях source-to-source (рефакторинг, перевод кода на другой язык, и т.п.), когда нужно разобрать текст на языке программирования, обработать его, и вывести обработанный текст (на том же или на другом языке).

    Безусловно, компиляция — довольно экзотичное занятие для программиста, где-нибудь в одном ряду с программированием огромных боевых человекоподобных роботов. Однако у всего этого есть практические применения, в отличие от многих других экстравагантных программистских хобби.

    Общий план


    Процесс компиляции, в принципе, состоит из двух основных этапов:
    1. Анализ текста на исходном языке
    2. Генерация машинного кода
    На первом этапе строится представление программы, с которым будет удобно работать дальше; обычно это дерево, но вовсе не обязательно. На втором этапе компилятор проходит по дереву, и для каждого узла генерирует окончательный код.

    Самая challenging часть компилятора — оптимизация кода; на первом этапе выполняется высокоуровневая оптимизация, на уровне узлов дерева (например, разворачиваются циклы, вживляются inline-функции); на втором — низкоуровневая, на уровне потока команд (например, переупорядочиваются так, чтобы полнее нагрузить конвейеры конкретного процессора). До оптимизаций, по традиции, в вузовских курсах дело никогда не доходит; но самые простые (copy elimination, constant propagation) мы в нашем примере постараемся реализовать.

    Старые посты на тему синтаксического разбора я на Хабре видел; но к генерации кода, вроде бы, авторы не приближались ни разу.

    Анализ текста


    Когда начинающий программист пытается написать парсер текста, его естественный подход — рекурсивное углубление: найти начало конструкции (например, {); найти её конец (например, } на том же уровне вложенности); выделить содержимое конструкции, и пропарсить её рекурсивно.

    Проблемы с таким подходом — во-первых, избыточная сложность (по одному и тому же фрагменту текста гуляем взад-вперёд); во-вторых, неудобство поддержки (синтаксис языка оказывается рассредоточен по килобайтам и килобайтам ветвистого кода).

    Синтаксис языка можно задать декларативно; например, всем знакомы регулярные выражения. Идеально было бы написать стопочку регэкспов для всех конструкций языка, напротив каждого — определение узла, который должен создаться в дереве программы; «универсальный парсер» бы просто подставлял программу в один регэксп за другим, и создавал узлы согласно описанию, один за другим.

    Первая из названных проблем решается тем, что поиск всех регэкспов в тексте можно выполнить за один проход, т.е. нет надобности хранить всю программу в памяти целиком — достаточно читать её по одному символу, обрабатывать символ, и тут же забывать.
    Вторая — тем, что теперь у нас есть централизованное, формальное описание языка: можем менять регэкспы, вовсе не трогая код; и наоборот, менять код, не рискуя повредить парсер.

    Практический пример


    Упомянутый «универсальный парсер», конечно же, давно реализован, и не одиножды. Классическая реализация — lex — принимает описания действий для каждого регэкспа на Си. В стандартные утилиты GNU входит flex, совместимый с lex, — его мы и будем использовать для примеров. (Есть аналогичные утилиты для C#, Java и пр.)

    По традиции, первым примером станет написание калькулятора:
    %{
        #include <stdio.h>
        int reg = 0;
        char op = '+';
        int unmin = 0;
    %}
    
    %option main
    %option yylineno
    
    %%
    
    [/][/].*\n      ; // comment
    [0-9]           { int opnd = atoi(yytext);
                      if (unmin) opnd =- opnd; unmin=0;
                      switch(op) {
                        case '+': reg += opnd; break;
                        case '-': reg -= opnd; break;
                        case '*': reg *= opnd; break;
                        case '/': reg /= opnd; break;
                      }
                      op = 0;
                    }
    [-+*/]          { if (op) {
                        if (*yytext=='-')
                          unmin = 1;
                        else {
                          printf("Unexpected operator in line %d\n", yylineno);
                          exit(1);
                        }
                      } else
                        op = *yytext;
                    }
    [;]             { if (op) {
                        printf("Unexpected semicolon in line %d\n", yylineno);
                        exit(1);
                      }
                      printf("=%d\n", reg);
                      reg = 0;
                      op = '+';
                    }
    [ \t\r\n]       ; // whitespace
    .               { printf("Syntax error in line %d\n", yylineno); exit(1); }
    %%
    


    Имитируется калькулятор торговок с рынка: без скобок, без приоритета операций, без дробей. Выражения разделяются точкой с запятой, и можно вставлять комментарии от // до конца строки.

    Скомпилируем наш калькулятор, и попробуем с ним поиграть:
    [tyomitch@home ~]$ lex 1.lex
    [tyomitch@home ~]$ cc lex.yy.c
    [tyomitch@home ~]$ ./a.out
    2+2;
    =4
    2+2*2;
    =8
    2 + // hello
    - 3
    ;
    =-1
    2 / /
    Unexpected operator in line 6

    Разбор кода

    • Вверху, в тегах %{ %}, идёт код, который скопируется напрямик в парсер. Объявляем три переменные: reg («регистр») для промежуточного результата, op для набранной операции, и unmin — флаг, что был набран минус после знака операции, и он должен трактоваться как знак второго операнда.
    • %option main указывает, что нас устроит стандартная main (которая читает с stdin до EOF). %option yylineno создаёт в парсере переменную int yylineno, где будет храниться номер текущей строки входного текста: он пригодится для диагностических сообщений.
    • %% разделяет область объявлений и область собственно правил языка
    • В каждом правиле слева пишется регэксп, справа — код на Си. В первом регэкспе код пуст; т.е. такая конструкция (а это коммментарий) будет просто игнорироваться. Аналогично, предпоследнее правило предписывает игнорировать пробелы и переводы строк между распознаваемыми конструкциями.
    • Во втором правиле пользуемся переменной yytext: в ней хранится совпавший с регэкспом текст (в нашем случае, число-операнд)
    • В третьем правиле — пример обработки ошибки в тексте программы (используем yylineno в тексте сообщения)
    • Правила пробуются в порядке их появления, от первого к последнему. Если ни одно не подошло, парсер просто напечатает текущий символ в stdout. Вместо этого, мы добавляем последним правилом . — оно подойдёт к любому символу, и напечатает сообщение об ошибке.
    В настоящем компиляторе, разумеется, правила будут не печатать результаты вычислений на экран, а сохранять сами выражения для последующей генерации кода.

    Как это работает?

    У математиков есть понятие ДКА (детерминированный конечный автомат) — штука, которая может находиться в одном из N состояний; которая читает из входного потока символ за символом; и у которой есть таблица: для каждого сочетания текущего состояния и прочитанного символа — в которое состояние перейти. Работа flex заключается в том, что он строит ДКА по заданному набору регэкспов; в некоторых состояниях этот ДКА не только переходит в следующее, но вдобавок вызвает наши правила.

    Увидеть построенный ДКА можно, заглянув внутрь сгенерированного парсера lex.yy.c. Потребовалось 15 состояний. Для экономии места, таблица переходов хранится не в явном виде (размером 15х256), а разбитая на мудрёные накладывающиеся списки. Чтобы увидеть её в наиболее наглядной форме, скомпилируем парсер с опцией -Cef («отключить сжатие таблиц»):

    static yyconst short yy_nxt[][8] =
        {
        {    0,    0,    0,    0,    0,    0,    0,    0    },
        {    3,    4,    5,    6,    7,    8,    9,   10    },
        {    3,    4,    5,    6,    7,    8,    9,   10    },
        {   -3,   -3,   -3,   -3,   -3,   -3,   -3,   -3    },
        {    3,   -4,   -4,   -4,   -4,   -4,   -4,   -4    },
        {    3,   -5,   -5,   -5,   -5,   -5,   -5,   -5    },
        {    3,   -6,   -6,   -6,   -6,   -6,   -6,   -6    },
        {    3,   -7,   -7,   -7,   -7,   -7,   -7,   -7    },
        {    3,   -8,   -8,   -8,   -8,   11,   -8,   -8    },
        {    3,   -9,   -9,   -9,   -9,   -9,   12,   -9    },
        {    3,  -10,  -10,  -10,  -10,  -10,  -10,  -10    },
        {    3,   13,   13,   14,   13,   13,   13,   13    },
        {    3,  -12,  -12,  -12,  -12,  -12,   12,  -12    },
        {    3,   13,   13,   14,   13,   13,   13,   13    },
        {    3,  -14,  -14,  -14,  -14,  -14,  -14,  -14    },
        } ;
    
    static yyconst short int yy_accept[15] =
        {   0,
            0,    0,    8,    6,    5,    5,    3,    3,    2,    4,
            0,    2,    0,    1
        } ;
    

    Символы здесь разбиты на 8 классов, идентичных с точки зрения парсера (например, все цифры объединены в один класс). Отдельный массив static yyconst int yy_ec[256] ставит каждому символу в соответствие класс.

    Основной цикл работы парсера весьма незамысловат:
    yy_match:
       while ( (yy_current_state = yy_nxt[yy_current_state][yy_ec[(unsigned char)(*yy_cp)]]) > 0 )
           {
           if ( yy_accept[yy_current_state] )
               {
               yy_last_accepting_state = yy_current_state;
               yy_last_accepting_cpos = yy_cp;
               }
    
             yy_cp;
           }
    
       yy_current_state = -yy_current_state;
    

    Положительные числа в таблице переходов означают «перейти в состояние и продолжить читать»; отрицательные — «перейти в состояние и выполнить действие». Номер действия, которое должно выполняться по приходу в состояние, хранится в yy_accept.

    Рассмотрим пример: для цифр номер «класса символа» — 6.
    В начальном состоянии (1) по символу 6 видим в таблице переходов 9. Переходим, читаем дальше.
    В состоянии 9, если есть ещё одна цифра (6), переходим в состояние 12 и читаем дальше.
    Из состояния 12, если есть ещё одна цифра, просто читаем дальше. (В таблице стоит 12)
    Если увидели не-цифру (любой символ, кроме 6), нужно выполнить действие: видим в 9-ой строчке ряд из -9, и в 12-ой строчке ряд из -12.
    Проверяем yy_accept: в обоих случаях применяем правило 2. (Вспомним, что правило, распознающее числа, в нашем парсере действительно второе.)
    Непонятно, зачем flex решил разделить состояния 9 и 12, если в обоих делает одно и то же самое. Но он бездушная железяка, ему виднее.

    Замечательно то, насколько готовый парсер прост: вместо ветвистого распознавания разных конструкций, у нас одна большая таблица, и цикл из десяти строк. Но есть существенная проблема. Вернёмся к примеру из самого начала поста: «найти начало конструкции (например, {); найти её конец (например, } на том же уровне вложенности)...» Как регэкспом обозначить условие «на том же уровне вложенности»?

    Математики и тут постарались: доказали, что невозможно. Значит, для парсинга языков, поддерживающих вложенные конструкции (а это все языки сложнее BAT-файлов), нам потребуется более мощный инструмент, чем регэкспы. В следующий раз — о нём.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 40
    • +2
      Так и не увидел за весь пост фразы" LR-грамматика"
      • +3
        последний параграф как бы намекает, что в следующей части вы как раз и увидите эту фразу.
        • +2
          Грубо говоря, лексеру пофиг на грамматику: он разбирает поток символов на токены. Грамматика имеет смысл, когда уже есть токены.
        • +2
          К сожалению, российские вузы уделяют мало внимания сей интереснейшей теме

          не, ну ты смотри какие подлецы. и дисциплины «теория компиляторов» наверное нет нигде?
          • +2
            Нас на этой дисциплине учили только грамматикам.
            О применении грамматик в компиляции было вскользь.
            Об остальных этапах компиляции не было вовсе.
        • +4
          Если не секрет, чем обусловлен выбор lex а не antlr?
          • 0
            lex по православнее будет. а вобще я пользуюсь другим средством — ragel
          • 0
            Отличная статья! По-моему тема создания компиляторов, или точнее, более узкая тема source-to-source преобразований должна быть близка всем разработчикам на крупных проектах. Меня до сих пор не отпускает идея создания Domain-specific языка для предметной области своего проекта. Но знаний по компиляции пока не хватает.

            С нетерпением жду продолжения. :)
            • +1
              Для создания DSL есть много готовых решений и подходов, вроде MPS для внешних DLS или Scala для внутренних. Вовсе не требуется знать про компиляторы или, ещё хуже, писать их.

          • +2
            Хм.
            У нас (ВМК МГУ) был отдельный курс Системы Программирования, где половина материала была как раз о Формальных грамматиках и теории компиляции.
            + у одного из потоков еще отдельный курс Конструирование Компиляторов.
            Материал рассказывался довольно хорошо, хотя многие не понимали, зачем им это нужно будет. Ну, это как всегда)
            • +1
              Не всем повезло учиться в МГУ.
              • 0
                В НГУ есть спецкурсы на эту тема, есть даже отдельный спецкурс про оптимизирующие компиляторы. На МГУ свет клином не сошелся :)
                • +1
                  хе-хе, НГУ повезло что под боком Excelsior и Intel [причем именно в таком порядке].

                  не было бы их — кукишь, а не спецкурс по оптимизирующей компиляции.

            • +1
              если хочется начинать не с нуля, а на базе какого-нибудь современного языка, рекомендую посмотреть в сторону Scala.
              Scala позволяет писать плагины к компилятору, имеющие достаточно широкий простор действий.

              сам компилятор очень хорошо устроен, имеет чёткие стадии работы и вообще представляем из себя отличную иллюстрацию того, как надо работать =)

              • НЛО прилетело и опубликовало эту надпись здесь
                • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    Ух какие парсеры ревнивые! Подрались!

                    Вернул плюсики на место.
                  • +1
                    Автор, статья неплохая, но дайте перварительно общую картину — классы языков. Ту ж иерархию Хомского
                    К примеру, не очень понятно почему регекспами нельзя обрабатывать вложенность (а если ими нельзя, то чем можно?).

                    2Nostromo: читал какие-то мгушные лекции, не очень-то понятно. Не могли бы назвать хорошего лектора (по формальным языкам)?
                    • +1
                      Почему нельзя, и чем можно — обещаю во втором посте.

                      Теорию собирался касаться по минимуму, всё-таки целюсь в программистов, а не в математиков.
                      • +2
                        Волкова. У нее, кстати, есть прекрасная (имхо) методичка конкретно по формальным грамматикам и языкам и элементам теории трансляции.
                        Все это можно найти, например, вот здесь:
                        cmcmsu.no-ip.info/2course/ в материалах за 4 семестр.
                        там еще много полезной информации.
                        • +1
                          Спасибо, по этой ссылке есть занятные книжки.
                      • +1
                        Свердлов С. З. Языки программирования и методы трансляции, Питер, 2007, ISBN 978-5-469-00378-6.

                        Отличная книга на данную тему, где хорошо и доступно раскрыты как исторические предпосылки, так в необходимом объеме теория, а так же практическая реализация на примере реализации минимального подмножества языка Оберон, на различных языках программирования (Паскаль, Ява, Си шарп, Сам оберон и прочие). Рекомендована российским мин. образования. Одна из немногих известных мне достойных книг, российского авторства, а не перевод, на тему АйТи.
                        • 0
                          www.ozon.ru/context/detail/id/3829076/
                          Альфред В. Ахо, Моника С. Лам, Рави Сети, Джеффри Д. Ульман
                          Компиляторы. Принципы, технологии и инструментарий
                          Compilers: Principles, Techniques, & Tools
                          Издательство: Вильямс, 2008 г.
                          Твердый переплет, 1184 стр.
                          ISBN 978-5-8459-1349-4, 0-321-48681-1

                          Лучшая книга про разработку компиляторов.
                          • +3
                            Неужели следующая статья будет про yacc? Настоятельно не рекомендую.
                            Вернее так — Вы определитесь, про какой подход рассказывать. Можно долго делать очень быстрый компилятор, а можно в разы быстрее сделать компилятор, который будет работать с приемлемой скоростью. Так вот lex/yacc для второго подхода использовать не советую — отлаживать замучаетесь. К тому же в больших языках всегда есть какие-нибудь исключительные ситуации, когда возможностей генератора сканеров/парсеров не хватает и приходится выкручиваться. В этой ситуации я тоже предпочту какой-нибудь antlr.

                            В общем, задача настолько хорошо изучена, что есть смысл рассказывать только про современные технологии её решения, которые охватывают все этапы разработки. И тестирование, кстати.
                            • +1
                              Да, рассказываю о хорошо изученных вещах, и на первооткрывательство не претендую. Цель была — изложить популярно.

                              Да, у меня нет опыта с antlr и с LL-парсерами вообще.
                              В чём их преимущество? в удобстве отладки?
                              • +1
                                Гораздо больше возможностей и удобства для программиста. В частности, поддерживается работа с деревом, которое является результатом разбора. Впрочем, я предпочитаю пользоваться своим инструментом TreeDL. Также есть поддержка кодогенерации.

                                Извините за некоторую резкость, «ревную» близкую мне тему :) Давайте лучше дополню статью примерами из реальной жизни. Вот наши фронтенды для расширений С и Java, написанные на JavaCC+TreeDL и Antlr/Java+TreeDL соответственно:
                                сканер+парсер расширенного С
                                дерево расширенного С
                                сканер Java
                                • 0
                                  рука дернулась…

                                  парсер Java
                                  дерево Java
                                  • +1
                                    Давайте лучше дополню статью примерами из реальной жизни.

                                    С удовольствием бы прочитал, если будет не сухой код, а с пояснениями, откуда что и зачем.
                                    Особенно интересует «сравнительный анализ» yacc vs. antlr. (Зарылся в гугл.)
                                    • +2
                                      Так это писалось не один год и не одним человеком :) Я ещё могу ответить на конкретные вопросы, но всё прокомментировать нереально.

                                      Главное отличие lex/yacc от antlr — вид используемой грамматики. regexp/LR(1) (или точнее LALR(1), если я правильно помню) vs LL(k)/LL(k). Да, для сканеров на antlr используется та же грамматика, что и для парсеров. k — произвольный размер lookahead, причем в отдельных сложных местах его можно увеличивать задавая синтаксические предикаты (а еще есть семантические!). LL парсеры похожи на те, которые пишутся руками — рекурсивные. Сгенерированный код легко читать и отлаживаться по нему.

                                      Disclaimer: моё плотное знакомство с antlr закончилось на 2й версии, 3я может отличаться.
                                      • 0
                                        Вечер добрый. Пытаюсь родить pmd-rule (http://pmd.sourceforge.net/) для примитивного случая sql injection:

                                        ResultSet rs = null;
                                        PreparedStatement ps = null;
                                        /* some code… */
                                        ps = con.prepareStatement(mySQL);
                                        /* again some code… */

                                        Как я понял, pmd основан на JavaCC.
                                        Я без проблем нахожу вызов «prepareStatement», но никак не могу получить доступ к аргументу метода, чтоб поискать его usage в method scope (упрощаю задачу).

                                        В общем, идея следующая: если аргумент метода «prepareStatement» является производным (через конкатенацию или StringBuilder, StringBuffer) от строкового (String) аргумента метода класса, внутри которого готовится стейтмент, то надо выдать предупреждение о потенциальном SQL-injection.

                                        Понятное дело, озвученный кейс — самый примитивный, тем не менее подрядчики этим занимаются постоянно. К счастью, до чего-то более изощренного они не додумываются.

                                        Есть ли у вас опыт написания подобного, где можно посмотреть, почитать? форум pmd скорее мертв, чем жив.
                                        • 0
                                          Правил для PMD писать не приходилось, но общий принцип понятен. Самое главное — JavaCC практически ни при чем. Вы имеете дело с готовым деревом (AST), в котором надо найти узел, соответствующий вызову метода, проверить, что имя метода «prepareStatement» и в этом случае выполнить необходимую проверку типа параметра.
                                          Судя по примеру
                                          pmd.sourceforge.net/howtowritearule.html
                                          Вызову метода соответствует PrimaryExpression с детьми PrimaryPrefix и PrimarySuffix. Объект и имя метода сидят в префиксе, а аргументы — в суффиксе.
                                          Удобного описания дерева я что-то сходу не нашел (даже исходного *.jj в дистрибутиве не вижу), так что проще пользоваться предлагаемым дизайнером, который показывает дерево входного файла).
                                          Если вопросы останутся — пишите ICQ 740187 или allex@all-x.net
                            • +1
                              Прочитал статью «Редкая профессия» Евгения Зуева, про то, как чуваки писали компилятор
                              с++ когда-то давно. Как переводили стандарты, с какими проблемами сталкивались, про проблемы грамматики с++ и прочее.
                              Вот нашел ссылку: www.interstron.ru/upload/images/pubs/Redkaya_professiya.pdf
                              Тем, кто интересуется компиляторами рекомендую, можно узнать интересные вещи.
                              • +3
                                > Практический пример
                                > В стандартные утилиты GNU входит flex, совместимый с lex, — его мы и будем использовать для примеров.
                                > По традиции, первым примером станет написание калькулятора:

                                Товарищ, ты слишком быстро перепрыгнул.

                                Во-первых, ты не сказал толком, какой язык мы будем обрабатывать в примере. В нашем случае, мы будем обрабатывать «простейший язык математических выражений». В нем будут только цифры 0-9, четыре знака математических операций "+", "-", "*", "/"), символ завершения выражения ";".

                                Во-вторых, ты ни слова не сказал, что вообще такое lex / flex.

                                Коль рассказываешь основы, надо было бы пояснить, что lex — это генератор лексического анализатора, который на основе файла описания языка, создает c-файл с лексическим анализатором. Этот c-файл можно скомпилировать, после чего полученный бинарник сможет выполнять текст программы на придуманном нами языке. Грубо говоря, полученный бинарник находит в тексте программы на «придуманном» языке куски, попадающие под заданные нами маски, и вместо найденных кусков выполняет заданный нами код на языке C.

                                Кроме того, надо сразу сказать (а не после), что первый листинг в статье — это код lex-файла, в котором описываются маски для поиска кусочков текста, и С-код, кторый должен выполняться вместо этих масок. Данный файл скармливается программе lex, на выходе имеем C-файл с лексическим анализатором. Компилируем этот файл, и получаем бинарник лексического анализатора.

                                Этот бинарник как раз и умеет выполнять программы на придуманном нами языке. Он берет текст программы из стандартного ввода, и помещает результат выполнения программы в стандартный вывод. То есть, с помощью этого бинарника мы можем выполнять программы на «простейшем языке математических выражений» прямо в консоли.

                                После таких пояснений можно уже и код lex-файла показывать, и все остальное объяснять.
                                • 0
                                  Мне кажется, что все перечисленные факты в тексте есть.
                                  Хоть и не в таком порядке.
                                • 0
                                  > Мне кажется, что все перечисленные факты в тексте есть.
                                  > Хоть и не в таком порядке.

                                  В человеческой речи, в отличие от операции сложения в классической математике, от перестановки мест слагаемых результат меняется.

                                  У вас вся статья написана задом-наперед. К середине статьи накапливается куча необъяснённых вопросов, которые кое-как разрешаются в конце. Это сложно для понимая обычному человеку. Такое впечатление, что вы перепрограммировали на каком-то функциональном языке, и функциональное мышление повлияло на стиль вашей речи.
                                  • 0
                                    Если не секрет, по каким соображениям была выбрана связка c+lex+yacc? Чем не понравились такие инструменты как Ocaml или Haskell на пример? К тому же они вроде как получили некое распространение на поприще компилятора-строения.
                                    • 0
                                      Изучал то, по чему проще найти информацию.
                                      Если расскажете о функциональном компиляторо-строении, уверен, не одному мне будет интересно.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.