Лучшая обработка ошибок с помощью Flex и Bison
Советы по созданию более дружественных к пользователю компиляторов и интерпретаторов
Разработчики в UNIX® знают, что Flex и Bison - мощный инструмент для разработки программ лексического и синтаксического разбора, и особенно языковых компиляторов и интерпретаторов. Если Вы знакомы с этими программами или с инструментами, в них входящими -- соответственно, Lex и Yacc -- обратитесь к разделу Ресурсы данной статьи для получения ссылок на документацию по Flex Bison и дополнительные статьи, приводящие обзор обеих программ.
В данной статье рассматривается несколько более сложная проблема: режимы и приемы для снабжения Вашего копмилятора или интерпретатора более совершенными возможностями обработки ошибок. Для иллюстрации этих приемов я воспользуюсь программой-примером ccalc, реализующим усовершенствованный калькулятор, основанный на инфиксном калькуляторе, приведенном в учебнике по Bison. Вы можете загрузить программу ccalc и связанные с ней файлы, перейдя в раздел Загрузки в конце данной статьи.
Усовершенствование представлено использованием переменных. В ccalc переменная определяется ее первым использованием при инициализации, например
a = 3
. Если переменная используется до ее инициализации, генерируется семантическая ошибка, переменная создается со значением ноль и
выводится сообщение об ошибке.
Исходные файлы примеров
Пример исходного кода состоит из семи файлов:
- ccalc.c: Основная программа и некоторые функции для ввода, вывода и обработки ошибок
- ccalc.h:: Содержит описания для всех модулей
- cmath.c: Математические функции
- parse.y: Ввод грамматики для Bison
- lex.l Ввод для Flex
- makefile: Простое формирование файла
- defs.txt: Пример файла ввода
Программа принимает два параметра:
-debug
: Выводит отладочные выходные данныеfilename
: Имя файла ввода; по умолчанию принимается какdefs.txt
Настройки Bison
Для работы с переменными и вещественными значениями необходимо расширить семантический тип Bison:
Листинг 1. Расширение семантического типа Bison
/* создание включаемого файла с символами и типами */ %defines /* более сложный семантический тип */ %union { double value; char *string; }
Некоторые грамматические правила создают специальные семантические типы, которые необходимо объявить для Bison как показано в Листинге 2.
Для получения версии, лучше совместимой с грамматикой Bison, символы +-*/()
переопределяют. Вместо использования открывающей скобки
(
в примере используется терминальный символ LBRACE
, предоставленный лексическим анализом. Кроме того,
приоритет операторов также необходимо объявлять.
Для Flex сгенерированный код в общем зависит от кодовой страницы платформы. Вы можете использовать другую кодовую страницу, но Вам нужно изменить считываемые входные данные. Таким образом, в отличие от программы Bison, Flex не настолько просто переносим.
Листинг 2. Объявления Bison
/* терминальные символы */ %token <string> IDENTIFIER %token <value> VALUE %type <value> expression /* operator-precedence * top-0: - * 1: * / * 2: + - */ %left ADD SUB %left MULT DIV %left NEG %start program
Грамматика похожа на приведенную в учебнике по Bison, за исключением импользования имен как терминальных символов и ввод идентификаторов. Идентификатор описан и инициализирован присваиванием и может использоваться всюду, где допустимо присвоенное ему значение. В Листинге 3 приведен пример грамматики:
Листинг 3. Пример грамматики Bison
program : statement SEMICOLON program | statement SEMICOLON | statement error SEMICOLON program ; statement : IDENTIFIER ASSIGN expression | expression ; expression : LBRACE expression RBRACE | SUB expression %prec NEG | expression ADD expression | expression SUB expression | expression MULT expression | expression DIV expression | VALUE | IDENTIFIER ;
Третья продукция программы
позволяет анализатору миновать ошибку, найти точку с запятой и после этого продолжить работу
(обычно ошибка является фатальной для анализатора).
Сделаем пример более интересным, введя в тело правила фактическую арифметику отдельной функцией. При выполнении сложной грамматики сохраняйте правила короткими и используйте функции для обработки, которая не связана непосредственно с анализом:
Листинг 4. Использование отдельной функции для реализации математического правила
| expression DIV expression { $$ = ReduceDiv($1, $3); }
Наконец, необходимо объявить функцию yyerror()
. Эта функция вызывается, когда сгенерированный анализатор обнаруживает
синтаксическую ошибку, вызывая, в свою очередь простую функцию PrintError()
, которая выводит подробное сообщение об ошибке.
Для получения более подробной информации см. исходный код программы.
Настройки Flex
Лексический анализатор, генерируемый Flex предоставляет терминальные символы соответственно их семантическому типу. В Листинге 5 объявляются правила для пробела, вещественных значений, идентификаторов и символов.
Листинг 5. Пример правил Flex
[ \t\r\n]+ { /* убрать пробел */ } {DIGIT}+ { yylval.value = atof(yytext); return VALUE; } {DIGIT}+"."{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {DIGIT}+[eE]["+""-"]?{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {DIGIT}+"."{DIGIT}*[eE]["+""-"]?{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {ID} { yylval.string = malloc(strlen(yytext)+1); strcpy(yylval.string, yytext); return IDENTIFIER; } "+" { return ADD; } "-" { return SUB; } "*" { return MULT; } "/" { return DIV; } "(" { return LBRACE; } ")" { return RBRACE; } ";" { return SEMICOLON; } "=" { return ASSIGN; }
Для облегчения отладки в конце выполнения программы выводятся все известные переменные и их текущие значения.
Пример простых сообщений об ошибке
Скомпилируйте и запустите анализируемую программу-пример ccalc
со следующими входными данными (включающими неопределенный тип):
Листинг 6. Пример входных данных математического анализатора
a = 3; 3 aa = a * 4; b = aa / ( a - 3 );
Выходные данные будут иметь следующий вид:
Листинг 7. Пример выходных данных математического анализатора
Error 'syntax error' Error: reference to unknown variable 'aa' division by zero! final content of variables Name------------------ Value---------- 'a ' 3 'b ' 3 'aa ' 0
Такие выходные данные не слишком полезны, поскольку не указывают, где возникла ошибка. Эта проблема будет решена в следующем разделе.
Расширение Bison для улучшения сообщений об ошибках
Первое интересное свойство Bison, спрятанное глубоко в мануалах по Bison, заключается в том, что можно генерировать более информативные сообщения об ошибках
при возникновении синтаксической ошибки, используя макрос YYERROR_VERBOSE
.
Простое сообщение о 'синтаксической ошибке'
примет вид:
Error 'syntax error, unexpected IDENTIFIER, expecting SEMICOLON'
(Ошибка: ошибка синтаксиса, непредвиденный идентификатор, ожидается точка с запятой)
Такое сообщение намного полезнее при отладке.
Улучшение функции ввода
Определить семантическую ошибку непросто, имея старые сообщения об ошибках. Программа-пример, конечно, исправляется очень просто, как только Вам удастся обнаружить строку, содержащую ошибку. В более сложных грамматиках и соответствующих входных данных это может оказаться не так просто. Давайте напишем функцию ввода для получения соответствующих строк из файла.
Flex имеет полезный макрос YY_INPUT
, считывающий данные для интерпретации. Вы можете добавить вызов YY_INPUT
к функции GetNextChar()
, которая считывает данные из файла и хранит информацию о положении следующего символа для считывания.
GetNextChar()
использует буфер для хранения одной строки входных данных.
Две переменных хранят номер текущей строки и следующую позицию в строке:
Листинг 8. Более удобный макрос Flex YY_INPUT
#define YY_INPUT(buf,result,max_size) {\ result = GetNextChar(buf, max_size); \ if ( result <= 0 ) \ result = YY_NULL; \ }
С помощью улучшенной функции вывода ошибки, PrintError()
, описанной ранее, и выводом ошибочной строки входных данных
(полный программный код для PrintError()
приведен в примере программного кода), Вы получите дружественное к
пользователю сообщение, в котором будет указано положение следующего символа:
Листинг 9. Улучшение сообщения об ошибке в Flex: Положение символа
|....+....:....+....:....+....:....+....:....+....:....+ 1 |a = 3; 2 |3 aa = a * 4; ...... !.....^ Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON 3 |b = aa / ( a - 3 ); ...... !.......^ Error: reference to unknown variable 'aa' ...... !.................^ Error: division by zero!
Здесь та же функция может быть вызвана другими функциями, такими как ReduceDiv()
, для вывода сообщений о семантических ошибках,
например деление на ноль или неизвестный идентификатор.
Если Вы хотите пометить последний считанный символ, Вам придется усложнить правила Flex и изменить способ распечатки ошибок. Функции
BeginToken()
и PrintError()
(обе находятся в примере программного кода) - это ключи:
BeginToken()
вызывается при каждом действии и потому может запомнить начало и конец каждого символа, а PrintError()
вызывается каждый раз при выводе сообщения об ошибке. Таким образом, вы можете генерировать полезные сообщения следующего вида:
Листинг 10. Улучшение сообщения об ошибке в Flex: Указание точного положения символа
2 |3 aa = a * 4; ...... !..^^............ Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON
Трудности
Сгенерированный лексический анализатор может считать на несколько символов дальше местонахождения ошибки до ее обнаружения. Следовательно, эта процедура может и не показать правильно точное положение. Это зависит в конечном счете от инструкций, определенных Вами для Flex. Чем они сложнее, тем менее точно определяется положение. Инструкции, приведенные в программе-примере, могут быть обработаны Flex с опережением всего на один символ, что делает указание положения безошибочным.
Механизм определения местонахождения в Bison
Рассмотрим ошибку деление на ноль. Последний считанный символ (закрывающая скобка) не является причиной ошибки. Выражение
(a-3)
обращается в ноль. Для точного сообщения об ошибке вам необходимо указать местонахождение всего выражения.
Для этого укажите точное местонахождение символа в глобальной переменной yylloc
типа YYLTYPE
.
Вместе с макросом YYLLOC_DEFAULT (см. документацию по Bison для получения описания по умолчанию) Bison
высчитает местонахождение выражения.
Помните, что тип определяется только если Вы используете местонахождение в грамматике! Это распространенная ошибка.
Тип местонахождения по умолчанию, YYLTYPE
, приведен в Листинге 11. Вы можете переопределить этот тип,
чтобы включить в него больше информации, такой как имя файла, считываемого Flex.
Листинг 11. Тип местонахождения по умолчанию, YYLTYPE
typedef struct YYLTYPE { int first_line; int first_column; int last_line; int last_column; } YYLTYPE;
В предыдущем разделе Вы познакомились с функцией BeginToken()
, которая вызывается в начале каждой лексической единицы.
Теперь самое время сохранить информацию о местонахождении. В нашем случае лексическая единица не может простираться на несколько строк, поэтому
first_line
(первая строка) и last_line
(последняя строка) имеют одинаковое значение и хранят номер
текущей строки. Остальные атрибуты - начало единицы (first_column
(первый столбец)) и ее конец (last_column
(последний столбец)) -
высчитываются с помощью значения начала лексической единицы и ее длины.
Для использования местонахождения Вам нужно расширить функцию обработки инструкций, как показано в Листинге 12. Местонахождение лексической единицы
$3
обозначается как @3
. Во избежание копирования целой структуры инструкции генерируется указатель
&@3
. Это может выглядеть немного необычно, но, тем не менее, все правильно.
Листинг 12. Запоминание местонахождения в инструкции
| expression DIV expression { $$ = ReduceDiv($1, $3, &@3); }
В функции обработки Вы получаете указатель на структуру YYLTYPE
, хранящую информацию о местонахождении, и можете сгенерировать
милое сообщение об ошибке.
Листинг 13. Использование сохраненной информации о местонахождении в ReduceDiv
extern double ReduceDiv(double a, double b, YYLTYPE *bloc) { if ( b == 0 ) { PrintError("division by zero! Line %d:c%d to %d:c%d", bloc->first_line, bloc->first_column, bloc->last_line, bloc->last_column); return MAXFLOAT; } return a / b; }
Теперь сообщения об ошибке помогают Вам найти проблему. Ошибка деления на ноль находится в строке 3 между столбцами 10 и 18.
Листинг 14. Более удобные сообщения об ошибках ReduceDiv()
|....+....:....+....:....+....:....+....:....+....:....+ 1 |a = 3; 2 |3 aa = a * 4; ...... !..^^........... Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON 3 |b = aa / ( a - 3 ); ...... !....^^............... Error: reference to unknown variable 'aa' ...... !.................^.. Error: division by zero! Line 3:10 to 3:18 final content of variables Name------------------ Value---------- 'a ' 3 'b ' 3.40282e+38 'aa ' 0
Заключение
Flex и Bison - мощное сочетание для синтаксического разбора грамматики. Используя советы и приемы, приведенные в данной статье, Вы сможете создать интерпретаторы, выводящие удобные, простые для понимания сообщения об ошибках, которые Вы получаете в предпочитаемом Вами компиляторе.
Ресурсы для скачивания
- этот контент в PDF
- Sample source code for this article (ccalc.zip | 7KB)
Похожие темы
- Оригинал статьи: "Better error handling using Flex and Bison"
- Прочитайте онлайн документацию по Flex.
- Прочитайте онлайн документацию по Bison.
- Посетите страницу Lex и Yacc для получения сведений общего характера о крестных отцах Flex и Bison.
- Для получения дополнительной информации о Lex и Yacc, читайте серию developerWorks в двух частях Написание программы с lex и yacc. Часть 1 знакомит Вас с lex, yacc, flex, и bison, а Часть 2 изучает разработку, поиск и устранение неисправностей на более сложном уровне. Также изучите введение в Lex и Yacc, более детально описывающее использование Lex и Yacc.
- Найдите больше информации для разработчиков Linux в Разделе Linux на developerWorks Россия.
- Создайте Ваш новый проект для Linux с пробным ПО IBM, доступным для загрузки прямо с developerWorks.
Комментарии
Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.