Предиктивный синтаксический анализ — различия между версиями
(→Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW) |
|||
| Строка 1: | Строка 1: | ||
| − | Для [[LL(k)-грамматики, множества FIRST и FOLLOW | LL(1)-грамматик]] возможна автоматическая генерация парсеров, если известны множества FIRST и FOLLOW. Существуют общедоступные генераторы: ANTLR<ref>[http://www.antlr.org/ ANTLR {{---}} Parser generator]</ref>, Bison<ref>[http://www.gnu.org/software/bison/ Bison {{---}} GNU Project]</ref>, Yacc<ref>[http://dinosaur.compilertools.net/ Lex & Yacc {{---}} A Lexical Analyzer Generator and Yet Another Compiler-Compiler]</ref>, Happy<ref>[https://www.haskell.org/happy/ Happy {{---}} The Parser Generator for Haskell]</ref>. | + | Для [[LL(k)-грамматики, множества FIRST и FOLLOW | LL(1)-грамматик]] возможна автоматическая генерация парсеров, если известны множества <tex>\mathrm{FIRST}</tex> и <tex>\mathrm{FOLLOW}</tex>. Существуют общедоступные генераторы: ANTLR<ref>[http://www.antlr.org/ ANTLR {{---}} Parser generator]</ref>, Bison<ref>[http://www.gnu.org/software/bison/ Bison {{---}} GNU Project]</ref>, Yacc<ref>[http://dinosaur.compilertools.net/ Lex & Yacc {{---}} A Lexical Analyzer Generator and Yet Another Compiler-Compiler]</ref>, Happy<ref>[https://www.haskell.org/happy/ Happy {{---}} The Parser Generator for Haskell]</ref>. |
== Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW == | == Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW == | ||
| Строка 13: | Строка 13: | ||
'''function''' addChild('''Node''') <font color="green">// функция, подвешивающая поддерево к данному узлу</font> | '''function''' addChild('''Node''') <font color="green">// функция, подвешивающая поддерево к данному узлу</font> | ||
| − | Каждый момент времени парсер работает с определённым '''токеном''' (англ. ''token'') входного | + | Каждый момент времени парсер работает с определённым '''токеном''' (англ. ''token'') входного слова <tex>s</tex>. Токен {{---}} один или несколько терминалов, объединяемые по смыслу. Примерами токенов могут выступать следующие лексические единицы: |
* произвольный символ <tex> c </tex>, | * произвольный символ <tex> c </tex>, | ||
* целое слово, например <tex> public </tex>, | * целое слово, например <tex> public </tex>, | ||
| Строка 35: | Строка 35: | ||
<font color="green">// <tex>\alpha_1 = X_1X_2 \ldots X_{t}</tex> </font> | <font color="green">// <tex>\alpha_1 = X_1X_2 \ldots X_{t}</tex> </font> | ||
'''for''' i = 1 .. t | '''for''' i = 1 .. t | ||
| − | '''if''' <tex> X_i </tex> {{---}} | + | '''if''' <tex> X_i </tex> {{---}} терминал |
consume(<tex>X_i</tex>) | consume(<tex>X_i</tex>) | ||
res.addChild(Node("<tex>X_i</tex>") | res.addChild(Node("<tex>X_i</tex>") | ||
| − | + | '''else''' <font color="green">// <tex>X_i</tex> {{---}} нетерминал, нужно вызвать соответствующую ему функцию рекурсивного парсера </font> | |
| − | '''else''' <font color="green">// <tex>X_i</tex> {{---}} | ||
Node t = <tex>X_i()</tex> | Node t = <tex>X_i()</tex> | ||
res.addChild(t) | res.addChild(t) | ||
| Строка 147: | Строка 146: | ||
=== Дерево разбора === | === Дерево разбора === | ||
| − | Рассмотрим дерево разбора для выражения <tex>(1 + 2) * 3</tex> и несколько первых шагов алгоритма рекурсивного разбора. Сначала вызывается функция стартового нетерминала грамматики, то есть <tex>E</tex>. Так как первым токеном является '(', то будет использовано первое правило разбора <tex>TE'</tex>. Поэтому к вершине с меткой <tex>E</tex> добавятся два ребёнка: <tex>T</tex> и <tex>E'</tex> | + | Рассмотрим дерево разбора для выражения <tex>(1 + 2) * 3</tex> и несколько первых шагов алгоритма рекурсивного разбора. Сначала вызывается функция стартового нетерминала грамматики, то есть <tex>E</tex>. Так как первым токеном является '(', то будет использовано первое правило разбора <tex>TE'</tex>. Поэтому к вершине с меткой <tex>E</tex> добавятся два ребёнка: <tex>T</tex> и <tex>E'</tex>, а рекурсивный разборщик перейдёт к нетерминалу <tex>T</tex>. curToken по-прежнему равен '(', поэтому в <tex>F</tex> сработает второй case, первым ребёнком добавится '(', curToken станет равен <tex>1</tex>, а разборщик перейдёт к нетерминалу <tex>E</tex>. После того как выражение после '(', которое выводится из <tex>E</tex>, будет полностью разобрано, функция рекурсивного разбора для <tex>F</tex> добавит ')' последним сыном к этому нетерминалу. |
Продолжая в том же духе, мы построим всё дерево разбора данного выражения. | Продолжая в том же духе, мы построим всё дерево разбора данного выражения. | ||
| Строка 165: | Строка 164: | ||
* если <tex>A\ =\ \perp\ \land\ c\ =\ \$</tex>, то синтаксический анализатор прекращает работу, так как разбор строки завершён, | * если <tex>A\ =\ \perp\ \land\ c\ =\ \$</tex>, то синтаксический анализатор прекращает работу, так как разбор строки завершён, | ||
* eсли <tex>A = c</tex>, то синтаксический анализатор снимает со стека токен <tex>A</tex> и перемещает указатель текущего токена ленты к следующему токену (то есть вызывает <tex>\mathtt{nextToken}</tex>), таким образом, происходит выброс символа <tex> c </tex> со стека, | * eсли <tex>A = c</tex>, то синтаксический анализатор снимает со стека токен <tex>A</tex> и перемещает указатель текущего токена ленты к следующему токену (то есть вызывает <tex>\mathtt{nextToken}</tex>), таким образом, происходит выброс символа <tex> c </tex> со стека, | ||
| − | * eсли <tex>A</tex> | + | * eсли <tex>A</tex> является нетерминалом, то программа рассматривает запись <tex>\mathcal{M}[A,c]</tex> таблицы разбора <tex>\mathcal{M}</tex>. Эта запись представляет собой либо правило грамматики вида <tex>A \to \alpha_i,\ \alpha_i = X_1 X_2 \ldots X_t</tex>, и тогда <tex>\mathcal{M}[A,c]</tex> содержит номер <tex>i</tex> данного правила, либо запись об ошибке, и тогда <tex>\mathcal{M}[A,c] = \varnothing</tex>. Если <tex>\mathcal{M}[A,c] \neq \varnothing</tex>, то синтаксический анализатор замещает на стеке нетерминал <tex> A </tex> правой частью правила с номером <tex> i </tex>, помещая символы правила на стек в обратном порядке, |
| − | * во всех остальных случаях парсер | + | * во всех остальных случаях парсер выдаёт сообщение об ошибке. |
| − | Рассмотренные случаи отображены | + | Рассмотренные случаи отображены на картинке справа, где блок <tex> N </tex> отвечает нетерминалам грамматики. Из картинки видно, что вместо рассмотрения всех случаев в коде достаточно просто создать таблицу <tex>\mathcal{M}</tex> таким образом, чтобы она учитывала все случаи, что упростит код. |
=== Псевдокод === | === Псевдокод === | ||
| Строка 229: | Строка 228: | ||
|} | |} | ||
| − | Теперь можно построить часть таблицы <tex>\mathcal{M}</tex>, содержащей | + | Теперь можно построить часть таблицы <tex>\mathcal{M}</tex>, содержащей отвечающие нетерминалам строки. Её построение легко осуществить, если известны <tex>\mathrm{FIRST}</tex> и <tex>\mathrm{FOLLOW}</tex>. По этим множествам можно понять, какое правило использовать для данного нетерминала при текущем токене. |
{| style="background-color:#CCC;margin:0.5px" | {| style="background-color:#CCC;margin:0.5px" | ||
| Строка 289: | Строка 288: | ||
|} | |} | ||
| − | На картинке ниже показаны | + | На картинке ниже показаны состояния стека на нескольких первых итерациях цикла и указатель на текущий токен. |
[[Файл:Parse_stack.png|500px]] | [[Файл:Parse_stack.png|500px]] | ||
Версия 16:52, 25 мая 2015
Для LL(1)-грамматик возможна автоматическая генерация парсеров, если известны множества и . Существуют общедоступные генераторы: ANTLR[1], Bison[2], Yacc[3], Happy[4].
Содержание
Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW
Пусть — LL(1)-грамматика. Построим для нее парсер.
Для каждого нетерминала создадим функцию , возвращающую фрагмент дерева разбора, выведенный из нетерминала .
Здесь — структура следующего вида:
struct Node
children : list<Node>
value : string // имя нетерминала или текст терминала
function addChild(Node) // функция, подвешивающая поддерево к данному узлу
Каждый момент времени парсер работает с определённым токеном (англ. token) входного слова . Токен — один или несколько терминалов, объединяемые по смыслу. Примерами токенов могут выступать следующие лексические единицы:
- произвольный символ ,
- целое слово, например ,
- число со знаком, обозначаемое далее для краткости как .
В общем случае, токеном может являться любое слово, удовлетворяющее произвольному регулярному выражению.
Здесь обозначает маркер конца строки.
В псевдокоде используются следующие обозначения:
- — текущий токен строки,
- — функция, записывающая в следующий за ним токен.
Тогда шаблон функции рекурсивного парсера для нетерминала имеет вид:
A() : Node
Node res = Node("A")
switch (curToken) : // принимаем решение в зависимости от текущего токена строки
case :
//
for i = 1 .. t
if — терминал
consume()
res.addChild(Node("")
else // — нетерминал, нужно вызвать соответствующую ему функцию рекурсивного парсера
Node t =
res.addChild(t)
break
case :
break
default :
error("unexpected char")
return res
function consume(c: char)
if curToken != c
error("expected " + c)
nextToken()
Такой парсер не только разбирает строку, но и находит ошибки в неудовлетворяющих грамматике выражениях.
Пример
Рассмотрим построение парсера на примере LL(1)-грамматики арифметических выражений, которая уже была разобрана ранее:
Напомним, что множества и для неё выглядят так:
| Правило | FIRST | FOLLOW |
|---|---|---|
Псевдокоды
Построим функции обработки некоторых нетерминалов, используя описанный выше шаблон:
E() : Node
Node res = Node("E")
switch (curToken)
case n, '(' :
res.addChild(T())
res.addChild(E'())
break
default :
error("unexpected char")
return res
E'() : Node
Node res = Node("E'")
switch (curToken)
case '+' :
consume('+')
res.addChild(Node("+"))
res.addChild(T())
res.addChild(E'())
break
case '$', ')' :
break
default :
error("unexpected char")
return res
F() : Node
Node res = Node("F")
switch (curToken)
case n :
consume(n)
res.addChild(Node(curToken)) // подпадает под шаблон , поэтому запишем его в вершины
break
case '(' :
consume('(')
res.addChild(Node("("))
res.addChild(E())
consume(')')
res.addChild(Node(")"))
default :
error("unexpected char")
return res
Функции для и строятся аналогично.
Дерево разбора
Рассмотрим дерево разбора для выражения и несколько первых шагов алгоритма рекурсивного разбора. Сначала вызывается функция стартового нетерминала грамматики, то есть . Так как первым токеном является '(', то будет использовано первое правило разбора . Поэтому к вершине с меткой добавятся два ребёнка: и , а рекурсивный разборщик перейдёт к нетерминалу . curToken по-прежнему равен '(', поэтому в сработает второй case, первым ребёнком добавится '(', curToken станет равен , а разборщик перейдёт к нетерминалу . После того как выражение после '(', которое выводится из , будет полностью разобрано, функция рекурсивного разбора для добавит ')' последним сыном к этому нетерминалу.
Продолжая в том же духе, мы построим всё дерево разбора данного выражения.
Нерекурсивный нисходящий парсер
Рекурсивные разборщики можно генерировать автоматически, зная множества и , так как они имеют достаточно прозрачный шаблон построения. Альтернативным способом осуществления нисходящего синтаксического анализа является построение нерекурсивного нисходящего парсера. Его можно построить с помощью явного использования стека (вместо неявного при рекурсивных вызовах). Такой анализатор имитирует левое порождение.
Стек нерекурсивного предиктивного синтаксического анализатора содержит последовательность терминалов и нетерминалов и таблицу синтаксического анализа. На стеке располагается последовательность символов грамматики с маркером конца разбора на дне. В начале процесса анализа строки стек содержит стартовый нетерминал грамматики непосредственно над символом . Таблица синтаксического анализа представляет собой двухмерный массив , где — нетерминал или , — токен или символ конца строки .
Нерекурсивный синтаксический анализатор смотрит на текущий токен входного слова и на символ на вершине стека , а затем принимает решение в зависимости от одного из возникающих ниже случаев:
- если , то синтаксический анализатор прекращает работу, так как разбор строки завершён,
- eсли , то синтаксический анализатор снимает со стека токен и перемещает указатель текущего токена ленты к следующему токену (то есть вызывает ), таким образом, происходит выброс символа со стека,
- eсли является нетерминалом, то программа рассматривает запись таблицы разбора . Эта запись представляет собой либо правило грамматики вида , и тогда содержит номер данного правила, либо запись об ошибке, и тогда . Если , то синтаксический анализатор замещает на стеке нетерминал правой частью правила с номером , помещая символы правила на стек в обратном порядке,
- во всех остальных случаях парсер выдаёт сообщение об ошибке.
Рассмотренные случаи отображены на картинке справа, где блок отвечает нетерминалам грамматики. Из картинки видно, что вместо рассмотрения всех случаев в коде достаточно просто создать таблицу таким образом, чтобы она учитывала все случаи, что упростит код.
Псевдокод
Здесь по-прежнему обозначает текущий токен строки, а передвигает указатель на следующий токен.
function nonRecursiveParser():
st : Stack
st.push()
st.push() // здесь — стартовый нетерминал грамматики
while s.top()
A = st.top()
if // и
break // разбор строки завершён
else if // выброс
nextToken()
st.pop()
вывести в выходной поток нетерминал, отвечающий
else if — номер правила
st.pop()
for k = t downto 1
st.push()
вывести в выходной поток терминал, отвечающий
else
error("unexpected symbol")
Пример
Рассмотрим пример работы нерекурсивного парсера на всё той же грамматике арифметических выражений. Для начала пронумеруем все правила:
| Номер | Правило |
|---|---|
Теперь можно построить часть таблицы , содержащей отвечающие нетерминалам строки. Её построение легко осуществить, если известны и . По этим множествам можно понять, какое правило использовать для данного нетерминала при текущем токене.
На картинке ниже показаны состояния стека на нескольких первых итерациях цикла и указатель на текущий токен.
Примечания
Источники информации
- Альфред Ахо, Рави Сети, Джеффри Ульман. Компиляторы. Принципы, технологии, инструменты. Издательство Вильямс. Второе издание. 2008. Стр. 288 — 294.