Формальные грамматики

Чтобы парсер знал, какие конструкции допустимы, синтаксис языка описывают строгой грамматикой.

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

Естественный язык расплывчат, а язык программирования обязан быть однозначным. Грамматика задаёт точные правила: что можно написать, а что — синтаксическая ошибка. Парсер — это, по сути, исполнитель грамматики.

Из чего состоит грамматика

Грамматика оперирует четырьмя сущностями:

  • Терминалы — конкретные токены, дальше не раскрываются (число, +, ().
  • Нетерминалы — абстрактные понятия, которые раскрываются через другие правила (выражение, слагаемое).
  • Продукции — правила вида «нетерминал может быть тем-то».
  • Стартовый символ — нетерминал, с которого начинается разбор.

Пример: грамматика сложения

Опишем простейший язык — суммы чисел. Стрелка ::= читается «определяется как», вертикальная черта — «или».

<expr>   ::= <number> "+" <expr>
           | <number>
<number> ::= "0" | "1" | "2" | ... | "9"

Прочитаем: выражение <expr> — это либо число плюс ещё одно выражение, либо просто число. Такое правило, ссылающееся на само себя, называется рекурсивным — именно рекурсия даёт грамматике возможность описывать сколь угодно длинные выражения конечным набором правил.

Как работает под капотом

Разбор по грамматике — это поиск способа «вывести» входную строку из стартового символа, последовательно применяя продукции. Для строки 2+3 вывод выглядит так.

<expr>
=> <number> "+" <expr>     (первая продукция)
=> "2" "+" <expr>          (раскрыли число)
=> "2" "+" <number>        (вторая продукция)
=> "2" "+" "3"             (раскрыли число)

Если входную строку нельзя вывести ни одним способом — это синтаксическая ошибка. Например, 2 + + вывести не получится: после + грамматика требует выражение, а там опять +.

Проверим разбор кодом

Напишем игрушечный распознаватель именно для этой грамматики «число плюс число плюс...».

def is_valid_sum(text):
    text = text.replace(" ", "")
    # ожидаем: число (+ число)*
    expect_number = True
    for ch in text:
        if expect_number:
            if not ch.isdigit():
                return False
            expect_number = False
        else:
            if ch != "+":
                return False
            expect_number = True
    return not expect_number  # не должны кончиться на '+'

print(is_valid_sum("2+3+4"))
print(is_valid_sum("2++3"))

Вывод:

True
False

Частые ошибки

Путают терминалы и нетерминалы. Запомните: терминал — это то, что реально появляется во входе (токен), нетерминал — вспомогательное имя, которого во входе нет, оно лишь структурирует правила.

Ещё ошибка — забыть базовый случай в рекурсивном правиле. Если <expr> всегда раскрывается в <number> "+" <expr> без альтернативы «просто число», вывод никогда не завершится.

Итог

  • Грамматика формально задаёт, какие строки корректны в языке.
  • Терминалы — это токены, нетерминалы — абстрактные понятия из правил.
  • Рекурсивные правила описывают сколь угодно длинные конструкции.
  • Разбор — это вывод входной строки из стартового символа; невыводимость означает ошибку.
Проверьте себя
1. Что такое терминал в грамматике?
AАбстрактное понятие, раскрываемое другими правилами
BКонкретный токен, который реально появляется во входе
CСтартовый символ грамматики
DИмя файла компилятора
2. Зачем грамматике рекурсивные правила?
AЧтобы замедлить разбор
BЧтобы описывать сколь угодно длинные конструкции конечным набором правил
CЧтобы хранить комментарии
DОни не нужны