Компиляторы

Pratt Parser: элегантный парсинг выражений

Vaughan Pratt опубликовал этот алгоритм в 1973 году. Его почти забыли. В 2007 году Дуглас Крокфорд переоткрыл его и написал статью 'Top Down Operator Precedence'. В 2012 году TypeScript выбрал Pratt для парсинга выражений. В 2015 - Rust. В 2019 - Go 1.13. Хороший алгоритм не умирает.

  • TypeScript compiler: Pratt parsing для 18 уровней приоритетов JavaScript выражений
  • Rustc: expression parsing через Pratt в parser/expr.rs - ~600 строк вместо тысяч grammar rules
  • V8 (JavaScript engine): аналог Pratt в парсере для быстрой обработки выражений
  • Prettier (code formatter): Pratt-like подход для понимания приоритетов при форматировании

Приоритеты операторов: почему это сложно

`1 + 2 * 3` должно дать 7, а не 9. Все знают это с первого класса. Но попробуйте написать парсер который это корректно обрабатывает - и вы поймёте, почему в 1973 Vaughan Pratt придумал целый алгоритм для этой «заметной» задачи.

**Классическая проблема**: рекурсивный спуск для выражений требует отдельного уровня рекурсии на каждый приоритет оператора. 5 приоритетов = 5 функций (expr, term, factor, unary, primary). Добавить новый приоритет - рефакторинг всей грамматики. TypeScript поддерживает 18 уровней приоритетов. 18 функций?

**Левая и правая ассоциативность** - отдельная проблема. `a + b + c` = `(a+b)+c` (левая). `a = b = c` = `a=(b=c)` (правая). В классическом парсере это решается через рекурсию: левая ассоциативность - итерация, правая - рекурсивный вызов. В Pratt - через разные значения left и right binding power.

Почему классический рекурсивный спуск плохо масштабируется для языков с многими уровнями приоритетов?

Алгоритм Pratt: binding power и nud/led

Идея Pratt проста и гениальна: каждый токен имеет **binding power** - "силу притяжения" к соседним токенам. `*` притягивает сильнее чем `+`. Алгоритм: пока следующий оператор имеет большую binding power - продолжаем строить правый операнд.

Что произойдёт в Pratt Parser, если у следующего токена binding power меньше или равно текущему min_bp?

Pratt Parser в реальных компиляторах

TypeScript compiler, Rust compiler (rustc), Go parser - все используют Pratt parsing для выражений. Причина: алгоритм легко расширяется новыми операторами без изменения существующего кода. Добавить оператор `??` (nullish coalescing) в TypeScript - добавить строку в таблицу binding power.

Как в Pratt Parser реализуется правоассоциативность оператора?

Расширения: постфиксы, тернарный оператор, mixfix

Красота Pratt parsing - в обработке любой синтаксической формы через nud/led без изменения core алгоритма. Индексирование `a[i]`, вызов `f(x)`, тернарный `a ? b : c`, optional chaining `a?.b` - всё это led-обработчики с нужными binding power.

Pratt Parser сложнее рекурсивного спуска и не стоит его изучать

Pratt Parser проще рекурсивного спуска для языков с многими приоритетами - меньше кода, легче расширять

Recursive descent для JavaScript потребовал бы ~18 отдельных функций для уровней приоритетов. Pratt parser - одну функцию с таблицей binding power. TypeScript, Rust, V8 выбрали Pratt именно из-за этой простоты.

Что делает тернарный оператор `a ? b : c` особенным с точки зрения парсинга?

Ключевые идеи

  • Pratt Parser: каждый токен имеет binding power - 'силу притяжения' к соседям
  • nud (null denotation): обработка токена без левого контекста (prefix, literals)
  • led (left denotation): обработка с левым операндом (infix, postfix)
  • Правоассоциативность: передать bp-1 вместо bp при рекурсии
  • Расширяемость: новый оператор = новая строка в таблице, без изменения core алгоритма

Связанные темы

Pratt Parser строит AST и является основой expression parsing в современных компиляторах.

  • Рекурсивный спуск — Pratt parsing - эволюция recursive descent для expression parsing
  • AST — Pratt Parser строит AST-узлы - следующий этап компиляции

Вопросы для размышления

  • Как реализовать string template literals (`${expr}`) в Pratt Parser? Какой тип токена и обработчик нужны?
  • В чём разница между Pratt Parser и Shunting Yard алгоритмом Дейкстры? Когда выбрать каждый?
  • Как добавить поддержку pipe оператора `|>` (stage 2 proposal) в существующий Pratt Parser для JavaScript?

Связанные уроки

  • comp-11-recursive-descent — Рекурсивный спуск - база для понимания Pratt parsing
  • comp-10-cfg — Грамматики выражений - контекст для приоритетов операторов
  • comp-16-ast — Pratt parser строит AST - следующий урок
  • comp-12-lr-parsing — LR parsing - альтернативный подход для той же задачи
  • alg-21-dp
Pratt Parser: элегантный парсинг выражений

0

1

Войти