|
|
|
|
|
|
Компилятор компиляторов Bison – первое знакомство |
|
|
«Теория
без применения – мертва»! Поэтому изучаемый теоретический курс по теории и
реализации языков программирования предпочтительно дополнить хотя бы кратким
знакомством с наиболее известными имеющимися программными инструментами по
автоматизации конструирования компиляторов. Один из них – Bison. На ВМК МГУ, кстати, где на изучение
подобного нашему курса отводится три семестра, проводятся и лабораторные
работы, где рассматривается применение Bison, Yacc, Lex и Super. Слишком
много времени и сил на младших курсах университета тратить на это знакомство,
конечно, тоже излишне, но если совсем не знакомится – готовность при необходимости
применить изученную теорию в разумные сроки будет слабой. 1.
Что такое Bison и зачем он нужен 2.
Кто его поддерживает и под какой ОС он пасётся 3.
Где находится исходное описание работы с Bison и где – его русский перевод 4.
Общая схема получения пользовательского компилятора с помощью Bison 4.1.
Записать КС-грамматику в понятном для Bison виде 4.2.
Запустить Bison для записанной грамматики: 4.4.
Совместная компиляция подготовленных программ обычным компилятором Си 4.5.
Запуск полученного компилятора 5.
Законченные примеры применения Bison для некоторых простых грамматик 5.1.
Пример для грамматики {а^n | n >= 0}. 5.2.
Пример для грамматики {0^n1^n | n > 0}. (Примеры
цепочек: 01, 0011, 000111, …, 0^n1^n). 5.2.1.
Входной набор данных Bison для данного языка |
|
|
|
1. Что такое Bison и зачем он нужен
Bison – это GNU-выпуск известной программы YACC, предназначенной
для порождения компиляторов по описанной пользователем КС-грамматике. Одним
из следствий расширения возможностей и доступности использования
вычислительной техники стало производство прикладных программных систем со
всё более разнообразным и сложным поведением. При этом краеугольный принцип
кибернетики - принцип необходимого и
достаточного разнообразия, сформулированный Н.Виннером и Р.Эшби ещё до С какого-то уровня разнообразия
(сложности) управления такой системой оказывается проще и надёжнее предложить
пользователю строго определённый формальный язык, с помощью которого
пользователь сообщает системе задание на выполнение тех или иных её основных
задач, в т.ч. определяет состав и периодичность выдачи учётных и
уведомительных сообщений о ходе этого выполнения и т.п. К
примеру, для управления воздушным движением, в т.ч. в Российской Федерации,
соответствующими международными организациями разработан и используется
формальный язык из нескольких десятков предложений, однозначно сообщающих в
автоматизированном и ручном режимах о каждом достаточно значимом событии в
использовании самолёта и его характеристиках: взлёте, изменении курса,
прохождении контрольной точки полёта, посадке, возникновении различных иных
вероятных обстоятельств, которые можно и нужно предвидеть. Обычным является
дополнение и уточнение время от времени этого списка команд и/или их
параметров по решению соответствующих комитетов. Эти изменения необходимо
быстро и надёжно вносить в соответствующие системы управления. Для
проверки правильности и разбора предложений подобных формальных языков, как
известно, используют программу, именуемую компилятор. Если
мы такой компилятор научимся получать автоматически на основе описывающей
этот язык грамматики, то сможем все изменения (например, дополнения) к
пользовательскому языку, вносить очень быстро и при этом, что очень важно,
вполне надёжно. Важность
и всё большая распространённость этой задачи была осознана в программировании
довольно быстро и поэтому появился ряд разнообразных программных инструментов
для автоматизированной разработки компиляторов для языков, определяемых
контекстно-свободными грамматиками. Так, само название известного более двух
десятилетий компилятора компиляторов YACC переводится как «ещё один компилятор компиляторов» (Yet another compiler compiler). В качестве пробы сил и оттачивания
мастерства производство подобных систем продолжается и сейчас, особенно в
технических университетах (см., например, http://www.forth.org.ru). Свой компилятор компиляторов под названием «СУПЕР» был
создан более 15 лет назад и в ВЦ АН СССР (ныне ВЦ РАН, www.ccas.ru
) под рук. лаур. гос. премии, зав. сект. ВЦ РАН, доц. МФТИ, к.ф.м.н.
Владимира Михайловича Курочкина, ныне покойного. Последний более 30 лет назад
основал на ФУПМе и курс «Теория и реализация языков программирования». Однако большинство этих систем (включая YACC) было разработано в коммерческом варианте, либо не обладало достаточной универсальностью, надёжностью и эффективностью. Поэтому важным событием в мире свободного (в смысле лицензии GNU) программного обеспечения стала разработка GNU-аналога компилятора YACC, получившего название BISON. Теперь этим компилятором компиляторов можно пользоваться для разработки программных комплексов с самыми высокими требованиями к качеству лицензирования применяемых инструментальных средств. 2. Кто его поддерживает и под какой ОС
он пасётся
Домашней
страницей Bison является http://www.gnu.org/software/bison/ Bison доступен для установки во всех
распространённых разновидностях ОС Linux. Возможно, он
уже установлен на вашей системе. Просто наберите bison без параметров и посмотрите на её отклик… 3. Где находится исходное описание
работы с Bison и где – его русский перевод
http://www.gnu.org/software/bison/manual/ - исходное описание руководства по Bison на английском языке (талантом писать
кратко и доступно автор наделён весьма скромно). http://www.opennet.ru/docs/RUS/bison_yacc/
- перевод этого руководства на русский язык. 4. Общая схема получения
пользовательского компилятора с помощью Bison
4.1. Записать КС-грамматику в понятном
для Bison виде
Тип
н/д должен быть “.y” (наследие YACC и способ прямой
(но не обратной, т.е Bison шире YACC) совместимости с YACC). Например, foo.y или task.y. Тип
принимаемых будущим компилятором лексем (основных знаков) определяется макро YYSTYPE, например: #define
YYSTYPE char или #define
YYSTYPE double При
этом основные знаки алфавита пользовательской грамматики обозначаются словами
из больших (латинских) букв и должны явно определяться после ключевого слова
%token, например: %token ONE, TWO // Определяются два осн.
знака ONE и TWO. Вспомогательные
знаки сразу можно использовать в правилах, их принято обозначать словами из
малых латинских букв. Например, для грамматики a^n: exp: exp A // S
-> Sa (вид обозначений в пояснениях - уже из
курса ТРЯП) exp: A // S -> a ; // или, что тоже самое: exp: exp A | A ; Начальным
знаком (аксиомой) грамматики по умолчанию считается первый употреблённый вспомогательный
знак в первом приведённом правиле грамматики. В данном случае как аксиома
будет распознана “exp”. В
фигурных скобках после каждого правила грамматики можно указать связанные с
ним действия в стиле оператора Си. Наличие таких связок (правило + действие)
говорит о том, что это не просто КС-грамматика, а атрибутная грамматика (см.
гл. 5 по курсу ТРЯП). В данном примере подсчитывается степень правильно
введённой цепочки из n знаков ‘a’ (распознаваемых в лексическом анализаторе yylex (), который пишется (обеспечивается)
пользователем). Заодно с помощью printf () показывается
порядок применения правил. exp : exp A { $$ = $1 +1; printf
("\t I'm in middle rule\n ");}
| A { $$ = 1; printf ("\t I'm in last
rule\n\n");} ; Из
особых обозначений (кроме обычных для самого языка Си) здесь используются
именователи смысловых значений правила, а именно $$ - означает имя
переменной, содержащую смысловое значение левой части правила (здесь, число
прочитанных знаков ‘a’), а $1, $2, …,
$n – соответственно имена переменных для
смысловых значений 1-го, 2-го и т.д. члена из правой части правила. 4.2. Запустить Bison для
записанной грамматики:
bison foo.y При
отсутствии ошибок вы должны получить н/д с именем foo.tab.c, содержащий текст разборщика предложений yyparse (). 4.3. Подготовить в ручную или с
использованием других инструментальных средств (например, LEX) лексический распознаватель yylex (), обработчик ошибок yyerror () и главную программу main ()
Программа
main () вызывает порождённый Bison’ом
разборщик предложений yyparse (). Последняя
программа, в свою очередь, обращается по мере необходимости за очередной
лексемой к yylex (), а при
обнаружении ошибки разбора - к yyerror (). Обратите
внимание, что в грамматике для синтаксического разборщика *yyparse ()” необходимо предусмотреть
обработку (порождение) знаков конца строки и конца всего н/д, иначе никакая
введённая цепочка не будет признана правильной, поскольку содержит хотя бы
один из этих знаков. Примеры
программирования указанных функций для соответствующих языков приведены ниже. 4.4. Совместная компиляция
подготовленных программ обычным компилятором Си
Вы можете, к примеру, добавить тексты программ yylex (), yyerror () и main () в конец н/д с текстом программы yyparse в н/д foo.tab.c, либо создать короткую программку с простейшими включениями их
текстов, например,
#include "simple_CF.tab.c" #include
"lex.c" #include
“error_handler.c” #include “main.c” И откомпилировать их обычным компилятором Си. Под Linux эта команда выглядит так (если все
программы для компиляции уже собраны в н/д simple_CF.tab.c, а исполнимую
программу мы хотим назвать simple_CF): cc -o simple_CF simple_CF.tab.c 4.5. Запуск полученного компилятора
Производится
как обычно. Для примера выше, это: ./simple_CF Знаки
“./ “ здесь, как обычно, указывают, что поиск выполнимого н/д производится в
текущей папке. 5. Законченные примеры применения
Bison для некоторых простых грамматик
5.1. Пример для грамматики {а^n | n >= 0}.
(Примеры цепочек:
а, аа, ааа, …, а^n). 5.1.1. Входной набор данных Bison,
указывающий грамматику, по которой должен быть построен текст искомого
разборщика предложений (синтаксического анализатора)
/* Simplest Context free grammar (a^n) */ %{
#define YYSTYPE char // Тип входного
знака
int
yylex (void); void yyerror (char const
*); %} // Main symbols of grammar
(synonyms: terminals, tokens, etc.) %token A
%% /*Правила грамматики и связанные с ними действия */
%% /* Grammar rules and actions
follow. */ input: /* empty */ { printf ("\n\tThis program expecting
string as a^n\n");} | input line ; line: '\n' | exp '\n' {printf
("Result power of char \"a\" is n = %i\n", $$);} ; exp: A { $$ = 1; printf ("\tI'm in last
rule\n\n");} | exp A { $$ = $1 +
1; printf ("\tI'm in middle
rule\n");} ; %% /* The lexical analyzer
returns a int code on the stack and the token
A, or the numeric code of the character read if
not a number. It skips all blanks and tabs, and returns 0
for end-of-input. */ 5.1.2. Набор данных с текстами
распознавателя слов yylex() - lexical analyzer, простого обработчика ошибок yyerror () и главной программы-испытателя main ()
//-------------------- yylex () --------------------------- #include <ctype.h> #include <stdio.h> int yylex (void) { int c; /* Skip white space. */ while ((c = getchar ())
== ' ' || c == '\t') ; /* Process symbols. */ if (c == 'a') return A; else /* Return
end-of-input. */ if (c == EOF) return 0; /* Return a single
char. */ return c; } //----------------- yyerror () ----------------------------- /* Called by yyparse on
error. */ void yyerror (char const *s) { fprintf (stderr,
"*** %s *** \n", s); fprintf (stderr, "This string is not a (a^n)\n", s); } //------------------ main () -------------------------------- int main (void) { return yyparse (); } 5.2. Пример для грамматики {0^n1^n | n > 0}.
(Примеры
цепочек: 01, 0011, 000111, …, 0^n1^n).
5.2.1. Входной набор данных Bison для
данного языка
/* Simplest Context free grammar
(0^n1^n) */ %{ #define YYSTYPE char // Type of input int yylex
(void); void yyerror (char const
*); %} // Main symbols of grammar
(synonyms: terminals, tokens, etc.) %token ONE %token TWO %% /* Grammar rules and
actions follow. */ input: /* empty */ { printf
("\n\tThis program expecting string as 0^n1^n\n");} | input line ; line: '\n' { printf ("empty line was
introduced\n");} | exp '\n'
{ printf ("\t n = %i\n", $1); } ; exp: ONE exp TWO { $$ = $2 + 1;} | ONE TWO { $$ = 1; } ; %% /* The lexical analyzer
returns the tokens ONE or TWO, or the numeric code of the character read if
not a number. It skips all blanks and
tabs, and returns 0 for end-of-input. */ 5.2.2. Набор данных с текстами распознавателя
слов yylex() - lexical analyzer, простого обработчика ошибок yyerror () и главной программы-испытателя main ()
//-------------------- yylex () --------------------------- #include <ctype.h> #include <stdio.h> int yylex (void) { int c; /* Skip white space */ while ((c = getchar ())
== ' ' || c == '\t') ; /* Process symbols */ if (c == '0') { n1++; return ONE; } else if (c == '1') { n2++; return TWO; } else /* Return
end-of-input. */ if (c == EOF) return 0; /* Return a single
char. */ return c; } //----------------- yyerror () ----------------------------- /* Called by yyparse on
error. */ void yyerror (char const *s) { fprintf (stderr,
"stderror: %s\n", s); } //------------------ main () -------------------------------- int main (void) { return yyparse (); } |
|
|
|
|
|
|