•T; 1>, которые говорят о том, что с
первой позиции мы ожидаем строку, порожденную из E по продукции E->E+T либо по
продукции E->T. Остальные три ситуации добавляются повторным применением
предсказателя. Считыватель добавляет в следующее множество (M2) ситуацию a•;
1>. На этом построение множества M1 заканчивается.
Продолжаем с M2. Это множество изначально включает ситуацию
a•; 1>,
полученную из M1 считывателем со сдвигом указателя положения за встреченный
терминал a, т.к. он является очередным входным символом. Завершатель заставляет
добавить в множество M2 ситуацию P•; 1>. Дальнейшее применение завершателя
вынуждает добавить в M2 еще четыре ситуации T•; 1>, T•*P; 1>, E•+T; 1>
и #E•#; 0>. Ни в одной из ситуаций в M2 указатель положения не стоит перед
нетерминалом, поэтому предсказателю здесь делать нечего. Считыватель добавляет в
следующее множество (M3) ситуацию E+•T; 1>. На этом построение множества M2
заканчивается.
Множества M3 – M5 строятся аналогично.
Из схемы видно, что все ситуации связаны указателями. Указатель от ситуации Ki к
ситуации Kj проводятся тогда, когда Kj порождена из Ki любым из трех операторов.
Кроме того, видно, что часть ситуаций в построенных множествах несущественные.
Например, в M4 это ситуации E•+T; 1> и •T*P; 3>, которые не приводят к
завершающим ситуациям вида •; k>. Последовательное выбрасывание таких
ситуаций приводят к связному списку существенных ситуаций. См. СЛАЙД 15.
Восстановление дерева разбора входной строки выполняется в АЭ после построения
всех множеств ситуаций, если последнее множество содержит #E#•; 0>. Дерево легко
восстанавливается по списку существенных ситуаций. На СЛАЙДЕ 16 показано такое
дерево из примера 63. На СЛАЙДЕ 15 более ясным образом представлены зависимости
ситуаций. Горизонтальные указатели связывают одну и ту же продукцию с
последовательно перемещаемой «меткой». Такие ситуации последовательно порождаются
считывателем и завершателем. Вертикальные указатели связывают ситуации,
порожденные предсказателем. Для неоднозначных строк из одного нетерминала может
идти более чем один вертикальный указатель, что демонстрирует возможность
неоднозначного построения дерева разбора.
По схеме на СЛАЙДЕ 16 очевидна справедливость следующей теоремы, которую
мы принимаем без доказательства. СЛАЙД 17.
Теорема 9.1. Ситуация •β; i> находится во множестве Mj тогда и только тогда,
когда существует такое порождение S=>*γAδ, что:
1. γ =>* a1 … ai
2. =>* ai+1 … aj
Еще раз отметим следующие особенности АЭ. Во-первых, он универсальный, т.е.
подходит для всех КСГ.
4
Версия 0.9pre-release от 02.04.2014. Возможны незначительные изменения.
Во-вторых, он выполняет нисходящий анализ и восстанавливает левое порождение.
В-третьих, он строит состояния распознавателя динамически, непосредственно при
обработке конкретной строки языка. Для каждой такой строки множества ситуаций,
строящиеся при ее распознавании, уникальны. Каждой такое множество связано с
конкретным местом в анализируемой строке.
В общем случае, АЭ затрачивает O(n3) шагов при распознавании строки длиной n,
используя емкость памяти O(n2). Если же КСГ однозначна и приведена в НФХ, то
алгоритму понадобится O(n2) шагов. Автор алгоритма утверждал, что для широкого
класса однозначных грамматик время распознавания линейно.
Мы описали бесконтекстную версию алгоритма, однако автор алгоритма
рекомендует добавить в каждую ситуацию терминальную строку длиной k – допустимый
терминальный контекст, который может встретиться в строках языка после того, как
данная продукция была применена. Контекст используется завершателем для принятия
решения о возможности применения закончившейся продукции. Приписанный ей
контекст сравнивается с последующей подстрокой длины k, и продукция применятся
только тогда, когда эти строки совпадают. СЛАЙД 18.
Использование контекста может повысить эффективность распознавателя, т.к.
лишние ветви вычислений могут быть отброшены раньше, и на их обработку время и
память не будут тратиться. С другой стороны, на ведение и расчет контекста тоже
требуются ресурсы. Автора алгоритма не рекомендовал использовать контекст длины
более единицы.
9.4 Алгоритм Кока-Янгера-Касами
Этот алгоритм восстанавливает снизу вверх все возможные порождения сначала для
всех подстрок входной строки длиной 1, затем – длиной 2 и т.д. до тех пор, пока не будет
проанализирована вся строка. Основная идея алгоритма заключается в том, что
результаты СА более коротких подстрок, полученные на предыдущих шагах,
используются для определения тех продукций, которые могут быть использованы для
порождения более длинных строк. КСГ в этом случае должна быть приведена в НФХ, что
позволяет анализировать разбиение любой подстроки входной строки длиной более 1 на
две части, СА каждой из которых уже произведен на предыдущих шагах и запомнен в
таблице. СЛАЙД 19.
Это универсальный алгоритм для всех КСГ, он осуществляет восходящий СА,
затрачивая O(n3) шагов при распознавании строки длиной n, используя емкость памяти
O(n2). Был независимо с небольшими вариациями получен тремя учеными Дж.Коком,
Д.Янгером и Т.Касами, далее мы будем использовать анонсированное выше название –
CYK-алгоритм.
В процессе работы CYK-алгоритм строит треугольную таблицу разбора (таблицу
синтаксического анализа, далее – ТСА) T непосредственно по анализируемой
последовательности символов. В каждую ячейку tik ТСА помещается множество
нетерминалов, из которых можно вывести отрезок выходной строки длиной k, начиная с
ai: tij = {A | A =>* ai … ai+j+1}. Содержимое ячейки ТСА вычисляется очень просто по КСГ в
НФХ.
Действительно, ti1 A A ai P ,
а
ti1 A A BC P & k : 1 k j : B tik & C tik , j k . Входная строка принадлежит
языку, порождаемому грамматикой, если в ячейке t1n есть аксиома. СЛАЙД 20.
ТСА заполняется снизу вверх, причем каждая строка соответствует определенной
длине подстрок: Нижняя – подстрокам длины 1, вторая снизу – длины 2 и так далее вплоть
до верхней строки, соответствующей одной подстроке длины n, т.е. всей анализируемой
строки.
Нижняя строка ТСА T заполняется так: в ti1 помещаются все нетерминалы A, для
которых есть продукция A -> ai. Предположим, уже заполнены все строки таблицы T от 1
5
Версия 0.9pre-release от 02.04.2014. Возможны незначительные изменения.
до j-1 включительно. Рассмотрим ячейку tij. Она соответствует фрагменту ai … ai+j-1. Этот
фрагмент разбивается всеми возможными способами на пары соседних строк и и и и т.д. Каждому варианту разбиения соответствует пара
ячеек ТСА, в которых стоят нетерминалы, из которых могут быть порождены
соответствующие строки. Пусть эта пара ячеек – (t', t"). В рассматриваемую ячейку tij
помещается нетерминал A, если среди продукций есть A -> BC, и нетерминал B входит в
ячейку t', а C – в t''. СЛАЙД 21.
Рассмотрим, к примеру, ячейку t34 из ТСА, представленной на СЛАЙДЕ 22, для
строки из шести символов. В нее должны быть помещены нетерминалы, из которых
выводится фрагмент выходной строки, начинающийся с a3, длина фрагмента – четыре
символа. Очевидно, это последовательность символов a3a4a5a6. Эта подстрока может быть
разбита на пары непустых соседних фрагментов: (1) и ; (2) и ;
(3) и . Этим трем парам соответствуют пары ячеек, где могут стоять
нетерминалы, из которых эти подстроки порождаются. Для пары (1) это t31 и t43; для пары
(2) это t32 и t52; для пары (3) это t33 и t61. Если в первой ячейке пары есть нетерминал B, во
второй – нетерминал C, а среди продукций есть A -> BC, то нетерминал A помещается в
ячейку t34. Это схематично показано на СЛАЙДЕ 22.
Пример 64.
Приведем СА строки ()()() из языка скобок с использованием неоднозначной
грамматики с продукциями S -> SS | LR, L -> (,. R -> ). ТСА представлена на СЛАЙДЕ 23.
В ней нетерминалы, стоящие в ячейке tij, помечены индексом ij.
Второй шаг алгоритма – это восстановление дерева разбора строки. Оно
осуществляется с помощью рекурсивной процедуры REDUCE(i, j, A), которая дает левое
порождение A =>* aiai+1 … aj-1.
Эту процедуру легко построить:
1. Если j = 1 и A -> ai – это продукция с номером m, то выдать m.
2. Пусть j > 1. Выберем k – наименьшее из чисел, для которых существуют B из tik, C
из ti+k,j-k и продукция A -> BC с номером m. Выберем эту продукцию, выдаем m и
выполняем REDUCE(i, k, B) и REDUCE(i+1, j-k, С).
Работа процедуры начинается с REDUCE(1, n, S). СЛАЙД 24.
На СЛАЙДЕ 25 для ячейки t16 показаны два возможных варианта включения
нетерминала S16 в эту ячейку: либо по продукции S16 -> S12S34, либо по продукции S16 ->
S14S34. Оба варианта возможны, поэтому мы можем построить два дерева разбора этой
строки по заданной грамматике.
Литература к лекции 9
1. Контекстно-свободная
грамматика
http://ru.wikipedia.org/wiki/Контекстносвободная_грамматика
2. Серебряков В. А., Галочкин М. П., Гончар Д. Р., Фуругян М. Г. Теория и реализация
языков программирования — М.: МЗ-Пресс, 2006 г., 2-е изд. - http://trpl7.ru/tbooks/TRYAP_BOOK_Details.htm
3. Контекстно-свободные грамматики - http://www.math.spbu.ru/user/mbk/PDF/Ch-4.pdf
4. J. Earley, "An efficient context-free parsing algorithm", Communications of the Association
for Computing Machinery, 13:2:94-102, 1970.
5. Earley parser - http://en.wikipedia.org/wiki/Earley_parser
6. CYK algorithm - http://en.wikipedia.org/wiki/CYK_algorithm
7. John Cocke and Jacob T. Schwartz (1970). Programming languages and their compilers:
Preliminary notes. Technical report, Courant Institute of Mathematical Sciences, New York
University.
8. T. Kasami (1965). An efficient recognition and syntax-analysis algorithm for context-free
languages. Scientific report AFCRL-65-758, Air Force Cambridge Research Lab, Bedford,
6
Версия 0.9pre-release от 02.04.2014. Возможны незначительные изменения.
MA.
9. Daniel H. Younger (1967). Recognition and parsing of context-free languages in time n3.
Information and Control 10(2): 189–208.
7