Восходящий синтаксический анализ соответствует построению дерева разбора начиная с листьев. Аналогично, это соответствует построению правого порождения для входной строки в обратном порядке.
В качестве типового метода восходящего синтаксического анализа будем рассматривать метод перенос/свёртка.
Метод перенос-свёртка
При использовании метода перенос-свёртка (далее ПС-анализ), для хранения символов грамматики используется, аналогично LL(1)-анализу, стек, а для входной строки – входной буфер. Из соображений удобства, при рассмотрении ПС-анализа, вершину стека будем располагать справа.
На каждом шаге анализа, принимается решение о том, какое действие должен совершить анализатор. Возможные действия – это перенос, свёртка, принятие и ошибка.
Перенос – это чтение символа из входной строки, помещение его на вершину стека, и переход к следующему символу входной строки. Иными словами, “перенос” очередного символа из входной строки на вершину стека.
Свёртка – это замена нескольких символов на вершине стека, совпадающих с телом какой-то продукции, на её заголовок. Иными словами, “свёртка” тела продукции в её заголовок.
Принятие и ошибка завершают работу анализатора соответственно успехом и неудачей. Собственно, успешное завершение соответствует ситуации, что стек содержит только стартовый символ грамматики, а входная строка пуста.
Использование стека обусловлено тем, что при рассмотрении правого порождения (в обратном порядке), тело очередной продукции, которая может быть свёрнута, будет всегда находиться в самой правой позиции, что соответствует вершине стека.
Конфликты при ПС-анализе
При ПС-анализе, на очередном шаге анализа возможны два типа конфликтов:
- Конфликт перенос/свёртка – соответствует ситуации, когда анализатор не может на основе имеющейся у него информации принять решение о том, следует ли на данном шаге произвести свёртку или перенос.
- Конфликт свёртка/свёртка – соответствует ситуации, когда анализатор не может на основе имеющейся у него информации принять решение о том, следует ли на данном шаге произвести свёртку, соответствующую одной или другой продукции.
Чаще всего, конфликты свидетельствуют о неоднозначности грамматики. Реже – о том, что конкретный анализатор (использующий \(k\) символов предпросмотра) недостаточно мощный для данной грамматики.
LR-анализ
Наиболее распространённый тип анализаторов типа перенос-свёртка основан на принципе LR-анализа, разработанном Д. Кнутом в 1965 году1.
LR-анализ соответствует автомату с магазинной памятью, реализующему, собственно, перенос/свёртку, программа которого управляется детерминированным конечным автоматом. При этом, на каждом шаге из входной строки читается \(k\) символов, где \(k \in \mathbb N\) – константа. Собственно, любой LR-анализатор для какого-то \(k\) называется LR(k)-анализатором, а множество грамматик, которые может разбирать LR(k)-анализатор называется LR(k)-грамматиками.
Так же доказано2, что для любого фиксированного \(k \ge 1\), язык имеет LR(k) грамматику тогда и только тогда, когда он является детерминистическим контекстно-свободным языком, т.е. языком, который может быть разобран детерминированным автоматом с магазинной памятью. Следует, правда, отметить, что множество всех детерминистических контекстно-свободных языков является строгим подмножеством всех однозначных контекстно-свободных языков (т.е. языков для которых существует однозначная грамматика), поэтому не все однозначные языки могут быть разобраны при помощи LR-анализа. Впрочем, для большинства языков возможно написать LR-анализатор (используя таблицу символов для разрешения неоднозначностей), одно из известных исключений – C++, который является LR(∞)-языком.
Как следствие, для любого LR(k)-языка существует LR(1)-грамматика, соответственно множество всех LR-языков (т.е. порождаемых LR(k)-грамматиками для любых конечных k) есть множество LR(1)-языков. Отметим так же, что LR(0)-язык должен дополнительно обладать свойством префиксного кода, т.е. никакое предложение языка не должно быть префиксом другого предложения.
Множество LR-грамматик есть истинное надмножество LL-грамматик. В LR-анализе мы должны распознать правую часть продукции на вершине стека с дополнительным предпросмотром k входных символов. Это требование гораздо более мягкое, чем для LL(k) грамматик где необходимо “угадать” продукцию по первым k символам некого порождения её правой части.
Основной недостаток LR-анализа в относительной сложности построения анализатора, однако он “исправляется” использованием автоматических генераторов. По большому счёту, “хороших” (т.е. формально обоснованных) причин для того, чтобы использовать LL-анализатор или рекурсивный спуск вместо LR-анализатора нет. Однако, для простых, “одноразовых” языков, проще бывает написать LL-грамматику или даже написать рекурсивный анализатор “на коленке”.
Мы рассмотрим несколько подходов к LR-анализу. Сначала мы рассмотрим LR(0)-анализ, который практически не слишком значителен, однако сравнительно прост. Затем, рассмотрим как LR(0) анализ может быть улучшен эвристическими методами (т.н. SLR-анализ). Наконец, мы рассмотрим LR(1)-анализ и кратко обсудим упрощение LR(1), называемое LALR.
LR(0)
LR-анализатор принимает решение о выборе между переносом и свёрткой, отслеживая, в каком месте тела продукции находится анализ.
Это реализуется при помощи состояний, называемых “пунктами”. LR(0)-пункты описываются в виде продукций с точкой в “текущем” месте. Так, продукция \(A \to X Y Z\) имеет 4 пункта \[\begin{align} A & \to . X Y Z \\ A & \to X . Y Z \\ A & \to X Y . Z \\ A & \to X Y Z . \\ \end{align}\]
Продукция с пустым телом \(A \to ε\) имеет единственный пункт \(A \to .\)
- Замыкание множества пунктов
Пусть \(I\) – множество пунктов грамматики \(G\). Тогда \(I^+\) – замыкание множества пунктов, соответствующее правилу:
Если \((A \to α . B β) \in I^+,\) и \(\exists (B \to γ),\) то \((B \to . γ) \in I^+\).
Интуитивно, если в замыкании присутствует \(A \to α . B β,\) то мы ожидаем во входной строке порождения из \(B\), и поэтому мы добавляем первые пункты всех продукций B в замыкание.
Вторая часть, определяющая LR(0)-автомат – функция переходов GOTO. Переход в LR(0)-анализе однозначно определяется текущим состоянием (множеством пунктов I) и текущим грамматическим символом X. \[\mathrm{GOTO}(I,X) = \{ A → α X . β | A → α . X β ∈ I \}^+\]
Один набор пунктов LR(0), называемый каноническим, служит для построения ДКА, называемого LR(0)-автоматом, который используется в процессе анализа. Каждое состояние такого автомата представляет множество LR(0)-пунктов.
Для упрощения построения автомата, грамматики обычно расширяют правилом \(S' \to S\), где \(S\) – стартовый символ исходной грамматики, \(S'\) – стартовый символ новой грамматики. Будем называть это новое правило продукции стартовым правилом (или стартовой продукцией).
Канонический набор \(C\) строится на основе функции переходов начиная с замыкания первого пункта стартовой продукции:
\[\begin{align} & \{S' \to .S\}^+ \in C \\ & \forall I \in C, \forall X \in N \cup T: \mathrm{GOTO}(I, X) \in C \\ \end{align}\]
По сути, \(C\) представляет собой замыкание \(\{\{S' \to S\}^+\}\) по функции \(\mathrm{GOTO}\) для всех символов грамматики.
Отдельно отметим, что “тупиковое” состояние, соответствующее пустому множеству пунктов, обычно исключается из дальнейшего рассмотрения.
“Чистый” LR(0)-анализ
Пронумеруем все элементы канонического набора \(C\) таким образом, чтобы множеству пунктов, содержащему первый пункт стартовой продукции, соответствовал \(0\). Построим LR(0)-автомат, использующий эти номера в качестве номеров состояний. Переходы между состояниями определяются функцией \(\mathrm{GOTO}\).
Будем использовать LR(0)-автомат для принятия решения о действии, совершаемом на очередном шаге анализа. Сам анализ будем производить при помощи автомата с магазинной памятью.
На стеке будем хранить номера состояний LR(0)-автомата. Символы грамматики могут быть однозначно восстановлены по номерам состояний, исходя из того, какой символ находится слева от точки в соответствующем пункте состояния.
Анализатор управляется стеком состояний \(s\), текущим входным символом \(a\) и двумя таблицами, таблицей действий \(\mathrm{ACTION}[s, a]\) и таблицей переходов \(\mathrm{GOTO}[s, X]\). Последняя строится по функции переходов, первая же может отличаться в зависимости от типа анализа.
Программу LR-анализа можно записать в виде псевдокода
= getNextToken();
a while(true) {
= stack.top();
s if(ACTION[s, a] = перенос) {
.push(GOTO[s, a]);
stack= getNextToken();
a else if (ACTION[s, a] = свёртка A -> β) {
} for (i in 1…|β|) stack.pop();
= stack.top();
t .push(GOTO[t, A]);
stackprint("A -> β");
else if (ACTION[s, a] = принятие) {
} break;
else { // ACTION[s, a] = ошибка
} reportError();
} }
В рамках некоторой оптимизации, обычно оставляют только часть таблицы GOTO
, содержащую переходы по нетерминалам, а информацию о новом состоянии GOTO[s, a]
при переносе помещают прямо в таблицу ACTION
:
// ...
if(ACTION[s, a] = перенос j) {
.push(j); // j = GOTO[s, a]
stack= getNextToken();
a // ... }
Таблица \(\mathrm{ACTION}[s, t]\) для “чистого” LR(0)-анализа строится тривиально:
- \(\mathrm{ACTION}[s, t] =\) перенос \(j,\) если \(GOTO[s, t] = j\) и \(j\) не соответствует “тупиковому” состоянию.
- \(\mathrm{ACTION}[s, t] =\) свёртка по продукции \(A \to \alpha\), такой, что последний пункт этой продукции входит в \(s\)-тый элемент канонического набора пунктов \(C\): \((A \to \alpha .) \in I_s,\) \(I_s \in C\).
Отметим, что \(\mathrm{ACTION}[s, t]\) для свёртки не зависит от \(t\). Как следствие, если состояние соответствует последним пунктам нескольких продукций, неизбежно возникает конфликт свёртка/свёртка.
Более того, во многих случаях возникнет конфликт перенос/свёртка.
SLR
Конфликты свёртка/свёртка и перенос/свёртка, возникающие при LR(0)-анализе, можно устранить на основе простых эвристик. В частности, в случае конфликтов пернос/свёртка, можно детерминировано делать выбор в пользу переноса или свёртки. Немного более сложная эвристика – алгоритм SLR.
Алгоритм SLR дополнительно ограничивает случаи, когда возможна свёртка, на основе функции \(\mathrm{FOLLOW}\). Конкретно, второе правило для LR(0) модифицируется следующим образом:
- \(\mathrm{ACTION}[s, t] =\) свёртка по продукции \(A \to \alpha\), такой, что \((A \to \alpha .) \in I_s,\) \(I_s \in C\), и (здесь изменение) \(t \in \mathrm{FOLLOW}(A)\).
Это небольшое изменение позволяет устранить большой класс “очевидных” конфликтов типа перенос/свёртка и свёртка/свёртка.
Пример
Рассмотрим грамматику \[\begin{align} E & \to E + T | T \\ T & \to T * F | F \\ F & \to ( E ) | val \\ \end{align}\]
Расширим эту грамматику стартовым правилом: \[\begin{align} S & \to E \\ E & \to E + T | T \\ T & \to T * F | F \\ F & \to ( E ) | val \\ \end{align}\]
Построим LR(0)-автомат для этой грамматики. Начальное состояние: \[\begin{array}{rll} I_0 = \{S \to .E\}^+ = \{ & S\to .E, \\ & E\to .E+T, \\ & E\to .T, \\ & T \to .T*F, \\ & T \to .F, \\ & F \to .(E), \\ & F \to .val \} \end{array}\]
Функция переходов: \[\begin{align} \mathrm{GOTO}(I_{0}, E) = & I_{1} \\ = & \{\\ & \begin{array}{l} S\to E. \\ E\to E.+T \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, T) = & I_{2} \\ = & \{\\ & \begin{array}{l} E\to T. \\ T\to T.*F \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, F) = & I_{3} \\ = & \{\\ & \begin{array}{l} T \to F. \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, () = & I_{4} \\ = & \{\\ & \begin{array}{l} F \to (.E) \\ \hline E\to .E+T \\ E\to .T \\ T \to .T*F \\ T \to .F \\ F \to .(E) \\ F \to .val \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, val) = & I_{5} \\ = & \{\\ & \begin{array}{l} F \to val. \\ \hline \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{1}, +) = & I_{6} \\ = & \{\\ & \begin{array}{l} E \to E+.T \\ \hline T \to .T*F \\ T \to .F \\ F \to .(E) \\ F \to .val \\ \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{2}, *) = & I_{7} \\ = & \{\\ & \begin{array}{l} T\to T*.F \\ \hline F \to .(E) \\ F \to .val \\ \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{4}, E) = & I_{8} \\ = & \{\\ & \begin{array}{l} F \to (E.) \\ E\to E.+T \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{4}, T) = & I_{2} \\ \mathrm{GOTO}(I_{4}, F) = & I_{3} \\ \mathrm{GOTO}(I_{4}, () = & I_{4} \\ \mathrm{GOTO}(I_{4}, val) = & I_{5} \\ \hline \mathrm{GOTO}(I_{6}, T) = & I_{9} \\ = & \{\\ & \begin{array}{l} E \to E+T. \\ T \to T.*F \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{6}, F) = & I_{3} \\ \mathrm{GOTO}(I_{6}, () = & I_{4} \\ \mathrm{GOTO}(I_{6}, val) = & I_{5} \\ \hline \mathrm{GOTO}(I_{7}, F) = & I_{10} \\ = & \{\\ & \begin{array}{l} T \to T*F. \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{7}, () = & I_{4} \\ \mathrm{GOTO}(I_{7}, val) = & I_{5} \\ \hline \mathrm{GOTO}(I_{8}, )) = & I_{11} \\ = & \{\\ & \begin{array}{l} F \to (E). \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{8}, +) = & I_{6} \\ \hline \mathrm{GOTO}(I_{9}, *) = & I_{7} \\ \end{align}\]
Здесь начальные пункты отделены от пунктов, получаемых при построении замыкания, горизонтальной чертой. Не указанные комбинации аргументов дают пустое множество – тупиковое состояние \(I_\varnothing\).
Диаграмма переходов:
Вычислим FOLLOW
для всех нетерминальных символов грамматики:
\[\begin{align} \mathrm{FOLLOW}(E) & = \{+, ), $\} \\ \mathrm{FOLLOW}(T) & = \{*, +, ), $\} \\ \mathrm{FOLLOW}(F) & = \{*, +, ), $\} \\ \end{align}\]
Пронумеруем продукции следующим образом: \[\begin{align} E & \to E + T & (1) \\ E & \to T & (2)\\ T & \to T * F & (3)\\ T & \to F & (4)\\ F & \to ( E ) & (5) \\ F & \to val & (6)\\ \end{align}\]
Тогда пусть si обозначает перенос и переход в состояние i, rj обозначает свёртку по j-той продукции.
Теперь построим таблицы ACTION и GOTO:
ACTION | val | + | * | ( | ) | $ | GOTO | E | T | F |
---|---|---|---|---|---|---|---|---|---|---|
0 | s5 | s4 | 0 | 1 | 2 | 3 | ||||
1 | s6 | acc | 1 | |||||||
2 | r2 | s7 | r2 | r2 | 2 | |||||
3 | r4 | r4 | r4 | r4 | 3 | |||||
4 | s5 | s4 | 4 | 8 | 2 | 3 | ||||
5 | r6 | r6 | r6 | r6 | 5 | |||||
6 | s5 | s4 | 6 | 9 | 3 | |||||
7 | s5 | s4 | 7 | 10 | ||||||
8 | s6 | s11 | 8 | |||||||
9 | r1 | s7 | r1 | r1 | 9 | |||||
10 | r3 | r3 | r3 | r3 | 10 | |||||
11 | r5 | r5 | r5 | r5 | 11 |
Следует отметить, что при LR(0)-анализе, в строках 2 и 9 для символа * возник бы конфликт перенос-свёртка (эти места в таблице выделены полужирным), поскольку LR(0) полагает свёртку для всех символов. В то же время, r1 и r2 (свёртки в символ \(E\)) невозможны для входного символа *, поскольку \(* \notin \mathrm{FOLLOW}(E)\).
Отметим также, что в случае варианта LR(0), всегда предпочитающего перенос, эта грамматика тоже разбирается.
Рассмотрим теперь, как будет разбираться строка val+val*val
.
Стек | Символы | Вход | Действие |
---|---|---|---|
0 | val+val*val | s5 | |
0 5 | val | +val*val | r6 (\(F \to val\)) |
0 3 | F | +val*val | r4 (\(T \to F\)) |
0 2 | T | +val*val | r2 (\(E \to T\)) |
0 1 | E | +val*val | s6 |
0 1 6 | E+ | val*val | s5 |
0 1 6 5 | E+val | *val | r6 (\(F \to val\)) |
0 1 6 3 | E+F | *val | r4 (\(T \to F\)) |
0 1 6 9 | E+T | *val | s7 |
0 1 6 9 7 | E+T* | val | s5 |
0 1 6 9 7 5 | E+T*val | $ | r6 (\(F \to val\)) |
0 1 6 9 7 10 | E+T*F | $ | r3 (\(T \to T * F\)) |
0 1 6 9 | E+T | $ | r1 (\(E \to E + T\)) |
0 1 | E | $ | acc |
LR(1)
LR(1) аналогичен SLR, с одним ключевым отличием: дабы исключить часть конфликтов перенос/свёртка, LR(1)-пункты дополнительно включают один символ предпросмотра, то есть LR(1)-пункт может иметь вид, скажем \([A \to α . B β, a]\). Здесь \(a\) – это символ, который следует после \(β\).
Соответственно модифицируются определения замыканий LR(1)-пунктов и функция переходов.
- Замыкание
Пусть I – множество пунктов грамматики G. Тогда I⁺ – замыкание множества пунктов, соответствующее правилу:
Если \([A \to α . B β, a] \in I^+,\) и \(\exists (B \to γ),\) то \(\forall b \in \mathrm{FIRST}(β a), [B \to . γ, b] \in I^+\).
Это определение интуитивно понятно: после строки \(α B β\) мы ожидаем терминал \(a\); тогда после \(B\) разумно ожидать терминалы, входящие в \(\mathrm{FIRST}(β a)\) (полагая, что возможно \(β \overset{*}\Rightarrow ε\)).
Определение \(\mathrm{GOTO}\) практически не меняется, так как переход не влияет на символ предпросмотра: \[\mathrm{GOTO}(I,X) = \{ [A \to α X . β, a] | [A \to α . X β, a] ∈ I \}^+\]
Таблица действий строится как
- Если \(\mathrm{GOTO}(i, a) = j,\) то \(\mathrm{ACTION}[i, a] =\) перенос. a – терминал.
- Если \([A \to α ., a] \in I_i\), то \(\mathrm{ACTION}[i, a] =\) свёртка A → α.
Пример
Рассмотрим расширенную грамматику \[\begin{align} S' & \to S \\ S & \to C C \\ C & \to c C | d \\ \end{align}\]
Нетерминалы: \(\{S', S, C\}\)
Терминалы: \(\{c, d\}\)
Стартовый символ: \(S'\)
Построим LR(1)-автомат для этой грамматики. Вычислим \(\mathrm{FIRST}\) для всех нетерминалов: \[\begin{align} \mathrm{FIRST}(S') & = \mathrm{FIRST}(S) \\ \mathrm{FIRST}(S) & = \mathrm{FIRST}(C) \\ \mathrm{FIRST}(C) & = \{c, d\} \\ \end{align}\]
Начальное состояние: \[\begin{array}{rll} I_0 = \{[S' \to .S, $]\}^+ = \{ & [S' \to .S, $], \\ & [S\to .C C, $], \\ & [C\to .c C, c/d], \\ & [C\to .d, c/d] \} \end{array}\]
Функция переходов: \[\begin{align} \mathrm{GOTO}(I_{0}, S) = & I_{1} \\ = & \{\\ & \begin{array}{l} [S' \to S., $] \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, C) = & I_{2} \\ = & \{\\ & \begin{array}{l} [S\to C .C, $] \\ \hline [C\to .c C, $] \\ [C\to .d, $] \\ \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, c) = & I_{3} \\ = & \{\\ & \begin{array}{l} [C\to c .C, c/d] \\ \hline [C\to .c C, c/d], \\ [C\to .d, c/d] \\ \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, d) = & I_{4} \\ = & \{\\ & \begin{array}{l} [C\to d., c/d] \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{2}, C) = & I_{5} \\ = & \{\\ & \begin{array}{l} [S\to C C., $] \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{2}, c) = & I_{6} \\ = & \{\\ & \begin{array}{l} [C\to c .C, $] \\ \hline [C\to .c C, $], \\ [C\to .d, $] \end{array}\\ & \} \\ \mathrm{GOTO}(I_{2}, d) = & I_{7} \\ = & \{\\ & \begin{array}{l} [C\to d., $] \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{3}, C) = & I_{8} \\ = & \{\\ & \begin{array}{l} [C\to c C., c/d] \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{3}, c) = & I_{3} \\ \mathrm{GOTO}(I_{3}, d) = & I_{4} \\ \mathrm{GOTO}(I_{6}, C) = & I_{9} \\ = & \{\\ & \begin{array}{l} [C\to c C., $] \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{6}, c) = & I_{6} \\ \mathrm{GOTO}(I_{6}, d) = & I_{7} \\ \end{align}\]
Диаграмма переходов:
Пронумеруем правила грамматики \[\begin{align} S & \to C C & (1)\\ C & \to c C & (2)\\ C & \to d & (3)\\ \end{align}\]
ACTION | c | d | $ | GOTO | S | C |
---|---|---|---|---|---|---|
0 | s3 | s4 | 0 | 1 | 2 | |
1 | acc | 1 | ||||
2 | s6 | s7 | 2 | 5 | ||
3 | s3 | s4 | 3 | 8 | ||
4 | r3 | r3 | 4 | |||
5 | r1 | 5 | ||||
6 | s6 | s7 | 6 | 9 | ||
7 | r3 | 7 | ||||
8 | r2 | r2 | 8 | |||
9 | r2 | 9 |
LALR(1)
LALR(1) – это упрощение LR(1), в котором используется множество пунктов LR(0), но каждый из них расширен множеством допустимых символов предпросмотра из соответствующих LR(1)-пунктов. При этом, количество состояний совпадает с количеством состояний в SLR, но вероятность возникновения конфликтов перенос/свёртка ниже – хотя и выше чем в полноценном LR(1) анализе.
Простой способ построения LALR-анализатора заключается в построении LR(1) анализатора с последующим отождествлением состояний, имеющих одинаковые первые компоненты LR(1)-пунктов (соответствующие, вообще говоря, LR(0)-пунктам)
Пример
Рассмотрим пример для LR(1). Состояния 4 и 7 содержат первой частью пункта \(C \to d .\); состояния 3 и 6 – \(C\to c.C\); состояния 8 и 9 – \(C\to cC.\); Объединим эти пары состояний в новые состояния 47, 36 и 89 соответственно.
ACTION | c | d | $ | GOTO | S | C |
---|---|---|---|---|---|---|
0 | s36 | s47 | 0 | 1 | 2 | |
1 | acc | 1 | ||||
2 | s36 | s47 | 2 | 5 | ||
36 | s36 | s47 | 36 | 89 | ||
47 | r3 | r3 | r3 | 47 | ||
5 | r1 | 5 | ||||
89 | r2 | r2 | r2 | 89 |
Использование приоритета и ассоциативности для разрешения неоднозначности
Рассмотрим неоднозначную грамматику выражений \[E \to E + E | E * E | (E) | val.\]
Она очевидно является неоднозначной, поскольку не определяет ни ассоциативность, ни приоритет операторов \(+\) и \(*\). Ранее мы рассматривали однозначную грамматику, порождающую тот же язык, которая включала приоритет и ассоциативность операторов непосредственно в определение. С другой стороны, определение той грамматики достаточно объёмное по сравнению с неоднозначным. Возникает вопрос: нельзя ли разрешить конфликты, возникающие в силу неоднозначности грамматики, используя информацию о приоритете и ассоциативности операторов? Попробуем построить SLR-анализатор для этой неоднозначной грамматики.
Введём правило расширенной грамматики \[S\to E\]
Построим LR(0)-автомат. Начальное состояние: \[\begin{array}{rll} I_0 = \{S \to .E\}^+ = \{ & S \to .E \\ & E \to .E + E \\ & E \to .E * E \\ & E \to .(E) \\ & E \to .val \\ \} \end{array}\]
Функция переходов: \[\begin{align} \mathrm{GOTO}(I_{0}, E) = & I_{1} \\ = & \{\\ & \begin{array}{l} S \to E . \\ E \to E . + E \\ E \to E . * E \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, () = & I_{2} \\ = & \{\\ & \begin{array}{l} E \to (.E) \\ \hline E \to .E + E \\ E \to .E * E \\ E \to .(E) \\ E \to .val \\ \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, val) = & I_{3} \\ = & \{\\ & \begin{array}{l} E \to val. \\ \hline \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{1}, +) = & I_{4} \\ = & \{\\ & \begin{array}{l} E \to E + . E \\ \hline E \to .E + E \\ E \to .E * E \\ E \to .(E) \\ E \to .val \\ \end{array}\\ & \} \\ \mathrm{GOTO}(I_{1}, *) = & I_{5} \\ = & \{\\ & \begin{array}{l} E \to E * . E \\ \hline E \to .E + E \\ E \to .E * E \\ E \to .(E) \\ E \to .val \\ \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{2}, E) = & I_{6} \\ = & \{\\ & \begin{array}{l} E \to (E.) \\ E \to E .+ E \\ E \to E .* E \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{2}, () = & I_{2} \\ \mathrm{GOTO}(I_{2}, val) = & I_{3} \\ \hline \mathrm{GOTO}(I_{4}, E) = & I_{7} \\ = & \{\\ & \begin{array}{l} E \to E + E . \\ E \to E . + E \\ E \to E . * E \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{4}, () = & I_{2} \\ \mathrm{GOTO}(I_{4}, val) = & I_{3} \\ \hline \mathrm{GOTO}(I_{5}, E) = & I_{8} \\ = & \{\\ & \begin{array}{l} E \to E * E . \\ E \to E . + E \\ E \to E . * E \\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{5}, () = & I_{2} \\ \mathrm{GOTO}(I_{5}, val) = & I_{3} \\ \hline \mathrm{GOTO}(I_{6}, +) = & I_{4} \\ \mathrm{GOTO}(I_{6}, *) = & I_{5} \\ \mathrm{GOTO}(I_{6}, )) = & I_{9} \\ = & \{\\ & \begin{array}{l} E \to (E). \\ \hline \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{7}, +) = & I_{4} \\ \mathrm{GOTO}(I_{7}, *) = & I_{5} \\ \hline \mathrm{GOTO}(I_{8}, +) = & I_{4} \\ \mathrm{GOTO}(I_{8}, *) = & I_{5} \\ \end{align}\]
Здесь начальные пункты отделены от пунктов, получаемых при построении замыкания, горизонтальной чертой. Не указанные комбинации аргументов дают пустое множество – тупиковое состояние \(I_\varnothing\).
Диаграмма переходов:
Вычислим FOLLOW
для всех нетерминальных символов грамматики:
\[\begin{align} \mathrm{FOLLOW}(S) & = \{$\} \\ \mathrm{FOLLOW}(E) & = \{*, +, ), $\} \\ \end{align}\]
Пронумеруем продукции следующим образом: \[\begin{align} E & \to E + E & (1)\\ E & \to E * E & (2)\\ E & \to (E) & (3)\\ E & \to val & (4)\\ \end{align}\]
Теперь построим таблицу ACTION:
ACTION | val | + | * | ( | ) | $ |
---|---|---|---|---|---|---|
0 | s3 | s2 | ||||
1 | s4 | s5 | acc | |||
2 | s3 | s2 | ||||
3 | r4 | r4 | r4 | r4 | ||
4 | s3 | s2 | ||||
5 | s3 | s2 | ||||
6 | s4 | s5 | s9 | |||
7 | s4/r1 | s5/r1 | r1 | r1 | ||
8 | s4/r2 | s5/r2 | r2 | r2 | ||
9 | r3 | r3 | r3 | r3 |
Мы видим, что ожидаемо возникают конфликты перенос/свёртка для символов \(+\) и \(*\).
В состояние 7 анализатор попадает, разобрав строку \(E+E\). Конфликт для символа \(+\) связан с отсутствием в грамматике информации об ассоциативности оператора \(+\). Если оператор левоассоциативен, то приоритетной должна быть свёртка (поскольку выражение \(E+E+E\) должно быть интерпретировано как \((E+E)+E\), и мы должны свернуть первый прочитанный \(E+E\) как если бы он был в скобках). Если правоассоциативен – то перенос (соображения аналогичные).
Конфликт в состоянии 7 для символа \(*\) связан с отсутствием информации об относительном приоритете операторов \(+\) и \(*\). Если \(+\) имеет меньший приоритет, то выбор должен быть сделан в пользу переноса (действительно, выражение \(E+E*E\) должно быть разобрано как \(E+(E*E)\), и тогда, прочитав в нём \(E+E\) мы должны прочитать \(*\), чтобы можно было произвести свёртку \(E*E\)). Если \(*\) имеет меньший приоритет, то выбор – в пользу свёртки. Если приоритет одинаковый, то выбор определяется ассоциативностью.
Аналогично можно рассуждать о конфликтах в состоянии 8, в которое анализатор приходит, прочитав строку \(E*E\).
Тогда для левоассоциативных операторов, и \(*\) имеющего более высокий приоритет, таблицу \(\mathrm{ACTION}\) можно записать:
ACTION | val | + | * | ( | ) | $ |
---|---|---|---|---|---|---|
0 | s3 | s2 | ||||
1 | s4 | s5 | acc | |||
2 | s3 | s2 | ||||
3 | r4 | r4 | r4 | r4 | ||
4 | s3 | s2 | ||||
5 | s3 | s2 | ||||
6 | s4 | s5 | s9 | |||
7 | r1 | s5 | r1 | r1 | ||
8 | r2 | r2 | r2 | r2 | ||
9 | r3 | r3 | r3 | r3 |
Таблица GOTO (только значащие строки):
GOTO | E |
---|---|
0 | 1 |
2 | 6 |
4 | 7 |
5 | 8 |
Здесь может показаться, что мы определяем приоритет и ассоциативность для токенов, однако на самом деле мы определяем их также и для правил грамматики, содержащих эти токены. Показательным будет пример для грамматики с унарным оператором \(-\): \[\begin{align} E & \to E - E | - E | val \end{align}\]
Начальное состояние \[\begin{align} I_0 = \{S\to .E\}^+ = \{ & S\to E\\ & E\to .E - E\\ & E\to . - E\\ & E\to . val\\ \} \end{align}\]
Здесь оператор \(-\) имеет разный приоритет (и ассоциативность) в разных продукциях.
Функция \(\mathrm{GOTO}\):
\[\begin{align} \mathrm{GOTO}(I_{0}, E) = & I_{1} \\ = & \{\\ & \begin{array}{l} S \to E . \\ E\to E. - E\\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, -) = & I_{2} \\ = & \{\\ & \begin{array}{l} E\to - . E\\ \hline E\to .E - E\\ E\to . - E\\ E\to . val\\ \end{array}\\ & \} \\ \mathrm{GOTO}(I_{0}, val) = & I_{3} \\ = & \{\\ & \begin{array}{l} E\to val .\\ \hline \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{1}, -) = & I_{4} \\ = & \{\\ & \begin{array}{l} E\to E - .E\\ \hline E\to .E - E\\ E\to . - E\\ E\to . val\\ \end{array}\\ & \} \\ \hline \mathrm{GOTO}(I_{2}, E) = & I_{5} \\ = & \{\\ & \begin{array}{l} E\to - E .\\ E\to E . - E\\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{2}, -) = & I_{2} \\ \mathrm{GOTO}(I_{2}, val) = & I_{3} \\ \hline \mathrm{GOTO}(I_{4}, E) = & I_{6} \\ = & \{\\ & \begin{array}{l} E\to E - E .\\ E\to E . - E\\ \hline \end{array}\\ & \} \\ \mathrm{GOTO}(I_{4}, -) = & I_{2} \\ \mathrm{GOTO}(I_{4}, val) = & I_{3} \\ \hline \mathrm{GOTO}(I_{5}, -) = & I_{4} \\ \hline \mathrm{GOTO}(I_{6}, -) = & I_{4} \\ \end{align}\]
Номера продукций: \[\begin{align} E & \to E - E & (1) \\ E & \to - E & (2) \\ E & \to val & (3) \\ \end{align}\]
\[\mathrm{FOLLOW}(E) = \{-, \$\}\]
Таблица \(\mathrm{ACTION}\):
ACTION | - | val | $ | GOTO | E |
---|---|---|---|---|---|
0 | s2 | s3 | 0 | 1 | |
1 | s4 | acc | 1 | ||
2 | s2 | s3 | 2 | 5 | |
3 | r3 | r3 | 3 | ||
4 | s2 | s3 | 4 | 6 | |
5 | s4/r2 | r2 | 5 | ||
6 | s4/r1 | r1 | 6 |
В состоянии 5 автомат оказывается, прочитав строку \(-E\). Обнаружив очередной \(-\), он должен произвести свёртку, так как унарный минус имеет более высокий приоритет, чем бинарный.
Таким образом, продукция \(E\to -E\) имеет более высокий приоритет, чем продукция \(E \to E - E\), и тогда, в случае конфликта, производится операция, связанная с этой продукцией.
Рассмотрим как автомат будет читать строку \(val--val-val\):
Стек | Символы | Строка | Действие |
---|---|---|---|
0 | \(val--val-val\) | s3 | |
0 3 | \(val\) | \(--val-val\) | r3 |
0 1 | \(E\) | \(--val-val\) | s4 |
0 1 4 | \(E-\) | \(-val-val\) | s2 |
0 1 4 2 | \(E--\) | \(val-val\) | s3 |
0 1 4 2 3 | \(E--val\) | \(-val\) | r3 |
0 1 4 2 5 | \(E--E\) | \(-val\) | r2 |
0 1 4 6 | \(E-E\) | \(-val\) | r1 |
0 1 | \(E\) | \(-val\) | s4 |
0 1 4 | \(E-\) | \(val\) | s3 |
0 1 4 3 | \(E-val\) | \(\$\) | r3 |
0 1 4 6 | \(E-E\) | \(\$\) | r1 |
0 1 | \(E\) | \(\$\) | acc |