Содержание


Лучшая обработка ошибок с помощью Flex и Bison

Советы по созданию более дружественных к пользователю компиляторов и интерпретаторов

Comments

Разработчики в 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 - мощное сочетание для синтаксического разбора грамматики. Используя советы и приемы, приведенные в данной статье, Вы сможете создать интерпретаторы, выводящие удобные, простые для понимания сообщения об ошибках, которые Вы получаете в предпочитаемом Вами компиляторе.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: "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.

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=183539
ArticleTitle=Лучшая обработка ошибок с помощью Flex и Bison
publish-date=12132006