Лекция 14: LR-разбор
Содержание лекции
Метод определения принадлежности заданной цепочки языку, заданному
формальной грамматикой, и восстановления вывода цепочки:
LR-разбор. Определение понятий ситуации грамматики и состояния
разбора. Состояния LR(0)-разбора и LR(1) разбора. Построение
множества возможных состояний и переходов. Построение таблиц для
LR(0) и LR(1) разбора. Алгоритм работы парсера.
Пусть задана некоторая контекстно-свободная грамматика. Рассмотрим
произвольную терминальную цепочку. Требуется определить, принадлежит
ли она языку, заданному данной грамматикой, и если принадлежит, то
восстановить вывод ее вывод. Процесс решения этой задачи называется
"разбором", программа, выполняющая разбор, называется "парсером"
(от английского "to parse") или синтаксическим анализатором.
Наиболее популярны два метода разбора: рекурсивный разбор и разбор,
использующий стек и конечный автомат (часто употребляется термин
"автомат с магазинной памятью"). Мы не будем рассматривать
рекурсивный разбор, потому что класс грамматик, допускающий его,
значительно уже, чем в случае LR-разбора, и зачастую леворекурсивная
грамматика конкретного языка выглядит неестественно, даже если язык
допускает рекурсивный разбор. Перейдем к определению LR-разбора --
разбора, использующего стек и конечный автомат. Буква L означает,
что исходная цепочка просматривается слева направо, R -- что при
этом строится правый вывод.
Определим понятие LR-процесса. Состояние LR-процесса в любой момент
времени описывается стеком LR-процесса и непрочитанной частью
входной цепочки. Стек LR-процесса хранит обычные символы и
метасимволы. Удобно представлять стек LR-процесса "повернутым
невстречу входной цепочке".
+-----------------
|содержимое стека << входная цепочка
+-----------------
Стек LR-процесса представляет собой слово из терминалов и
нетерминалов, которое наращивается с правого конца.
При выполнении LR-процесса в любой момент времени возможны следующие
два действия.
-
Сдвиг.
Мы берем первый непрочитанный символ входной цепочки
и переносим его в стек. Пример:
+---------
до сдвига | aMBbaC abcdef
+---------
+---------
после сдвига | aMBbaCa bcdef
+---------
(символ "a" передвинут из начала входной цепочки в стек).
-
Свертка
по некоторому правилу. Пусть конец слова, содержащегося
в стеке, совпадает с правой частью некоторого правила грамматики.
Тогда сверткой по этому правилу называется замена этого конца
на левую часть правила, т.е. на один нетерминал. Рассмотрим
следующий пример. Пусть в грамматике имеется правило
A -> baCa
Состояние LR-процесса до свертки:
+---------
| aMBbaCa bcdef
+---------
Состояние LR-процесса после свертки по этому правилу:
+---------
| aMBA bcdef
+---------
(конец "baCa" слова в стеке заменен на "A").
Определение.
LR-процесс называется успешным, если по его окончании
непрочитанная часть входной цепочки пуста (т.е. входная цепочка
прочитана до конца), а в стеке находится начальный метасимвол
грамматики.
Конечное состояние успешного LR-процесса:
+----
| S
+----
Пример: рассмотрим грамматику
S -> F
F -> a
F -> F+F
F -> F*F
Рассмотрим выводимую в этой грамматике цепочку "a+a*a".
Изобразим последовательные состояния и действия
успешного LR-процесса для этой цепочки.
-
Начальное состояние
+--
| a+a*a
+--
действие -- сдвиг.
-
Состояние LR-процесса
+--
|a +a*a
+--
действие -- свертка по правилу F -> a
-
+--
|F +a*a
+--
действие -- сдвиг.
-
+---
|F+ a*a
+---
действие -- сдвиг.
-
+---
|F+a *a
+---
действие -- свертка по правилу F -> a
-
+---
|F+F *a
+---
действие -- сдвиг.
-
+----
|F+F* a
+----
действие -- сдвиг.
-
+-----
|F+F*a
+-----
действие -- свертка по правилу F -> a
-
+-----
|F+F*F
+-----
действие -- свертка по правилу F -> F*F
-
+----
|F+F
+----
действие -- свертка по правилу F -> F+F
-
+--
|F
+--
действие -- свертка по правилу S -> F
-
+--
|S
+--
конечное состояние успешного LR-процесса.
Предложение.
Имеется взаимно-однозначное соответствие
между успешными LR-процессами и правыми выводами терминальных
цепочек.
Приведенному выше успешному LR-процессу соответствует правый вывод
S -> F -> F+F -> F+F*F -> F+F*a -> F+a*a -> a+a*a
LR-процесс работает в направлении, противоположном выводу -- справа
налево, начиная с терминальной цепочки и заканчивая начальным
метасимволом грамматики. Свертки LR-процесса в точности
соответствуют заменам в выводе. Для накопления правой части правила
в стеке LR-процесса между свертками делается необходимое число
сдвигов. Получаются именно правые выводу, потому что правее
нетерминала, появляющегося на вершине стека в результате свертки,
стоят только терминалы, составляющие непрочитанную часть входной
цепочки (если рассматривать соответствующий вывод, то замена,
соответствующая свертке, всегда применяется к самому правому
нетерминалу выводимой цепочки).
Следующее предложение очевидным образом вытекает из предыдущего.
Предложение.
Для всякой выводимой терминальной цепочки существует
успешный LR-процесс.
Проблема состоит в том, что априори не всегда можно решить,
какое действие в данный конкретный момент следует выполнить:
сдвиг или свертку, если свертку, то по какому правилу -- ведь
могут быть ситуации, когда возможны свертки по разным правилам.
Ошибочное решение может привести к тому, что LR-процесс невозможно
будет довести до успешного завершения. Например, если в приведенном
выше примере LR-процесса на шаге 1)
+--
|a +a*a
+--
мы бы сделали сдвиг вместо свертки по правилу F -> a, то LR-процесс
не удалось бы довести до успешного завершения. Для разрешения таких
вопросов строится конечный автомат -- вернее, таблицы LR-разбора, в
которых указывается, какое действие следует совершить в зависимости
от вершины стека и очередного символа входной цепочки. Стек
LR-разбора хранит не цепочки из терминалов и нетерминалов, как в
случае LR-процесса, а номера состояний разбора. Тем не менее его
глубина равна глубине стека соответствующего LR-процесса, и полезно
представлять эти два стека параллельными (хотя реально стек
LR-процесса в практических вычислениях не используется).
Перейдем к построению конечного автомата. Рассмотрим сначала
более простой случай -- LR(0)-разбор. На практике используется
LR(1)-разбор, определения для LR(1)-разбора будут даны позже.
Определение.
LR(0)-ситуацией грамматики называется некоторое
правило этой грамматики с отмеченной позицией в правой части.
Позиция может быть перед началом правой части, где-нибудь в
середине правой части или за концом правой части.
Давайте с данного момента времени вместо символа "->" (стрелка) в
записи правила использовать двоеточие, как это принято в YACC'е.
Правилу A: bCd (раньше мы бы написали A -> bCdE) соответствуют
четыре LR(0)-ситуации:
A: _bCd, A: b_Cd, A: bC_d, A: bCd_
Позиция в правой части правила отмечается символом подчеркивания.
Определение.
LR(0)-состоянием разбора называется конечное
подмножество множества всех возможных LR(0)-ситуаций.
Так как имеется лишь конечное число LR(0)-ситуаций, то
число LR(0)-состояний также конечно.
Построение множества возможных LR(0)-состояний и переходов
В автомате, который мы будем строить, не все состояния будут
использоваться. Проиллюстрируем процесс построения множества
возможных LR(0)-состояний на примере. Рассмотрим грамматику
S: F
F: T | F+T
T: a | (F)
Язык, заданный этой грамматикой, состоит из арифметических формул,
включающих только сложение; в качестве имени переменной используется
только буква "a". Грамматика детерминированная. Пример цепочки,
принадлежащей языку: a+(a+a).
Постороение LR(0)-состояний
-
Начинаем с ситуации
S: _F,
где S -- начальный метасимвол грамматики, отмеченная позиция перед
началом правой части правила. Если было бы несколько правил с
левой частью S, то надо было бы добавить их все. Далее,
ОЧЕНЬ ВАЖНО!!! если символ подчеркивания в правой части некоторой
ситуации, уже включенной в множество, стоит перед
нетерминалом, то надо добавить все ситуации для правил, левая часть
которых совпадает с этим нетерминалом, с позицией перед началами
правых частей этих правил.
В нашем случае символ подчеркивания стоит перед нетерминалом "F",
поэтому надо добавить ситуации
F: _T, F: _F+T.
Теперь получили метасимволы "T" и "F" после подчеркивания.
Значит, еще раз надо добавить ситуации для правил с левой
частью "T" и с левой частью "F". Во втором случае мы уже не добавим
ничего нового (замечание на будущее: для LR(1)-разбора и здесь
будут добавляться новые ситуации). Итак, добавляются ситуации
T: _a, T: _(F).
Окончательно множество ситуаций, составляющих начальное состояние
LR(0)-разбора, равно
{ S: _F, F: _T, F: _F+T, T: _a, T: _(F) }.
-
Делая сдвиг метасимвола "F" в ситуациях S: _F и F: _F+T
начального состояния, получаем состояние
{ S: F_, F: F_+T },
которое состоит из двух ситуаций.
-
Делая сдвиг "T" в ситуации F: _T начального состояния, получаем
состояние
{ F: T_ }.
Это состояние состоит всего из одной ситуации.
-
Делая сдвиг "a" в ситуации T: _a начального состояния, получаем
состояние
{ T: a_ }.
-
Наконец, делая сдвиг круглой скобки "(" в ситуации
T: _(F) начального состояния, получаем ситуацию
T: (_F)
Здесь опять приходится, как и в случае состояния 1, добавлять
все ситуации с левой частью "F" и позицией перед началом правой
части,
F: _T, F: _F+T,
затем все ситуации с левой частью "T" (т.к. подчеркивание
стоит перед "T") и ситуации с левой частью "F" (что в случае
LR(0)-разбора, к счастью, ничего не добавляет, но, увы, в случае
LR(1)-разбора придется добавить еще ситуации...).
T: _a, T: _(F).
Окончательно получаем, что шестое состояние состоит из
следующих ситуаций:
{ T: (_F), F: _T, F: _F+T, T: _a, T: _(F) }.
-
Делая сдвиг символа "+" в ситуации F: F_+T состояния 2,
получим состояние
F: F+_T.
Так как подчеркивание стоит перед нетерминалом "T", надо добавить
все ситуации с левой частью "T":
T: _a, T: _(F).
Окончательно получаем состояние
{ F: F+_T, T: _a, T: _(F) }.
-
Делая сдвиг метасимвола "F" в ситуациях T: (_F) и F: _F+T
состояния 5, получим состояние
{ T: (F_), F: F_+T }.
Отметим, что делая сдвиги символов "T", "a", "(" в состоянии 5,
мы уже не получим новых состояний -- все они уже были построены
раньше.
-
Делая сдвиг метасимвола "T" в ситуации F: F+_T состояния 6,
получаем состояние
{ F: F+T_ }.
-
Делая сдвиг закрывающей скобки в T: (T_) состояния 7,
получим состояние
{ F: (F)_ }.
Итак, мы построили все возможные состояния LR(0)-разбора.
Их получилось девять. Выпишем их еще раз:
1 { S: _F, F: _T, F: _F+T, T: _a, T: _(F) }
2 { S: F_, F: F_+T }
3 { F: T_ }
4 { T: a_ }
5 { T: (_F), F: _T, F: _F+T, T: _a, T: _(F) }
6 { F: F+_T, T: _a, T: _(F) }
7 { T: (F_), F: F_+T }
8 { F: F+T_ }
9 { F: (F)_ }
Эти состояния и будут состояниями нашего конечного автомата. В
каждом состоянии нужно определить действие, которое совершается --
сдвиг или свертка, и если свертка, то по какому правилу. При сдвиге
определенного терминала осуществляется переход в новое состояние.
Будем говорить, что в состоянии возможна свертка по правилу,
если это состояние содержит ситуацию, соответствующую этому правилу,
с позицией за концом правой части правила. Например, в состоянии
2 возможна свертка по правилу S: F, в состоянии 3 -- свертка по
правилу F: T. Будем говорить, что в состоянии возможен сдвиг,
если есть ситуация, в которой после в правой части за отмеченной
позицией следует терминальный символ. В нашем случае сдвиги
возможны в состояниях 1, 2, 5, 6, 7. Естественным образом
определяются переходы при сдвигах: например, из состояния 7
{ T: (F_), F: F_+T }
при сдвиге символа ")" мы переходим в состояние 9
{ F: (F)_ },
поскольку только первая ситуация состояния 7 содержит
закрывающую скобку после символа подчеркивания, и при сдвиге
в этой ситуации мы получаем состояние 9.
Мы будем говорить, что в данном состоянии имеется конфликт типа
сдвиг-свертка, если оно допускает и сдвиг, и свертку. Будем
говорить, что имеется конфликт типа свертка-свертка, если допустима
свертка по двум разным правилам. В нашем случае имеется конфликт
типа сдвиг-свертка в состоянии 2:
{ S: F_, F: F_+T }.
Конфликтов типа свертка-свертка нет.
Будем говорить, что грамматика допускает LR(0)-разбор или
является LR(0)-грамматикой, если построенной множество
LR(0)-состояний не имеет конфликтов. Рассмотренная нами
грамматика не является LR(0) грамматикой, поскольку имеет
один конфликт типа сдвиг-свертка во втором состоянии.
Вообще говоря, класс LR(0)-грамматик достаточно узок.
LR(0)-парсер должен принимать решение о сдвиге или свертке,
а также о том, по какому правилу выполнять свертку, только
по состоянию стека, не используя информацию о первом символе
непрочитанной части цепочки. Гораздо более общим методом
является LR(1)-разбор, в котором парсер знает первый
символ непрочитанной части цепочки (т.е. следующий символ)
и может использовать эту дополнительную информацию. Так,
в нашем случае во втором состоянии
{ S: F_, F: F_+T }
надо выполнять свертку по правилу S: F_, если цепочка прочитана до
конца, и сдвиг, если первый непрочитанный символ -- это знак "+"
(в остальных случаях цепочка не принадлежит языку). Не будем пока
формально выписывать состояния LR(1)-разбора (их очень много), а
выпишем таблицу LR(1)-парсера, основываясь на построенном множестве
LR(0)-состояний и приведенном выше соображении. Строки таблицы
нумеруются состояниями разбора, столбцы -- первым непрочитанным
символом цепочки, а также всеми метасимволами, которые встречаются в
правых частях правил. В клетке таблицы стоит действие, которое
выполняется в этом состоянии по данному первому символу
непрочитанной части цепочки. Если это действие -- сдвиг, то указывается
номер нового состояния. Если свертка, то указывается, по какому
правилу. Среди действий есть также слово "допуск", что означает
успешное завершение разбора, и "ошибка", означающее, что цепочка не
принадлежит языку.
сост |
a |
( |
+ |
) |
конец |
F |
T |
1 |
сдвиг -> 4 |
сдвиг -> 5 |
ошибка |
ошибка |
ошибка |
сдвиг -> 2 |
сдвиг -> 3 |
2 |
ошибка |
ошибка |
сдвиг -> 6 |
ошибка |
свертка S: F, допуск |
ошибка |
ошибка |
3 |
свертка по правилу F: T |
4 |
свертка по правилу T: a
|
5 |
сдвиг -> 4 |
сдвиг -> 5 |
ошибка |
ошибка |
ошибка |
сдвиг -> 7 |
сдвиг -> 3 |
6 |
сдвиг -> 4 |
сдвиг -> 5 |
ошибка |
ошибка |
ошибка |
ошибка |
сдвиг -> 8 |
7 |
ошибка |
ошибка |
сдвиг -> 6 |
сдвиг -> 9 |
ошибка |
ошибка |
ошибка |
8 |
свертка по правилу F: F+T
|
9 |
свертка по правилу F: (F)
|
Упражнение.
Постройте систему LR(0)-состояний для грамматики
S: F
F: a | (F+F)
Будет ли эта грамматика обладать свойством LR(0)?
Язык, заданваемый приведенной выше грамматикой, состоит из
цепочек вида
a, (a+a), (a+(a+a)), ((a+a)+a), ((a+a)+(a+a)), ...
т.е. всевозможных формул, содержащих переменную "a" и
знак операции "+", в которых каждое сложение обязательно
заключается в скобки (опрерация "+" как бы неассоциативна).