Урок 7: Улучшение сообщений об ошибках
Вступление
Предыдущий урок был довольно объёмным и сложным, мы все устали пока строили дерево разбора, пытались понять почему узлы стоят не на своих местах, и где образуются утечки памяти. Этот урок будет разгрузочным, в нём мы воспользуемся прошлыми наработками и улучшим качество сообщений об ошибках, а заодно добавим новые диагностики.
Выполнение
Посмотрим текущее состояние сообщений об ошибках:
$ cat tests/in/incorrect/2.1.x
/* Ошибки в теле функции */
void f1() { a; }
void f2() { a }
void f3() { f1(;}
void f4() {a = 1}
void f5() {int int ;}
void f6() {5;}
$ ./bin/debug/x_d tests/in/incorrect/2.1.x
2.1.x:3:14: ';' идентификатор неизвестного предназначения
2.1.x:5:15: '}' идентификатор неизвестного предназначения
2.1.x:9:17: '}' не хватает ';' после выражения
2.1.x:11:16: 'int' некорректное описание тела функции
2.1.x:13:12: '5' выражение не может начинаться с литерала
Эти сообщения выглядят странно, а некоторые попросту ошибочны. Например символ
;
точно никак не может быть идентификатором. В рамках сегодняшнего
урока мы доработаем выдачу сообщений чтобы получать следующие диагностики:
2.1.x:3:14: ошибка: найден идентификатор `a', не являющийся частью выражения
2.1.x:5:15: ошибка: найден идентификатор `a', не являющийся частью выражения
2.1.x:9:17: ошибка: не хватает ';' после выражения
2.1.x:11:16: ошибка: ожидается выражение, но найдено: `int'
2.1.x:13:12: ошибка: найдено выражение, начинающееся с литерала `5'
Разница в выводе довольно большая. Начнём с самого простого, а именно с доработки функции вывода первой части сообщения. Сейчас она автоматически выводит последнюю прочитанную лексему, что не всегда является точной причиной ошибки. Переделаем функцию следующим образом:
void yyerror(Node_t * moduleNode, char const * msg)
{
assert(!NeedErrorMessage);
fprintf (stderr,
"%s:%d:%d: \e[31mошибка\e[0m: ",
ModuleName,
yylloc.first_line,
yylloc.first_column);
GotError = true;
NeedErrorMessage = true;
ErrorLexeme = strdup(yytext);
}
Специальная печать вида \e[31 ... \e[0m
обеспечивает нам красный
текст при выводе в командную строку. Ну и на всякий случай сохраняем последнюю
считанную лексему, она нам ещё понадобится. Теперь надо подправить функцию
вывода второй части сообщения. Приведём её к более привычному для языка Си
формату:
static void printErrorMessage(const char * msg, ...)
{
if( !NeedErrorMessage )
{
return;
}
va_list va;
va_start(va, msg);
vfprintf(stderr, msg, va);
va_end(va);
fprintf(stderr, "\n");
NeedErrorMessage = false;
free(ErrorLexeme);
ErrorLexeme = NULL;
}
Теперь синтаксис функции printErrorMessage
аналогичен синтаксису
printf
с форматной строкой и аргументами. Приведём все
сообщения об ошибках в соответствующий вид.
Т.к. в прошлом уроке мы начали сохранять лексемы в узлах дерева, то теперь при выдаче ошибок мы можем использовать именно их, а не просто последнюю прочитанную лексему:
statement:
/* ... */
| literal_token {
yyerror(NULL, NULL);
printErrorMessage("найдено выражение, начинающееся с литерала `%s'",
node_GetLiteralLexeme($1));
node_DeleteNode($1);
$$ = NULL;
}
Собственно, из новшеств данного урока мы рассмотрели всё что нужно, далее читателю предлагается самостоятельно создать программы с различными ошибками и проверить на сколько адекватны сообщения его компилятора.
Итог
В данном уроке мы улучшили пользовательские характеристики компилятора за счёт информации, предоставляемой нам деревом разбора. Это не последнее такое улучшение, по мере доработки компилятора мы будем получать дооплнительную информацию о программе, и сможем диагностировать большее количество проблем. Пример данного урока можно посмотреть здесь.