Skip to content

Commit

Permalink
Update parsing.md
Browse files Browse the repository at this point in the history
  • Loading branch information
ksromanov authored Oct 9, 2023
1 parent 5d1e109 commit e50e72e
Showing 1 changed file with 2 additions and 2 deletions.
4 changes: 2 additions & 2 deletions docs/parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ _пока пусто_

Очень часто разбор текста и построение из него дерева разбора (_parse tree_) удобно разбить на две фазы. Очень упрощённо, можно сказать, что в первой фазе используется набор переключаемых грамматик, близких к регулярным (грамматика, описываемая регулярными выражениями); а во второй фазе — контекстно-свободная или контекстно-зависимая. Это разбиение сильно упрощает задачу, позволяя использовать классические _CFG_ парсеры для разбора промышленных языков. В принципе оно не обязательно, поскольку есть альтернативные подходы: PEG, scannerless, парсер-комбинаторы, ручной разбор методом рекурсивного спуска.

Первая фаза называется _лексическим_ разбором, а вторая — _синтаксическим_ или _грамматическим_ разбором. Исторически для этих двух фаз часто используются две разные программы с разными языками описания грамматик. Передача потока промежуточных символов (_token_'ов) обычно происходит конвейерным образом (см. генераторы Python, списки в Haskell) с возможной обратной связью (см. _lexer modes_). Например, связки *lex/yacc* или *Alex/Happy*. Но есть и программы, объединяющие в себе обе фазы с единым конфигурационным файлом, такие как *antlr4* или *BNFC*.
Первая фаза называется _лексическим_ разбором, а вторая — _синтаксическим_ или _грамматическим_ разбором. Исторически для этих двух фаз часто используются две разные программы с разными языками описания грамматик. Передача потока промежуточных символов (_token_'ов) обычно происходит конвейерным образом (см. генераторы Python, списки в Haskell) с возможной обратной связью (см. _lexer modes_). Например, связки *lex/yacc* или *Alex/Happy*. Но есть и программы, объединяющие в себе обе фазы с единым конфигурационным файлом, такие как *antlr4*.

В первой фазе текст пропускается через _лексический анализатор_ или _лексер_ (например, сгенерированный *flex* или *Alex*), который читает текст программы посимвольно (например, UTF8 или ASCII символы) и составляет из этих символов слова (так называемые _tokens_) используя конечные автоматы для распознавания регулярных выражений. В самом простейшем случае, когда есть ровно одно правило, _лексический анализатор_ разбирает язык, грамматика которого очень похожа на регулярную. Её отличие от регулярной заключается в том, что в случае нескольких подходящих _регулярных выражений_ применяется то, что даёт наиболее длинный префикс (жадный выбор `||`), внутри же _регулярных выражений_ работает обычная регулярная грамматика:

Expand All @@ -51,7 +51,7 @@ Pattern_i → <regexp>

Когда правил больше одного, появляется возможность учитывать контекстную зависимость, например комментарии. Важно, что получающееся дерево разбора *вырождено*, то есть, это просто список, и, таким образом, поток символов ASCII/UTF8 превращается в поток _token_'ов. С формальной точки зрения и то, и другое — _символы_, но _разных_ языков.

Во второй фазе поток _token_'ов разбирается уже с помощью _парсера_ контекстно-свободной грамматики (_парсер_, сгенерированный *bison* или *Happy*), превращаясь в обычное дерево разбора. За счёт того, что в этой фазе _символами_ являются _token_'ы, то есть _слова_ первоначального языка, разбор происходит быстрее, а значит упрощается и сам текст грамматики, и уменьшается количество символов, которые подаются на вход _парсера_. Также очень важно то, что вынеся различных контекстно-зависимых нерегулярностей в изначально простую грамматику _лексера_, в _парсере_ мы можем использовать грамматику относительно простого класса, например контекстно-свободную или даже LR(0), LL(1) и т.п. Это в ряде случаев позволяет гарантировать однозначность грамматики _парсера_, её простоту, и высокую скорость _синтаксического разбора_ (почти всегда _O(n)_). Таким образом, грамматики _лексера_ и _парсера_ остаются относительно простыми со всеми сопутствующими выгодами, но в композиции позволяют описывать очень сложные, далеко не контекстно-свободные, грамматики промышленных языков программирования.
Во второй фазе поток _token_'ов разбирается уже с помощью _парсера_ контекстно-свободной грамматики (_парсер_, сгенерированный *bison* или *Happy*), превращаясь в обычное дерево разбора. За счёт того, что в этой фазе _символами_ являются _token_'ы, то есть _слова_ первоначального языка, упрощается и сам текст грамматики, и уменьшается количество символов, которые подаются на вход _парсера_. Также очень важно, что вынеся различные контекстно-зависимые нерегулярности в _лексер_, где грамматики очень просты, и незначительно усложнив его, в _парсере_ мы можем использовать грамматику относительно простого класса, например контекстно-свободную или даже LR(0), LL(1) и т.п. Это в ряде случаев позволяет гарантировать однозначность грамматики _парсера_, её простоту, и высокую скорость _синтаксического разбора_ (почти всегда _O(n)_). Таким образом, грамматики _лексера_ и _парсера_ остаются относительно простыми со всеми сопутствующими выгодами, но в композиции позволяют описывать очень сложные, далеко не контекстно-свободные, грамматики промышленных языков программирования.

Иногда используют обратную связь между _парсером_ и _лексером_ (_lexer mode_), таким образом, не всегда можно сказать, что лексический и синтаксический анализы вообще можно разделить на две фазы разбора. Подобная обратная связь используется, например, для интерполяции строк, встроенных языков регулярных выражений, сочетаний языков (JavaScript внутри HTML) и т.д.

Expand Down

0 comments on commit e50e72e

Please sign in to comment.