Урок 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;
  }

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

Итог

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