Лекция 14: LR-разбор

Содержание лекции

Метод определения принадлежности заданной цепочки языку, заданному формальной грамматикой, и восстановления вывода цепочки: LR-разбор. Определение понятий ситуации грамматики и состояния разбора. Состояния LR(0)-разбора и LR(1) разбора. Построение множества возможных состояний и переходов. Построение таблиц для LR(0) и LR(1) разбора. Алгоритм работы парсера.

Пусть задана некоторая контекстно-свободная грамматика. Рассмотрим произвольную терминальную цепочку. Требуется определить, принадлежит ли она языку, заданному данной грамматикой, и если принадлежит, то восстановить вывод ее вывод. Процесс решения этой задачи называется "разбором", программа, выполняющая разбор, называется "парсером" (от английского "to parse") или синтаксическим анализатором.

Наиболее популярны два метода разбора: рекурсивный разбор и разбор, использующий стек и конечный автомат (часто употребляется термин "автомат с магазинной памятью"). Мы не будем рассматривать рекурсивный разбор, потому что класс грамматик, допускающий его, значительно уже, чем в случае LR-разбора, и зачастую леворекурсивная грамматика конкретного языка выглядит неестественно, даже если язык допускает рекурсивный разбор. Перейдем к определению LR-разбора -- разбора, использующего стек и конечный автомат. Буква L означает, что исходная цепочка просматривается слева направо, R -- что при этом строится правый вывод.

Определим понятие LR-процесса. Состояние LR-процесса в любой момент времени описывается стеком LR-процесса и непрочитанной частью входной цепочки. Стек LR-процесса хранит обычные символы и метасимволы. Удобно представлять стек LR-процесса "повернутым невстречу входной цепочке".

+----------------- |содержимое стека << входная цепочка +----------------- Стек LR-процесса представляет собой слово из терминалов и нетерминалов, которое наращивается с правого конца.

При выполнении LR-процесса в любой момент времени возможны следующие два действия.

  1. Сдвиг. Мы берем первый непрочитанный символ входной цепочки и переносим его в стек. Пример: +--------- до сдвига | aMBbaC abcdef +--------- +--------- после сдвига | aMBbaCa bcdef +--------- (символ "a" передвинут из начала входной цепочки в стек).
  2. Свертка по некоторому правилу. Пусть конец слова, содержащегося в стеке, совпадает с правой частью некоторого правила грамматики. Тогда сверткой по этому правилу называется замена этого конца на левую часть правила, т.е. на один нетерминал. Рассмотрим следующий пример. Пусть в грамматике имеется правило 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-процесса для этой цепочки.

  1. Начальное состояние +-- | a+a*a +-- действие -- сдвиг.
     
  2. Состояние LR-процесса +-- |a +a*a +-- действие -- свертка по правилу F -> a
     
  3. +-- |F +a*a +-- действие -- сдвиг.
     
  4. +--- |F+ a*a +--- действие -- сдвиг.
     
  5. +--- |F+a *a +--- действие -- свертка по правилу F -> a
     
  6. +--- |F+F *a +--- действие -- сдвиг.
     
  7. +---- |F+F* a +---- действие -- сдвиг.
     
  8. +----- |F+F*a +----- действие -- свертка по правилу F -> a
     
  9. +----- |F+F*F +----- действие -- свертка по правилу F -> F*F
     
  10. +---- |F+F +---- действие -- свертка по правилу F -> F+F
     
  11. +-- |F +-- действие -- свертка по правилу S -> F
     
  12. +-- |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)-состояний

  1. Начинаем с ситуации 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) }.
  2. Делая сдвиг метасимвола "F" в ситуациях S: _F и F: _F+T начального состояния, получаем состояние { S: F_, F: F_+T }, которое состоит из двух ситуаций.
     
  3. Делая сдвиг "T" в ситуации F: _T начального состояния, получаем состояние { F: T_ }. Это состояние состоит всего из одной ситуации.
     
  4. Делая сдвиг "a" в ситуации T: _a начального состояния, получаем состояние { T: a_ }.
  5. Наконец, делая сдвиг круглой скобки "(" в ситуации 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) }.
  6. Делая сдвиг символа "+" в ситуации F: F_+T состояния 2, получим состояние F: F+_T. Так как подчеркивание стоит перед нетерминалом "T", надо добавить все ситуации с левой частью "T": T: _a, T: _(F). Окончательно получаем состояние { F: F+_T, T: _a, T: _(F) }.
  7. Делая сдвиг метасимвола "F" в ситуациях T: (_F) и F: _F+T состояния 5, получим состояние { T: (F_), F: F_+T }. Отметим, что делая сдвиги символов "T", "a", "(" в состоянии 5, мы уже не получим новых состояний -- все они уже были построены раньше.
     
  8. Делая сдвиг метасимвола "T" в ситуации F: F+_T состояния 6, получаем состояние { F: F+T_ }.
  9. Делая сдвиг закрывающей скобки в 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" и знак операции "+", в которых каждое сложение обязательно заключается в скобки (опрерация "+" как бы неассоциативна).