КС-грамматики: правила и вывод

Контекстно-свободная грамматика описывает вложенные структуры — выражения, блоки, скобки, — на которых конечные автоматы спотыкались.

КС-грамматика — грамматика, все правила которой имеют вид A → γ, где A — один нетерминал, а γ — любая строка из терминалов и нетерминалов.

Анатомия грамматики

КС-грамматика — это четвёрка (V, Σ, P, S): нетерминалы V (синтаксические категории, обычно заглавные), терминалы Σ (символы языка), правила P и стартовый символ S. Ключевая особенность: слева от стрелки — ровно один нетерминал, безо всякого контекста (отсюда «контекстно-свободная»: A можно заменить на γ где угодно, независимо от соседей). Это и даёт силу описывать рекурсию: нетерминал может ссылаться сам на себя.

Канонический пример — арифметические выражения:

E → E + E
E → E * E
E → ( E )
E → num

Здесь E — нетерминал «выражение», num и +,*,(,) — терминалы.
Правило E → ( E ) рекурсивно: выражение может содержать выражение.

Вывод строки

Вывод — последовательность подстановок от S до строки из одних терминалов. На каждом шаге берём один нетерминал и заменяем его правой частью какого-то правила (записываем ⇒). Например, для грамматики скобок S → ( S ) S | ε выведем «(())»:

S ⇒ ( S ) S ⇒ ( ( S ) S ) S ⇒ ( ( ) S ) S ⇒ ( ( ) ) S ⇒ ( ( ) )
(последовательно заменяем S, в конце S → ε убирает лишние)

Грамматика S → ( S ) S | ε порождает ровно все сбалансированные скобочные последовательности — тот самый язык, что был не по зубам регуляркам. Стек «прятался» в рекурсии правила.

Порождение строк программно

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

from functools import lru_cache

# грамматика Дика: S -> ( S ) S | ε ; генерируем все строки длины 2n
@lru_cache(maxsize=None)
def dyck(n):
    if n == 0:
        return [""]
    res = []
    for i in range(n):                 # i пар внутри скобок, n-1-i снаружи
        for inner in dyck(i):
            for rest in dyck(n - 1 - i):
                res.append("(" + inner + ")" + rest)
    return res

for n in range(4):
    seqs = dyck(n)
    print(f"{2*n} символов ({len(seqs)} шт): {seqs}")

Вывод:

0 символов (1 шт): ['']
2 символов (1 шт): ['()']
4 символов (2 шт): ['()()', '(())']
6 символов (5 шт): ['()()()', '()(())', '(())()', '(()())', '((()))']

Числа 1, 1, 2, 5 — это числа Каталана: количество правильных скобочных последовательностей. Грамматика породила в точности все сбалансированные строки.

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

КС-грамматики — это BNF (форма Бэкуса-Наура), на которой записан синтаксис почти всех языков программирования. Когда в спецификации Python пишут `if_stmt: 'if' test ':' suite`, это правило КС-грамматики. Парсер компилятора берёт грамматику и по входному коду восстанавливает вывод — то есть доказывает, что программа порождается грамматикой, попутно строя дерево разбора. Контекстная свобода (A заменяется независимо от соседей) — почему синтаксис можно разбирать локально и эффективно; именно она делает возможными быстрые парсеры (LL, LR).

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

Ставить несколько символов слева от стрелки. Это уже контекстно-зависимая грамматика (тип 1), не КС. В КС слева строго один нетерминал.

Путать терминалы и нетерминалы. Терминалы — реальные символы выхода; нетерминалы — временные, в финальной строке их не должно остаться.

Забывать базовый случай рекурсии. Без правила-выхода (например, S → ε или E → num) вывод никогда не завершится строкой терминалов.

Итог

  • КС-грамматика: правила A → γ с одним нетерминалом слева; описывает рекурсивные, вложенные структуры.
  • Вывод — цепочка подстановок от S до строки терминалов; S → ( S ) S | ε задаёт сбалансированные скобки.
  • КС-грамматики = BNF: на них записан синтаксис языков программирования.
  • Контекстная свобода делает синтаксический разбор локальным и эффективным.
Проверьте себя
1. Какой вид имеют правила контекстно-свободной грамматики?
AA → aB (нетерминал и терминал)
BA → γ — один нетерминал слева, любая строка справа
CαAβ → αγβ — с контекстом
Dлюбые правила без ограничений
2. Почему грамматику называют «контекстно-свободной»?
Aв ней нет терминалов
Bнетерминал заменяется правилом независимо от соседних символов
Cона не зависит от стартового символа
Dв ней нет рекурсии
3. Чем являются КС-грамматики на практике в языках программирования?
Aрегулярными выражениями
Bформой BNF, задающей синтаксис языка
Cтаблицами переходов автомата
Dмашинами Тьюринга