BNF и EBNF

BNF и EBNF — стандартные нотации, которыми в документации описывают синтаксис языков.

BNF (Backus–Naur Form) — нотация для записи контекстно-свободных грамматик через продукции с нетерминалами в угловых скобках.

BNF придумали в 1950-х для описания языка Algol. С тех пор почти каждая спецификация языка (Python, JSON, SQL) использует BNF или её расширение EBNF. Уметь читать эти нотации — базовый навык для понимания спецификаций.

Синтаксис BNF

В классической BNF нетерминалы заключают в угловые скобки, ::= означает «определяется как», | — «или». Терминалы пишут как есть или в кавычках.

<digit> ::= "0" | "1" | "2" | "3" | "4"
          | "5" | "6" | "7" | "8" | "9"
<sign>  ::= "+" | "-"
<int>   ::= <digit> | <digit> <int>

Здесь <int> — это либо одна цифра, либо цифра, за которой идёт ещё одно целое. Снова рекурсия, чтобы описать число из любого количества цифр.

EBNF: удобные расширения

EBNF (Extended BNF) — расширение BNF с операторами повторения {...}, необязательности [...] и группировки (...).

В чистой BNF повторения выражаются громоздкой рекурсией. EBNF добавляет краткие операторы. Сравните одно и то же правило «целое число — одна или несколько цифр».

НотацияЗапись
BNF<int> ::= <digit> | <digit> <int>
EBNFint = digit , { digit } ;

Операторы EBNF:

  • { X } — ноль или больше повторений X;
  • [ X ] — X необязателен (ноль или один раз);
  • ( X | Y ) — группировка альтернатив;
  • , — последовательность, ; — конец правила.

Пример: число со знаком

number = [ sign ] , digit , { digit } ;
sign   = "+" | "-" ;
digit  = "0" | "1" | "2" | "3" | "4"
       | "5" | "6" | "7" | "8" | "9" ;

Читается так: число — это необязательный знак, затем хотя бы одна цифра, затем сколько угодно цифр. Куда компактнее рекурсивной BNF.

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

EBNF-операторы напрямую переводятся в код парсера. { X } становится циклом while, [ X ] — условием if, | — выбором ветки по текущему токену. Эта связь не случайна: EBNF и придумали так, чтобы по нему было легко писать рекурсивный спуск.

def matches_number_ebnf(text):
    # number = [ sign ] , digit , { digit }
    i = 0
    if i < len(text) and text[i] in "+-":  # [ sign ]
        i += 1
    if i >= len(text) or not text[i].isdigit():  # digit (обязательна)
        return False
    i += 1
    while i < len(text) and text[i].isdigit():  # { digit }
        i += 1
    return i == len(text)

print(matches_number_ebnf("-42"))
print(matches_number_ebnf("+"))

Вывод:

True
False

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

Путают { } (ноль или больше) и [ ] (ноль или один). Из-за этого, например, требуют ровно одну цифру там, где их может быть много.

Ещё ошибка — забыть, что в EBNF { X } допускает ноль повторений. Если нужна «хотя бы одна цифра», пишут digit , { digit } — одна обязательная плюс сколько угодно необязательных.

Итог

  • BNF описывает грамматику продукциями с нетерминалами в угловых скобках.
  • EBNF добавляет { } (повторение), [ ] (необязательность), ( ) (группировку).
  • EBNF-операторы прямо переводятся в циклы и условия парсера.
  • { X } допускает ноль повторений — для «хотя бы одного» нужна явная первая X.
Проверьте себя
1. Что означает оператор { X } в EBNF?
AРовно один X
BX необязателен, ноль или один раз
CНоль или больше повторений X
DГруппировка альтернатив
2. Как в EBNF записать «хотя бы одну цифру»?
A{ digit }
B[ digit ]
Cdigit , { digit }
D( digit )
3. Как EBNF-оператор повторения превращается в код парсера?
AВ оператор return
BВ цикл while
CВ импорт модуля
DВ объявление класса