Yyparse что такое
Перейти к содержимому

Yyparse что такое

  • автор:

Компиляция. 3: бизон

Это единственный пост в серии, в центре внимания которого — старообрядный сишный бизон, так надоевший некоторым. Тем, кто пишет не на Си, пост всё равно должен быть интересен, потому что похожие по принципу работы генераторы LR-парсеров существуют для очень многих языков. Тех же, кто идеологически не приемлет LR-парсеры, мне сегодня привлечь нечем.

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

  1. Компиляция грамматики
  2. Двухступенчатый парсер
  3. Что у него внутри?
  4. Конфликты в грамматике
  5. Как это работает?
EXPR: TERM | EXPR '+' TERM | EXPR '-' TERM ; TERM: NUM | TERM '*' NUM | TERM '/' NUM ; NUM: DIGIT | NUM DIGIT ; DIGIT: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;

Закончили тем, что пристально рассмотрели автомат, который её парсит.
Придумывать парсящие автоматы сами мы не умеем, да и не надо — ведь бизон умеет строить их за нас!
С бизоньей помощью сможем скомпилировать наш парсер, и поглядеть, как всё это работает взаправду.

Компиляция грамматики

Общий принцип такой же, как с flex в первой части: мы перечисляем правила грамматики, и напротив каждого правила пишем сишный код, который будет выполняться во время свёртки правила.

В прошлый раз упомянули, что во время свёртки мы вынимаем из стека набор символов (строк или переменных), смотрим на их значения, задаём значение для свернувшейся переменной, и кладём её в стек вместо вынутых.

Разбор кода

Так же, как в lex -файлах, код в тегах % копируется в парсер неизменным. Есть две функции, которые необходимо определить: int yylex() возвращает следующий входной символ, и void yyerror(char *s) печатает сообщение об ошибке. Классический yacc включал готовую реализацию yyerror , которую можно было в случае необходимости переобъявить; но его GNU-клон bison требует от программиста явную реализацию.

%%, как и в lex -файлах, разделяет область объявлений и область правил грамматики. Правила перечисляются в том же виде, как мы уже привыкли; в конце правила добавляем код свёртки. В коде свёртки, чтобы сослаться на значения символов на стеке парсера, используем $-теги. $$ ссылается на сворачиваемую переменную (левую часть правила), $1 на самый левый символ в правой части правила (самый глубокий в стеке парсера), $2 на второй слева, и т.д. Если в правой части правила N символов, то можно пользоваться значениями от $1 до $N .
Если код свёртки не задан, и в правой части правила один символ, то по умолчанию бизон «наследует» его значение: $$=$1

Самая первая объявленная переменная считается «корнем» грамматики, т.е. весь входной текст должен в итоге свернуться в этот корень. EXPR подходил бы в качестве корня, но тогда печать вычисленного выражения пришлось бы засунуть в свёртку EXPR ; а значит, печаталось бы ещё и значение каждого подвыражения по дороге. Для удобства печати заведём новую переменную EVALUATE , которая используется только в качестве корня. Это заодно даёт возможность прочитать в конце выражения перевод строки — кроме удобства печати, получаем удобство ввода.

При компиляции парсера нужно указать библиотеку liby , в которой лежит стандартная бизонья main() .
[tyomitch@home ~]$ bison 2.y
[tyomitch@home ~]$ cc -ly 2.tab.c
[tyomitch@home ~]$ ./a.out
22+3*4-5
=29
[tyomitch@home ~]$ ./a.out
2 + 2
syntax error

В отличие от калькулятора торговок с рынка, который умел игнорировать пробелы и комментарии, бизоний калькулятор понимает строго заданный синтаксис: цифры, знаки операций, перевод строки в конце. Чтобы предусмотреть, например, пробелы между любой парой символов, пришлось бы добавлять их в грамматику явно:
EXPR: TERM | EXPR WS ‘+’ WS TERM | EXPR WS ‘-‘ WS TERM ;
TERM: NUM | TERM WS ‘*’ WS NUM | TERM WS ‘/’ WS NUM ;
NUM: DIGIT | NUM DIGIT ;
DIGIT: ‘0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’ ;
WS: | WS ‘ ‘ ;

Ясно, что это неудобно. Да и писать для распознавания цифр 10 разных правил неудобно; а если бы нам нужны были латинские буквы, например в именах переменных, мы бы задавали 52 правила?!

До чего здорово было бы совместить преимущества flex и bison ! Чтобы простые выражения (числа, пробелы) распознавать было просто, и чтобы сложные выражения распознавать было возможно.

Двухступенчатый парсер

Если у нас уже есть flex -парсер, который успешно распознаёт числа и удаляет комментарии и пробелы, то прогоним входной текст через него; а то, что получится, прогоним через продвинутый бизоний парсер. На самом деле, нет надобности хранить промежуточный результат: обе ступени можно выполнить за один проход. flex читает входной текст символ за символом, время от времени передавая «наверх» бизону токены — терминальные символы грамматики. Значения для токенов flex задаёт сам.

Чтобы такой симбиоз был возможен, flex должен как-то узнать терминальные символы бизоньей грамматики. Будем запускать bison с ключом -d , тогда он сгенерирует .h -файл с перечислением всех терминалов. Для этого нужно объявить (указанием %token ) терминалы в файле грамматики — и останется лишь сослаться на сгенерированный .h -файл в файле .lex .
% <
#include
void yyerror( char *s) <
fprintf ( stderr , » %s \n » , s);
>
%>

%%

Функция yylex нам больше не нужна: теперь входные символы будут поступать не из stdin , а от flex .
Кроме того, стёрли ‘\n’ после EXPR (его проглотит flex ), и убрали все правила про NUM и DIGIT .

Соответствующий файл .lex :
% <
#include «3.tab.h»
%>

%option yylineno
%option noyywrap

%%

Файл с определениями токенов получает суффикс .tab.h
Единственное, что в нём есть — #define NUM 258
Все токены получают номера выше 256, чтобы отличаться от «обычных» символов.

Чтобы передать токен бизону, его значение записываем в глобальную (ох ужас!) переменную yylval , и возвращаем код токена.
Обычные символы передаём обычным способом (возвращаем код символа).

Опция noyywrap указывает, что входной текст один, и после чтения EOF не нужно пытаться перейти к следующему тексту. Эта опция устанавливалась автоматически, пока мы пользовались %option main , задававшей чтение с stdin . Сейчас main() будет бизонья, поэтому нам не нужно ни просить у flex стандартную main() , ни писать свою.

Компилируем двухступенчатый парсер:
[tyomitch@home ~]$ lex 3.lex
[tyomitch@home ~]$ bison -d 3.y
[tyomitch@home ~]$ cc -ly lex.yy.c 3.tab.c
[tyomitch@home ~]$ ./a.out
22+ // hello
3*4 — 5
=29
[tyomitch@home ~]$ ./a.out
22+x
syntax error

По поводу терминологии: в такой двухступенчатой модели нижний парсер, распознающий токены, по традиции называют лексическим анализатором («лексером»), а верхний, распознающий конструкции из токенов — синтаксическим анализатором. Это указание именно роли парсера, а не его устройства: другие системы разбора могут, например, использовать для обеих ступеней магазинно-автоматные парсеры.

Что у него внутри?

Чтобы увидеть сгенерированный автомат, не нужно нырять в пучины сишного кода: у бизона могучие средства для отладки грамматик. Указываем ключ -v , и глядим в файл с суффиксом .output .
После переноса парсинга чисел в лексер, в автомате осталось 14 состояний, и описаны они примерно так:

. state 7 4 EXPR: EXPR '-' . TERM NUM shift, and go to state 1 TERM go to state 11 state 8 6 TERM: TERM '*' . NUM NUM shift, and go to state 12 state 9 7 TERM: TERM '/' . NUM NUM shift, and go to state 13 state 10 3 EXPR: EXPR '+' TERM . 6 TERM: TERM . '*' NUM 7 | TERM . '/' NUM '*' shift, and go to state 8 '/' shift, and go to state 9 $default reduce using rule 3 (EXPR) .

Для каждого состояния указаны правила, которые в этом состоянии распознаются (вместе с их номерами в грамматике), и перечислены действия, выполняемые для каждого входного символа. Не указано следующее состояние после свёртки; вместо этого автомат возвращается в состояние, прочитанное из стека, и в нём ищет правило «go to», соответствующее свежесвёрнутому нетерминалу. Таким образом, таблица переходов автомата получается двумерной: в каждом состоянии действие зависит только от входного символа, и не зависит от содержимого стека. (Но из стека берётся следующее состояние после свёртки.)

Чтобы не ползать с карандашом по распечатке автомата, подставляя в него символ за символом, можно попросить бизона во время парсинга печатать все переходы из состояния в состояние. Для этого компилируем грамматику с ключом -t , и в парсере появится глобальный флаг yydebug . Его нужно установить в 1 — например, в main() .
Если мы, кроме того, хотим, чтобы печатались значения символов, то нужно определить макрос YYPRINT :
% <
#include
void yyerror( char *s) <
fprintf ( stderr , » %s \n » , s);
>
#define YYPRINT(file, type, value) fprintf(file, » %d » , value);
%>

%%
int main ()

Код после второго тега %% копируется в парсер неизменным так же, как если бы он был в % .
Теперь, раз мы определили main() сами, уже не нужно при компиляции подключать liby :
[tyomitch@home ~]$ lex 3.lex
[tyomitch@home ~]$ bison -dt 3.y
[tyomitch@home ~]$ cc lex.yy.c 3.tab.c
[tyomitch@home ~]$ ./a.out
Starting parse
Entering state 0
Reading a token: 22+3*4-5
Next token is token NUM (22)
Shifting token NUM, Entering state 1
Reducing stack by rule 5 (line 20), NUM -> TERM
Stack now 0
Entering state 4
Reading a token: Next token is token ‘+’ (22)
Reducing stack by rule 2 (line 15), TERM -> EXPR
Stack now 0
Entering state 3
Next token is token ‘+’ (22)
Shifting token ‘+’, Entering state 6
Reading a token: Next token is token NUM (3)
Shifting token NUM, Entering state 1
Reducing stack by rule 5 (line 20), NUM -> TERM
Stack now 0 3 6
Entering state 10
Reading a token: Next token is token ‘*’ (3)
Shifting token ‘*’, Entering state 8
Reading a token: Next token is token NUM (4)
Shifting token NUM, Entering state 12
Reducing stack by rule 6 (line 21), TERM ‘*’ NUM -> TERM
Stack now 0 3 6
Entering state 10
Reading a token: Next token is token ‘-‘ (4)
Reducing stack by rule 3 (line 16), EXPR ‘+’ TERM -> EXPR
Stack now 0
Entering state 3
Next token is token ‘-‘ (4)
Shifting token ‘-‘, Entering state 7
Reading a token: Next token is token NUM (5)
Shifting token NUM, Entering state 1
Reducing stack by rule 5 (line 20), NUM -> TERM
Stack now 0 3 7
Entering state 11
Reading a token: Now at end of input.
Reducing stack by rule 4 (line 17), EXPR ‘-‘ TERM -> EXPR
Stack now 0
Entering state 3
Now at end of input.
Reducing stack by rule 1 (line 13), EXPR -> EVALUATE
=29
Stack now 0
Entering state 2
Now at end of input.

Из стека печатаются только состояния; типы символов в стеке, и их значения, остаётся угадывать из контекста.
Если макрос YYPRINT не задан, тогда угадывать придётся и значения прочитанных токенов: бизон будет печатать только пустые скобки.

Конфликты в грамматике

В прошлый раз упомянули неоднозначные грамматики, допускающие для одного выражения несколько вариантов разбора.
Что скажет бизон, если попытаемся скомпилировать неоднозначную грамматику?
% <
#include
void yyerror( char *s) <
fprintf ( stderr , » %s \n » , s);
>
%>

[tyomitch@home ~]$ bison 4.y
4.y: conflicts: 16 shift/reduce

Когда из одного состояния грамматика допускает несколько продолжений, бизону непонятно, что именно выполнять. В нашем случае он колеблется между сдвигом и свёрткой. Можно исправить грамматику, как мы в прошлый раз сделали; а можно «подтолкнуть» бизона в нужную сторону, и намекнуть, что делать в случае конфликта. Стоит относиться к этому как к быстрому хаку: парсер начинает работать, но отлаживать «грамматику с намёками» становится сложнее.

Поскольку типичный источник конфликтов — арифметические выражения, то и намёки даются в виде указания приоритета операторов (выполнять умножение раньше сложения) и их ассоциативности (из операторов с равным приоритетом, который выполнять раньше). Чем ниже оператор в списке, тем выше приоритет. Операторы в одной строчке списка получают одинаковый приоритет.

% <
#include
void yyerror( char *s) <
fprintf ( stderr , » %s \n » , s);
>
%>

%%

Для правоассоциативных операторов есть директива %right .
Противоестественность хака с приоритетами можно оценить на примере двусмысленности if (1) if (2) foo(); else bar();
Чтобы она распарсилась привычным образом — if (1) < if (2) foo(); else bar(); >— нужно, чтобы приоритет else был выше, чем у ‘)’
Оба этих терминала тяжело назвать операторами, и тем более тяжело задать им «естественный» приоритет.
Зато это работает!

«Грамматика с намёками» компактнее однозначной и в исходном виде (короче вдвое), и в виде автомата (сэкономили одно состояние).

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

WORD: S1 'a' 'i' 'l' | S2 'a' 'l' 'e' ; S1: 's' ; S2: 's' ;

— однозначная, и ей соответствует всего два слова — sail и sale. Когда парсер сдвинул первую букву ‘s’ и видит после неё ‘a’, он не может сделать выбор, сворачивать S1 или S2 : правильная свёртка зависит от того, какая буква будет после ‘a’; но её автомат ещё не видит.
Это вторая причина, по которой парсер делают двухступенчатым: за счёт того, что лексер сжимает строки в токены и отбрасывает ненужные символы между токенами, LR-парсеру удаётся «дальше» заглядывать: не на одну букву, а на один токен вперёд.

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

Как и у flex , ядро парсера — таблица переходов и небольшой цикл. Используется два параллельных стека: стек состояний yyssa и стек значений yyvsa — всё равно состояния и символы входят и выходят из стека всегда парами.

Как и в прошлый раз, символы, идентичные с точки зрения парсера, объединены в классы. В данном случае, классов 7, и они перечислены в файле .output . Массив static const unsigned char yytranslate[259] сопоставляет всем терминалам класс. (От 0 до 255 — обычные символы; 258 — объявленный нами терминал NUM ).

Таблицы переходов хитроумно объединены. В основной таблице хранятся описания действий: для сдвига (положительное число) — в какое состояние перейти; для свёртки (отрицательное) — по какому правилу свернуть.

static const unsigned char yytable[] = < 6, 7, 5, 8, 9, 10, 11, 1, 12, 13 >;

Удивительно, что таблица не только одномерная, но даже элементов в ней меньше, чем наших состояний (их 14).
Трюк в том, что индекс первого действия для каждого состояния хранится в отдельной таблице:

#define YYPACT_NINF -5 static const yysigned_char yypact[] = < 4, -5, 2, -4, -3, -5, 4, 4, 5, 6, -3, -3, -5, -5 >;

YYPACT_NINF означает, что состоянию не соответствует ни один элемент yytable ; иначе говоря, выполняемое действие не зависит от входного символа.
Действия по умолчанию для каждого состояния хранятся в другой отдельной таблице:

static const unsigned char yydefact[] = < 0, 6, 0, 2, 3, 1, 0, 0, 0, 0, 4, 5, 7, 8 >;

Не зависеть от входного символа может только выполнение свёртки, поэтому значения в yydefact — номера правил грамматики, по которым нужно сворачивать.

По индексу из yypact[n] хранится действие для состояния n и для класса символов 0. Действие для класса символов k хранится в yytable[yypact[n]+k] ; поэтому в yypact могут быть отрицательные индексы — это лишь «база», к которой будет прибавляться номер класса.

Чтобы проверить, к которому символу относится каждое действие в yytable , есть ещё одна таблица:

static const unsigned char yycheck[] = < 4, 5, 0, 6, 7, 6, 7, 3, 3, 3 >;

Что мы видим? yytable[0] относится к символам класса 4, yytable[1] к символам класса 5, и так далее.
Попробуем найти какое-нибудь действие, например приведённые выше:

. state 7 4 EXPR: EXPR '-' . TERM NUM shift, and go to state 1 TERM go to state 11 state 8 6 TERM: TERM '*' . NUM NUM shift, and go to state 12 .

Класс терминала NUM — 3.
Ищем первый сдвиг: yytable[yypact[7]+3]==yytable[4+3]==1 (и действительно, yycheck[yypact[7]+3]==3 )
Второй сдвиг: yytable[yypact[8]+3]==yytable[5+3]==12 (и действительно, yycheck[yypact[7]+3]==3 )

Аналогичным образом побита на три массива таблица «go to», которая задаёт, в какое состояние нужно перейти после свёртки.

Код собственно парсера: (неинтересные куски вырезаны, а интересные прокомментированы)
yynewstate :
*++yyssp = yystate; // кладём в стек текущее состояние

yyn = yypact[yystate]; // индекс первого действия
if (yyn == YYPACT_NINF)
goto yydefault; // не зависит от входного символа

yychar = yylex(); // читаем входной символ
yytoken = yytranslate[yychar]; // определяем класс
yyn += yytoken; // индекс внутрь yytable
if (yyn < 0 || YYLAST < yyn || yycheck[yyn] != yytoken)
goto yydefault; // нет подходящего действия
yyn = yytable[yyn];

if (yyn yyn = -yyn;
goto yyreduce;
>

*++yyvsp = yylval; // сдвигаем
yystate = yyn; // следующее состояние
goto yynewstate;

yydefault :
yyn = yydefact[yystate];
if (yyn == 0 ) // ошибка синтаксиса:
goto yyerrlab; // ни одно действие не подошло,
// и нет действия по умолчанию
yyreduce :
yylen = yyr2[yyn]; // длина правой части правила

yyval = yyvsp[ 1 -yylen]; // по умолчанию: унаследовать $1

// действия при свёртке:
// вместо $-тегов бизон подставил yyval слева и yyvsp[] справа

yyvsp -= yylen; // удаление из стека
yyssp -= yylen;

*++yyvsp = yyval; // кладём свежесвёрнутую переменную

yyn = yyr1[yyn]; // номер переменной по номеру правила

// вычисление следующего состояния после свёртки
yystate = yypgoto[yyn — YYNTOKENS] + *yyssp;
if ( 0 yystate = yytable[yystate];
else
yystate = yydefgoto[yyn — YYNTOKENS];

goto yynewstate;

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

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

Yyparse что такое

Анализатор Bison — это на самом деле функция на C, называющаяся yyparse . Здесь мы опишем соглашения по интерфейсу yyparse и других необходимых ей функций.

Имейте в виду, что для своих внутренних целей анализатор использует множество идентификаторов C, начинающихся с `yy’ и `YY’ . Если вы используете такой идентификатор (кроме тех, что описаны в настоящем руководстве) в действии или дополнительном коде на C в файле грамматики, вероятно, вы столкнётесь с неприятностями.

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

Если разбор завершён успешно (возврат вызван концом входного текста), yyparse возвращает значение 0.

Значение 1 возвращается, если разбор не удался (возврат вызван синтаксической ошибкой).

В действии вы можете потребовать немедленного возврата из yyparse , используя следующие макросы: YYACCEPT Немедленный возврат со значением 0 (сообщение об удачном разборе). YYABORT Немедленный возврат со значением 1 (сообщение об ошибке).

Функция лексического анализатора yylex распознаёт лексемы во входном потоке и передаёт их анализатору. Bison не создаёт эту функцию автоматически, вы должны написать её так, чтобы yyparse могла вызывать её. Эту функцию иногда называют лексическим сканером.

В простых программах yylex часто определяется в конце файла грамматики Bison. Если yylex определена в отдельном исходном файле, вам нужно сделать доступными там макроопределения типов лексем. Для этого используйте параметр `-d’ при запуске Bison, чтобы он записал эти макроопределения в отдельный файл заголовка ` имя .tab.h’ , который вы можете включить в другие исходные файлы, которым он нужен. См. раздел 10. Вызов Bison.

Значение, возвращаемое yylex должно быть числовым кодом типа только что встреченной лексемы, или 0 для обозначени конца входного текста.

Если в правилах грамматики на лексему ссылаются по имени, это имя становится в файле анализатора макросом C, определением которого будет числовой код, соответствующий этому типу лексемы. Таким образом, yylex может использовать для обозначения типа это имя. См. раздел 4.2 Символы, терминальные и нетерминальные.

Если в правилах грамматики на лексему ссылаются с помощью однолитерной константы, числовой код этой литеры также является кодом типа лексемы. Таким образом yylex может просто вернуть код этой литеры. Нулевая литера не должна использоваться таким образом, потому что её код — ноль, означающий конец входного текста.

Приведём пример, иллюстрирующий это:

int yylex (void) < . if (c == EOF) /* Проверка конца файла. */ return 0; . if (c == '+' || c == '-') return c; /* Полагаем, что тип лексемы `+' -- '+'. */ . return INT; /* Вернуть тип лексемы. */ . >

Этот интерфейс разрабатывался так, чтобы выход утилиты lex мог быть без изменений использован как определение yylex .

  • Если грамматика определяет символические имена лексем как псевдонимы строковых лексем, yylex может использовать эти символические имена как и все остальные. В этом случае использование строковых лексем в файле грамматики не окажет влияния на yylex .
  • yylex может найти многолитерную лексему в таблице yytname . Индекс лексемы в этой таблице — это код типа лексемы. Имя многолитерной лексемы записывается в yytname в виде: двойная кавычка, литеры лексемы, вторая двойная кавычка. Литеры лексемы никаким образом не экранируются, они дословно переносятся в содержимое строки в таблице. Приведём код поиска лексемы в yytname , полагая, что литеры лексемы находятся в массиве token_buffer .


for (i = 0; i

В обычном (не повторно входимом) анализаторе семантические значения лексем должны помещаться в глобальную переменную yylval . Если вы используете единственный тип данных для семантических значений, yylval имеет этот тип. Так, если этот тип int (по умолчанию), вы можете написать в yylex :

. yylval = value; /* Поместить значение на вершину стека Bison. */ return INT; /* Вернуть тип лексемы. */ .

Если вы используете множественные типы данных, тип yylval — объединение типов, полученное из объявления %union (см. раздел 4.7.3 Набор типов значений). Так, если вы сохраняете значение лексемы, вы должны использовать правильный элемент объединения. Если объявление %union выглядит так:

%union

то код в yylex может выглядеть так:

. yylval.intval = value; /* Поместить значение на вершину */ /* стека Bison. */ return INT; /* Вернуть тип лексемы. */ .

Если вы используете в действиях `@ n ‘ -свойства (см. раздел 4.6 Отслеживание положений) для отслеживания положений лексем и групп в тексте, ваша функция yylex должна предоставить эту информацию. Функция yyparse ожидает, что положение только что разобранной лексемы в тексте находится в глобальной переменной yylloc . Таким образом, yylex должна поместить в эту переменную правильные данные.

По умолчанию значение yylloc — это структура, и вам нужно только проинициализировать её элементы, которые вы собираетесь использовать в действиях. Эти четыре элемента называются, first_line , first_column , last_line и last_column . Отметим, что использование этих свойств делает анализатор заметно более медленным.

Тип данных yylloc называется YYLTYPE .

Если вы используете объявление Bison %pure_parser , требующее создания чистого, повторно входимого анализатора, глобальные переменные взаимодействия yylval и yylloc использовать нельзя (см. раздел 4.7.7 Чистый (повторно входимый) анализатор). В таких анализаторах эти две глобальные переменные замещаются указателями, передаваемыми в качестве аргументов функции yylex . Вы должны объявить их, как здесь показано, и передавать информацию назад, помещая её по этим указателям.

int yylex (YYSTYPE *lvalp, YYLTYPE *llocp) < . *lvalp = value; /* Поместить значение на вершину стека Bison. */ return INT; /* Вернуть тип лексемы. */ . >

Если файл грамматики не использует конструкции `@’ для ссылок на позиции в тексте, тип YYLTYPE не будет определён. В этом случае опустите второй аргумент, yylex будет вызываться только с одним аргументом.

Если вы используете повторно входимый анализатор, вы можете (необязательно) передавать ему информацию о дополнительных параметрах повторно входимым способом. Для этого определите макрос YYPARSE_PARAM как имя переменной. Это изменит функцию yyparse чтобы она принимала один аргумент с этим именем типа void * .

При вызове yyparse передайте адрес объекта, приведя его к типу void * . Действия грамматики могут ссылаться на сожержимое объекта, приводя значение указателя обратно к его правильному типу, и затем разыменовывая его. Приведём пример. Напишите в анализаторе:

%< struct parser_control < int nastiness; int randomness; >; #define YYPARSE_PARAM parm %>

Затем вызовите анализатор следующим образом:

struct parser_control < int nastiness; int randomness; >; . < struct parser_control foo; . /* Поместить правильные данные в foo. */ value = yyparse ((void *) &foo); . >

В действиях грамматики используйте для обращения к данным выражения наподобие следующего:

((struct parser_control *) parm)->randomness

Если вы хотите передать данные дополнительных параметров функции yylex , определите макрос YYLEX_PARAM тем же способом, что и для YYPARSE_PARAM , как показано ниже:

%< struct parser_control < int nastiness; int randomness; >; #define YYPARSE_PARAM parm #define YYLEX_PARAM parm %>

Затем вам следует определить yylex , чтобы она принимала дополнительный аргумент — значение parm (всего будет два или три аргумента, в зависимости от того, передаётся ли аргумент типа YYLTYPE ). Вы можете объявить аргумент как указатель на правильный тип объекта, или же объявить его как void * и получать доступ к содержимому как показано выше.

Вы можете использовать `%pure_parser’ и потребовать создания повторно входимого анализатора, не используя при этом YYPARSE_PARAM . Тогда вам следует вызывать yyparse без аргументов, как обычно.

Анализатор Bison обнаруживает ошибку разбора или синтаксическую ошибку каждый раз, когда читает лексему, которая не может удовлетворять никакому синтаксическому правилу. Действие в грамматике может также явно сообщить об ошибке, используя макрос YYERROR (см. раздел 5.4 Специальные возможности, используемые в действиях).

Анализатор Bison рассчитывает сообщить об ошибке, вызывая функцию сообщения об ошибке yyerror , которую должны предоставить вы. Она вызывается функцией yyparse каждый раз при обнаружении синтаксической ошибки, и принимает один аргумент. В случае ошибки разбора это обычно строка «parse error» .

Если вы определите макрос YYERROR_VERBOSE в секции объявлений Bison (см. раздел 4.1.2 Секция объявлений Bison), Bison будет давать более подробные и обстоятельные строки сообщений об ошибках, вместо обычного «parse error» . Не имеет значения, какое определение вы используете для YYERROR_VERBOSE , только то, определили ли вы его.

Анализатор может обнаружить ещё один тип ошибки — переполнение стека. Это происходит, когда входной текст содержит конструкции слишком большой глубины вложенности. Маловероятно, что вы столкнётесь с этим, поскольку анализатор Bison расширяет свой стек автоматически до очень больших пределов. Но если переполнение всё же происходит, yyparse вызывает обычным образом yyerror , за исключением того, что аргументом будет строка «parser stack overflow» .

Следующего определения достаточно для простых программ:

void yyerror (char *s)

После возвращения из yyerror в yyparse последняя попытается произвести восстановление после ошибки, если в грамматике вы написали подходящие правила восстановления после ошибок (см. раздел 7. Восстановление после ошибок). Если восстановление невозможно, yyparse немедленно завершит работу, вернув 1.

Переменная yynerrs содержит число обнаруженных до сих пор синтаксических ошибок. Обычно эта переменная глобальная, но если вы требуете создания чистого анализатора (см. раздел 4.7.7 Чистый (повторно входимый) анализатор), это локальная переменная, к которой могут иметь доступ только действия.

Ниже представлена таблица конструкций Bison, переменных и макросов, которые могут быть полезны в действиях. `$$’ Играет роль переменной, содержащей семантическое значение группы, собираемой текущим правилом. См. раздел 4.5.3 Действия. `$ n ‘ Играет роль переменной, содержащей семантическое значение n -го компонента текущего правила. См. раздел 4.5.3 Действия. `$< тип_альт >$’ Аналогична $$ , но задаёт альтернативу тип_альт в объединении, заданном объявлением %union . См. раздел 4.5.4 Типы данных значений в действиях. `$ < тип_альт >n ‘ Аналогична n , но задаёт альтернативу тип_альт в объединении, заданном объявлением %union . См. раздел 4.5.4 Типы данных значений в действиях. `YYABORT;’ Немедленно завершает работу yyparse , сообщая об ошибке. См. раздел 5.1 Функция анализатора yyparse . `YYACCEPT;’ Немедленно завершает работу yyparse , сообщая об удачном разборе. См. раздел 5.1 Функция анализатора yyparse . `YYBACKUP ( лексема , значение );’ Отмена сдвига лексемы. Этот макрос допустим только в правилах, которые выполняют свёртку единственного значения, и только когда нет предпросмотренной лексемы. Он устанавливает для предпросмотренной лексемы тип лексема и семантическое значение значение . Затем он отбрасывает значение, которое должно быть свёрнуто по этому правилу. Если макрос используется, когда его применение недопустимо, как например, когда уже есть предпросмотренная лексема, он сообщает о синтаксической ошибке сообщением `cannot back up’ и производит обычное восстановление после ошибки. В любом случае оставшаяся часть правила не выполняется. `YYEMPTY’ Значение, помещаемое в yychar , когда там нет предпросмотренной лексемы. `YYERROR;’ Немедленно вызывает синтаксическую ошибку. Этот оператор запускает восстановление после ошибки, как если бы ошибку обнаружил сам анализатор, и не выводит никакого сообщения. Если вы хотите вывести сообщение об ошибке, перед оператором `YYERROR;’ вызовите явно yyerror . См. раздел 7. Восстановление после ошибок. `YYRECOVERING’ Этот макрос заменяет выражение, имеющее значение 1 когда анализатор выполняет восстановление после синтаксической ошибки, и 0 всё остальное время. См. раздел 7. Восстановление после ошибок. `yychar’ Переменная, содержащая текущую предпросмотренную лексему (в чистом анализаторе это на самом деле локальная для yyparse переменная). Когда предпросмотренной лексемы нет, в неё помещается значение YYEMPTY . См. раздел 6.1 Предпросмотренные лексемы. `yyclearin;’ Отбросить текущую предпросмотренную лексему. Это полезно, прежде всего, в правилах обработки ошибок. См. раздел 7. Восстановление после ошибок. `yyerrok;’ Немедленно взобновляет создание сообщений об ошибках для последующих синтаксических ошибок. Это полезно, прежде всего, в правилах обработки ошибок. См. раздел 7. Восстановление после ошибок. `@$’ Играет роль структурной переменной, содержащей информацию о позиции в тексте группы, создаваемой текущим правилом. См. раздел 4.6 Отслеживание положений. `@ n ‘ Играет роль структурной переменной, содержащей информацию о позиции в тексте n -го компонента текущего правила. См. раздел 4.6 Отслеживание положений.

printРазработка компиляторов и интерпретаторов

printПринципы работы BISON, интеграция BISON и FLEX, обработка синтаксических ошибок

По описанию BISON генерирует автомат с магазинной памятью. В стеке хранятся символы грамматики и ассоциированные с ними значения, полученные в процессе разбора или из лексического анализатора (токены). Работа автомата определяется набором таблиц, связывающих текущее состояние стека и очередной входной токен с действием автомата. Возможно только два варианта действия:

  • свёртка (reduce) – применение одного из правил грамматики и превращение нескольких верхних символов в нетерминальный символ-результат (при этом выполняется код, указанный после правила);
  • сдвиг (shift) – помещение токена на стек и ввод следующего токена.

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

Если написанный вами набор правил некорректен, возможны два вида конфликтов:

  • свёртка/свёртка – есть несколько правил, которые можно применить для некоторой последовательности символов в стеке;
  • сдвиг/свёртка – существует как правило, которое можно применить, так и правило, в котором после последовательности символов из первого правила есть другие символы.

От конфликтов свёртка/свёртка нужно обязательно избавляться, но небольшое количество конфликтов сдвиг/свёртка может присутствовать из-за неоднозначности синтаксиса языка. Например, следующие правила дадут 1 конфликт сдвиг/свёртка:

operator : | IF expr THEN operator | IF expr THEN operator ELSE operator | IDENT '=' expr ;

При наличии такого конфликта всегда выполняется сдвиг, поэтому конструкция «if a then if b then c=x else c=y» интерпретируется как «if a then «, а не как «if a then else c=y». Обычно количество таких конфликтов не должно превышать 1-2, и чтобы BISON не давал предупреждений при обработке правил для подобных конструкций, применяется опция %expect n, где n – ожидаемое количество конфликтов сдвиг/свёртка.

Из описания BISON генерирует функцию yyparse(), которая для получения токена вызывает функцию yylex(). Функция yylex() должна вернуть тип лексемы (целое число), а ассоциированное с ней значение поместить в глобальную переменную yylval (или в параметр функции, если установлена опция %define api.pure full). Именованные константы для токенов определяются в описании для BISON с помощью %token, и чтобы они стали доступны в программе, сгенерированной FLEX, необходимо создать заголовочный файл и подключить его в описании лексического анализатора. Для этого в командной строке при генерации необходимо указать ключ -d:

bison -oy.tab.cpp -d my.grm

Первый ключ -o указывает имя файла для сгенерированного модуля, и в зависимости от расширения выходного файла будет создан заголовочный файл y.tab.hpp или y.tab.h. В заголовочный файл также попадает объявление типа-объединения и глобальной переменной yylval. Но информация о соответствии токенов с полями типа-объединения не будет доступна. Поэтому при помещении значения токена в yylval необходимо указывать имя поля явно.

По умолчанию функции yyerror передается сообщение «syntax error». Для более подробной информации нужно указать опцию

%define parse.error verbose

По умолчанию функция yyparse завершает разбор после обнаружения синтаксической ошибки, но можно продолжить анализ входного текста, чтобы найти другие ошибки или выполнить следующие команды в интерактивном интерпретаторе. Для этого используется специальный символ error. Пи ошибке разбора вызывается функция yyerror и выполняется поиск подходящего правила с error, из стека разбора удаляются верхние элементы, пока его состояние не будет соответствовать состоянию, при котором возможно добавление символа error, затем будут пропущены входные токены, пока не будет найден токен, указанный после символа error, или токен, который может встретиться после символа-результата правила с error. После применения правила для ошибки разбор будет продолжен, но новые сообщения об ошибке не выводятся, пока не будет выполнена хотя бы одна свёртка без error. Вы можете контролировать количество обнаруженных ошибок в функции yyerror. В действии для правила с error можно использовать вспомогательные макросы (всегда в паре!): yyerrok (обработка ошибки завершена и можно сообщать о следующей синтаксической ошибке) и yyclearin (перейти к следующему токену).

Пример программы
Лексический анализатор calc.lex:

%option noyywrap interactive bison % < #include "y.tab.h" #include int findid(const char *); %> %% [a-zA-Z][a-zA-Z0-9_]* < yylval=findid(yytext);8 return IDENT; >[0-9]+ < sscanf(yytext,"%d",&yylval); return NUMBER; >[\t ] ; \n|. return yytext[0]; %%

Синтаксический анализатор calc.grm:

%start program %token NUMBER "число" IDENT "идентификатор" %left '+' '-' %left '*' '/' %left UMINUS %define parse.error verbose % < #include int mem[1000]; void yyerror(const char *); int yylex(); %> %% program: | program operator '\n' ; operator: IDENT '=' expr < mem[$1]=$3; printf("%d\n",mem[$1]);>| expr < printf("%d\n",$1);>| error ; expr: expr '+' expr < $$ = $1+$3; >| expr '-' expr < $$ = $1-$3; >| expr '*' expr < $$ = $1*$3; >| expr '/' expr < $$ = $1/$3; >| '-' expr %prec UMINUS < $$ = -$2; >| '(' expr ')' < $$ = $2; >| NUMBER < $$ = $1; >| IDENT < $$ = mem[$1]; >;

Вспомогательные функции calc.cpp:

#include #include using namespace std; mapint> ti; int yyparse(); extern int yylineno; int findid(const char *yytext) < if(ti.find(yytext)==ti.end()) < int k=ti.size(); ti[yytext]=k; return k; > return ti[yytext]; > void yyerror(const char *msg) < fprintf(stderr,"%d: %s\n",yylineno, msg); >int main() < return yyparse(); >

Недостатки инструментов

В BISON нет поддержки РБНФ, дерево разбора не создается (нужно использовать дополнительные инструменты) и невозможно дать русские названия символам грамматики.

Возможность указать синоним для терминальных символов приближает правила грамматики к РБНФ и улучшает вывод информации о синтаксических ошибках. Но в РБНФ кавычки используются только для терминальных символов-литералов, а прочие терминальные символы должны записываться в ??. В результате в BISON нет визуальной разницы между литералами и прочими терминальными символами. Кроме того в РБНФ литералы можно писать в кавычках и апострофах по своему усмотрению, в BISON разрешено использовать апострофы только для односимвольных литералов. Для всех терминальных символов, которые не являются литералом из одного символа, необходимо придумать название, которое используется только во FLEX, а способ преобразования лексемы в номер типа лексемы, предлагаемый разработчиками BISON, неудобный и неэффективный.

FLEX не знает о соответствии между полями типа-объединения и терминальными символами. Синтаксис обращения к значению символа во FLEX и BISON отличается – вместо $$ необходимо писать yylval.имяполя.

Для каждого инструмента необходимо создавать отдельный файл с описанием. При этом в генерируемом коде нет даже объявлений для тех функций, которые в нем используются (BISON – yylex, FLEX – yyerror) и необходимо их добавлять самостоятельно в заголовочный файл.

printРазработка компиляторов и интерпретаторов

printГенератор синтаксических анализаторов BISON

BISON используется для создания синтаксического блока транслятора. Структура описания такая же как у FLEX:

объявления, опции и вставляемый код %% правила %% дополнительные подпрограммы

По описанию будет сгенерирована функция int yyparse(), набор таблиц и вспомогательных подпрограмм. На вход этой функции поступает последовательность токенов (типизированных лексем), а выходом является дерево синтаксического разбора (последовательность применений правил грамматики). Для получения токена вызывается функция yylex(), которая должна вернуть тип лексемы (целое число), а ассоциированное с ней значение поместить в глобальную переменную yylval. Лексемы, состоящие из одного символа, можно не определять как токены и писать в правилах как символ в апострофах, например, '=' или '('.

В первом разделе задаются стартовый символ грамматики, имена токенов (терминальных символов грамматики) и синонимы для них, приоритеты операций, структура элементов стека.

Стартовый символ грамматики задается с помощью конструкции %start символ. По умолчанию стартовым является первый определяемый нетерминальный символ.

Для вставки произвольного кода в начало генерируемой программы применяются % и %code top . С помощью %code provides можно добавить объявления в генерируемый заголовочный файл.

Так как с разными символами грамматики могут связаны значения разных типов, то для хранения этих значений во время разбора с помощью %union определяется тип-объединение, в которой для каждого типа значения задается отдельный элемент:

%union < int val; void *ptr; struct Point < int x,y; >point; >

По умолчанию с символами связано целое число. Для указания типа нетерминальных символов и токенов используется соответственно %type и %token:

%type operator program %type expression %token NUMBER IDENT %token LE GE

Данное определение указывает, что значения для нетерминальных символов operator и program хранятся в элементе ptr объединения, для токенов NUMBER и IDENT – в элементе val, а токенам LE и GE дополнительная информация не нужна.

После имени токена можно написать его лексическое представление в кавычках и использовать его в правилах для повышения наглядности:

%token LE "=" %token IF "if" ELSE "else"

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

Для задания приоритетов операций и упрощения правил используются объявления %left, %right и %nonassoc:

%right '=' %nonassoc '' "=" "!=" "==" %left '+' '-' %left '*' '/' %left UMINUS

Операция = имеет наименьший приоритет и выполняется справа налево (a=b=c), операции сравнения не могут комбинироваться и имеют более высокий приоритет, операции сложения и вычитания выполняются слева направо, а операция унарный минус имеет наивысший приоритет.

Во втором разделе задаются правила грамматики в формате:
символ : правило1 | правило2;

Для каждого нетерминального символа должно быть только одно определение. После каждого правила можно написать код на языке C/C++ в <>, получающий из значений, связанных с символами в правиле, значения для определяемого символа. Для обращения к значениям символов используется $n, где n – номер символа в правиле, а для обращения к значению символа-результата – $$.

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

Калькулятор для целых чисел:
% < #include #include void yyerror(const char *); int yylex(); %> %token DIGIT %left ‘+’ ‘-‘ %left ‘*’ ‘/’ %left UMINUS %% program: | program expr ‘\n’ < printf("%d\n",$2); >; expr: expr ‘+’ expr < $$ = $1+$3; >| expr ‘-‘ expr < $$ = $1-$3; >| expr ‘*’ expr < $$ = $1*$3; >| expr ‘/’ expr < $$ = $1/$3; >| ‘-‘ expr %prec UMINUS < $$ = -$2; >| ‘(‘ expr ‘)’ < $$ = $2; >| number < $$ = $1; >; number: DIGIT < $$=$1;>| number DIGIT < $$=$1*10+$2;>; %% int yylex() < int ch; while((ch=getchar())==' ') ; // пропустить пробелы if(ch==EOF) return 0; if(isdigit(ch)) < yylval=ch-'0'; return DIGIT; >return ch; > void yyerror(const char *msg) < printf("%s\n",msg); >int main()

Упражнения
Решите с использованием BISON следующие задачи: 1576, 1511, 459, 95, 49, 1333, 1233, 63
Пример решения для задачи 459:

% < #include #include void yyerror(const char *); int yylex(); int nerrors=0; int nline=0; int nop=0; %> %token NUMBER %% expr: NUMBER znak oper <++nop; >| '-' NUMBER | '-' '(' expr ')' <++nop; >| '(' expr ')' | expr znak oper <++nop;>; znak: '+' | '-' | '*' | '/'; oper: NUMBER | '(' expr ')' ; %% char str[200]; int pos; int yylex() < while(str[pos]==' ') ++pos; // пропустить пробелы if(str[pos]==0 || str[pos]=='\n') return 0; if(isdigit(str[pos])) < if(str[pos++]>'0') while(isdigit(str[pos])) ++pos; return NUMBER; > return str[pos++]; > void yyerror(const char *msg) < printf("%d\n",nline); ++nerrors; >int main() < while(fgets(str,sizeof(str),stdin)) < ++nline; pos=0; nop=0; if(yyparse()==0 && nop==0) yyerror("nop==0"); >if(nerrors==0) printf("0\n"); >

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *