Есть ли альтернатива для гибких/бизонов, которые можно использовать в 8-битных встроенных системах?

Я пишу небольшой интерпретатор для простого языка BASIC, как упражнение на микроконтроллере AVR в C, используя инструментальную цепочку avr-gcc. Тем не менее, мне интересно, есть ли какие-либо инструменты с открытым исходным кодом, которые могут помочь мне написать лексер и парсер.

Если бы я написал это для запуска на моем Linux-ящике, я мог бы использовать flex/bison. Теперь, когда я ограничился 8-битной платформой, я должен сделать все это вручную или нет?

Ответ 1

Я реализовал парсер для простого командного языка, предназначенного для ATmega328p. У этого чипа 32k ROM и только 2k RAM. ОЗУ, безусловно, является более важным ограничением - если вы еще не привязаны к определенному чипу, выберите один с максимально возможной оперативной памятью. Это облегчит вашу жизнь.

Сначала я рассмотрел использование flex/bison. Я решил отказаться от этого варианта по двум основным причинам:

  • По умолчанию Flex и Bison зависят от некоторых стандартных функций библиотеки (особенно для ввода-вывода), которые недоступны или не работают одинаково в avr-libc. Я уверен, что поддерживаются обходные пути, но это дополнительные усилия, которые вам необходимо принять во внимание.
  • У AVR есть Гарвардская архитектура. C не предназначен для учета этого, поэтому даже постоянные переменные загружаются в ОЗУ по умолчанию. Вы должны использовать специальные макросы/функции для хранения и доступа к данным в flash и EEPROM. Flex и Bison создают несколько относительно больших таблиц поиска, и они быстро съедят вашу оперативную память. Если я не ошибаюсь (что вполне возможно), вам придется отредактировать выходной источник, чтобы воспользоваться специальными интерфейсами Flash и EEPROM.

После отказа от Flex и Bison я пошел искать другие инструменты генератора. Вот несколько из них, которые я рассмотрел:

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

В конечном счете, я закончил ручное кодирование как лексера, так и парсера.

Для разбора я использовал рекурсивный синтаксический анализатор спуска. Я думаю, Ира Бакстер уже проделала адекватную работу по освещению этой темы, и в Интернете есть много учебников.

Для моего lexer я написал регулярные выражения для всех моих терминалов, нарисовал диаграмму эквивалентного состояния машины и реализовал ее как одну гигантскую функцию, используя goto для перехода между состояниями. Это было утомительно, но результаты отлично работали. В стороне, goto является отличным инструментом для реализации государственных машин - все ваши государства могут иметь четкие метки рядом с соответствующим кодом, нет функциональных вызовов или переменных состояния накладных расходов, и это примерно так же быстро, как вы можете получить. C действительно не имеет лучшей конструкции для построения статических состояний машин.

Что-то, о чем нужно подумать: лексеры на самом деле просто специализация парсеров. Самое большое различие заключается в том, что регулярных грамматик обычно достаточно для лексического анализа, тогда как большинство языков программирования имеют (в основном) контекстно-свободные грамматики. Таким образом, на самом деле ничего не мешает вам реализовать lexer в качестве рекурсивного анализатора спуска или использовать генератор парсера для написания лексера. Это просто не так удобно, как использование более специализированного инструмента.

Ответ 2

Если вам нужен простой способ кодирования парсеров, или у вас недостаточно места, вы должны вручную обработать рекурсивный парсер спуска; это, по существу, анализаторы LL (1). Это особенно эффективно для языков, которые являются "простыми", как Basic. (Я сделал несколько из них еще в 70-х годах!). Хорошая новость заключается в том, что они не содержат никакого кода библиотеки; только то, что вы пишете.

Их довольно легко закодировать, если у вас уже есть грамматика. Во-первых, вам нужно избавиться от левых рекурсивных правил (например, X = X Y). Это, как правило, довольно легко сделать, поэтому я оставляю это как упражнение. (Вам не нужно делать это для правил формирования списка; см. обсуждение ниже).

Тогда, если у вас есть правило BNF формы:

 X = A B C ;

создать подпрограмму для каждого элемента в правиле (X, A, B, C), который возвращает логическое значение говоря: "Я видел соответствующую конструкцию синтаксиса". Для X код:

subroutine X()
     if ~(A()) return false;
     if ~(B()) { error(); return false; }
     if ~(C()) { error(); return false; }
     // insert semantic action here: generate code, do the work, ....
     return true;
end X;

Аналогично для A, B, C.

Если токен - это терминал, напишите код, который проверяет входной поток для строки символов, составляющей терминал. Например, для номера проверьте, что входной поток содержит цифры и введите поток курсора за цифрами. Это особенно удобно, если вы анализируются из буфера (для BASIC вы, как правило, получаете по одной строке) просто продвигая или не продвигая указатель сканирования буфера. Этот код является, по сути, лексерной частью анализатора.

Если ваше правило BNF рекурсивно... не беспокойтесь. Просто введите код рекурсивного вызова. Это обрабатывает грамматические правила, такие как:

T  =  '('  T  ')' ;

Это может быть кодировано как:

subroutine T()
     if ~(left_paren()) return false;
     if ~(T()) { error(); return false; }
     if ~(right_paren()) { error(); return false; }
     // insert semantic action here: generate code, do the work, ....
     return true;
end T;

Если у вас есть правило BNF с альтернативой:

 P = Q | R ;

затем код P с альтернативными вариантами:

subroutine P()
    if ~(Q)
        {if ~(R) return false;
         return true;
        }
    return true;
end P;

Иногда вы будете сталкиваться с правилами формирования списка. Они, как правило, остаются рекурсивными, и этот случай легко обрабатывается. Пример:

L  =  A |  L A ;

Вы можете указать это как:

subroutine L()
    if ~(A()) then return false;
    while (A()) do // loop
    return true;
end L;

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

Если вы действительно напряжены в пространстве, вы можете создать виртуальную машину, которая реализует эти идеи. Это то, что я сделал еще в 70-х, когда 8K 16-битные слова были тем, что вы могли получить.


Если вы не хотите кодировать это вручную, вы можете автоматизировать его с помощью метакомпилятора (Meta II), который производит практически то же самое. Это безумная техническая забава и действительно требует всей работы, даже для больших грамматик.

Август 2014 года:

Я получаю много запросов на "как построить АСТ с парсером". Для получения дополнительной информации об этом, который по существу разрабатывает этот ответ, см. Мой другой ответ SO fooobar.com/questions/79071/...

Июль 2015:

Есть много людей, которые хотят написать простой оценщик выражений. Вы можете сделать это, выполнив те же самые вещи, которые предлагает ссылка "AST-строитель" выше; просто выполняйте арифметику вместо создания узлов дерева. Здесь оценщик выражений сделал этот путь.

Ответ 3

Вы можете использовать flex/bison в Linux со своим родным gcc для генерации кода, который вы затем перекрестно скомпилируете с вашим AVR gcc для встроенной цели.

Ответ 4

GCC может перекрестно компилировать на различные платформы, но вы запускаете flex и bison на платформе, на которой запущен компилятор. Они просто выплевывают код C, который затем компилирует. Протестируйте его, чтобы увидеть, насколько велик результирующий исполняемый файл. Обратите внимание, что у них есть библиотеки времени выполнения (libfl.a и т.д.), Которые вам также придется перекрестно скомпилировать с вашей целью.

Ответ 5

Попробуйте Boost:: Spirit. Это библиотека только для заголовков, в которую вы можете зайти и создать очень быстрый, чистый парсер полностью на С++. Перегруженные операторы в С++ используются вместо специального файла грамматики.

Ответ 6

Вместо того, чтобы повторно изобретать колесо, посмотрите LUA: www.lua.org. Это интерпретируемый язык, который должен быть встроен в другое программное обеспечение и использоваться в небольших системах, таких как встроенные системы. Встроенный синтаксический синтаксический анализ, логика управления, математика и поддержка переменных - нет необходимости изобретать что-то, что тысячи других уже отлаживали и использовали. И это расширяемо, что означает, что вы можете добавить к грамматике, добавив свои собственные функции C.