Предиктивный синтаксический анализ — различия между версиями
(→Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW) |
(→Пример) |
||
| Строка 64: | Строка 64: | ||
E' \to +TE' \mid \varepsilon \\ | E' \to +TE' \mid \varepsilon \\ | ||
T \to FT' \\ | T \to FT' \\ | ||
| − | T' \to | + | T' \to * FT' \mid \varepsilon \\ |
F \to n \mid (E) | F \to n \mid (E) | ||
</tex> | </tex> | ||
| Строка 88: | Строка 88: | ||
|- | |- | ||
|style="background-color:#FFF;padding:2px 30px"| <tex>T'</tex> | |style="background-color:#FFF;padding:2px 30px"| <tex>T'</tex> | ||
| − | |style="background-color:#FFF;padding:2px 30px"| <tex>\{\ | + | |style="background-color:#FFF;padding:2px 30px"| <tex>\{\ *,\ \varepsilon\ \} </tex> |
|style="background-color:#FFF;padding:2px 30px"| <tex>\{\ +,\ \$\ ,\ )\ \}</tex> | |style="background-color:#FFF;padding:2px 30px"| <tex>\{\ +,\ \$\ ,\ )\ \}</tex> | ||
|- | |- | ||
|style="background-color:#FFF;padding:2px 30px"| <tex>F</tex> | |style="background-color:#FFF;padding:2px 30px"| <tex>F</tex> | ||
|style="background-color:#FFF;padding:2px 30px"| <tex>\{\ n,\ (\ \} </tex> | |style="background-color:#FFF;padding:2px 30px"| <tex>\{\ n,\ (\ \} </tex> | ||
| − | |style="background-color:#FFF;padding:2px 30px"| <tex>\{\ | + | |style="background-color:#FFF;padding:2px 30px"| <tex>\{\ *, \ +,\ \$\ ,\ )\ \} </tex> |
|} | |} | ||
| Строка 102: | Строка 102: | ||
Node res = Node("E") | Node res = Node("E") | ||
'''switch''' (curToken) | '''switch''' (curToken) | ||
| − | '''case''' | + | '''case''' n, '(' : |
res.addChild(T()) | res.addChild(T()) | ||
res.addChild(E'()) | res.addChild(E'()) | ||
'''break''' | '''break''' | ||
'''default''' : | '''default''' : | ||
| − | error("unexpected char") | + | <font color="red">error</font>("unexpected char") |
'''return''' res | '''return''' res | ||
| − | E'() | + | E'() : '''Node''' |
| − | res = Node("E'") | + | Node res = Node("E'") |
| − | switch(curToken) | + | '''switch''' (curToken) |
| − | case '+' : | + | '''case''' '+' : |
consume('+') | consume('+') | ||
res.addChild(Node("+")) | res.addChild(Node("+")) | ||
res.addChild(T()) | res.addChild(T()) | ||
res.addChild(E'()) | res.addChild(E'()) | ||
| − | break | + | '''break''' |
| − | case '$', ')' : | + | '''case''' '$', ')' : |
| − | break | + | '''break''' |
| − | default : | + | '''default''' : |
| − | error("unexpected char") | + | <font color="red">error</font>("unexpected char") |
| − | return res | + | '''return''' res |
| − | F() | + | F() : '''Node''' |
| − | res = Node("F") | + | Node res = Node("F") |
| − | switch(curToken) | + | '''switch''' (curToken) |
| − | case 'n | + | '''case''' n : |
| − | consume( | + | consume(n) |
| − | res.addChild(Node("n | + | res.addChild(Node(curToken)) <font color="green">// <tex>\mathtt{curToken}</tex> подпадает под шаблон <tex>n</tex>, поэтому запишем его в <tex>\mathtt{value}</tex> вершины</font> |
| − | break | + | '''break''' |
| − | case '(' : | + | '''case''' '(' : |
consume('(') | consume('(') | ||
res.addChild(Node("(")) | res.addChild(Node("(")) | ||
| Строка 138: | Строка 138: | ||
consume(')') | consume(')') | ||
res.addChild(Node(")")) | res.addChild(Node(")")) | ||
| − | default : | + | '''default''' : |
| − | error("unexpected char") | + | <font color="red">error</font>("unexpected char") |
| − | return res | + | '''return''' res |
Функции для <tex>T</tex> и <tex>T'</tex> строятся аналогично. | Функции для <tex>T</tex> и <tex>T'</tex> строятся аналогично. | ||
| Строка 146: | Строка 146: | ||
=== Дерево разбора === | === Дерево разбора === | ||
| − | Рассмотрим дерево разбора для выражения (1 + 2) * 3 и несколько первых шагов алгоритма рекурсивного разбора. Сначала вызывается функция стартового нетерминала грамматики, то есть <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> добавит ')' последним сыном к этому нетерминалу. | + | Рассмотрим дерево разбора для выражения <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> добавит ')' последним сыном к этому нетерминалу. |
Продолжая в том же духе, мы построим всё дерево разбора данного выражения. | Продолжая в том же духе, мы построим всё дерево разбора данного выражения. | ||
| − | [[Файл:parse_ex1.png|400px|thumb|center|Дерево разбора выражения (1 + 2) * 3]] | + | [[Файл:parse_ex1.png|400px|thumb|center|Дерево разбора выражения <tex>(1 + 2) * 3</tex>]] |
== Нерекурсивный нисходящий парсер == | == Нерекурсивный нисходящий парсер == | ||
Версия 14:03, 25 мая 2015
Для LL(1)-грамматик возможна автоматическая генерация парсеров, если известны множества FIRST и FOLLOW. Существуют общедоступные генераторы: 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("")
nextToken()
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 станет равен , а разборщик перейдёт к нетерминалу . После того как выражение после '(', которое выводится из , будет полностью разобрано, функция рекурсивного разбора для добавит ')' последним сыном к этому нетерминалу.
Продолжая в том же духе, мы построим всё дерево разбора данного выражения.
Нерекурсивный нисходящий парсер
Рекурсивные разборщики можно генерировать автоматически, зная множества FIRST и FOLLOW, так как они имеют достаточно прозрачный шаблон построения. Альтернативным способом осуществления нисходящего синтаксического анализа является построение нерекурсивного нисходящего парсера. Его можно построить с помощью явного использования стека (вместо неявного при рекурсивных вызовах). Такое анализатор имитирует левое порождение.
Нерекурсивный предиктивный синтаксический анализатор содержит дополнительно стек, содержащий последовательность терминалов и нетерминалов, и таблицу синтаксического анализа. На стеке располагается последовательность символов грамматики с маркером конца строки $ на дне. В начале процесса анализа строки стек содержит стартовый нетерминал грамматики непосредственно над символом $. Таблица синтаксического анализа представляет собой двухмерный массив М[X, а], где X — нетерминал, а — терминал или символ $.
Нерекурсивный синтаксический анализатор смотрит на текущий токен строки a и на символ на вершине стека X, а затем принимает решение в зависимости от одного из возникающих ниже случаев:
- если Х=curToken=$, синтаксический анализатор прекращает работу, так как разбор строки завершён,
- eсли Х=curToken≠$, синтаксический анализатор снимает со стека X и перемещает указатель входного потока к следующему токену (то есть вызывает nextToken),
- eсли X представляет собой нетерминал, программа рассматривает запись M[Х,а] таблицы разбора М. Эта запись представляет собой либо X-продукцию грамматики, либо запись об ошибке. Если, например, М[Х,а] = {X → UVW}, синтаксический анализатор замещает X на вершине стека на WVU (с U на вершине стека). В кач-ве выхода синтаксический анализатор просто выводит использованную продукцию. Если M[Х,а] = error, синтаксический анализатор вызывает программу восстановления после ошибки.
Псевдокод
function nonRecursiveParser(w : String):
s : Stack
s.push(bottom)
s.push(Start)
do
X = s.top()
if X == c
c = nextToken()
s.pop()
else if X in Sigma
error("unexpected symbol")
else if M[X, c] == undefined // запись об ошибке
error("unexpected symbol")
else if M[X, c] == X -> Y_1 Y_2 ... Y_k
s.pop()
for i = k downto 1
s.push(Y_i)
while s.top() != bottom
Пример
| n | ( | ) | + | * | $ | |
|---|---|---|---|---|---|---|
Примечания
Источники информации
- Альфред Ахо, Рави Сети, Джеффри Ульман. Компиляторы. Принципы, технологии, инструменты. Издательство Вильямс. Второе издание. 2008. Стр. 288 — 294.