Синтаксический анализ Формальные языки Алфавит – непустое конечное множество произвольных элементов (символов), задаваемое перечислением. 1 = {a, b, c, …, y, z}, 2 = {0, 1}, 3 = {, , , } Цепочки, операции над цепочками – цепочка (над алфавитом ) – упорядоченная последовательность символов из алфавита : = a1a2…an (ai ) | | – длина цепочки : |a1…an| = n – пустая цепочка: | | = 0. – конкатенация (сцепление) цепочек и : = a1…an, = b1…bm = a1…anb1…bm | | = || + | |, = = Если = , то – префикс , – суффикс . k – k-степень цепочки : 0 = , k+1 = k Множества цепочек и операции над ними AB, AB, A\ B – теоретико-множественные операции над множествами. AB – конкатенация множеств: AB = { | A, B} Ak – k-степень множества: A0 = { }, Ak+1 = AAk A* – замыкание Клини множества (множество всех цепочек над A произвольной длины): A* = {i0}Ai A+ = A*\ A0 Языки и операции над ними цепочек над : L * Операции L1L2, Lk, L*, L+ для языков определяются как для множеств цепочек. Проблема описания языка: задание языка перечислением либо невозможно (бесконечные языки), либо непрактично (большие языки). Требуется компактный набор правил, позволяющих проверять L. Для описания языков применяются формализмы трех типов: – порождающие грамматики (метод исчисления составляющих); – автоматы-распознаватели (метод эффективного вычисления характеристической функции языка); – алгебры множеств (аналитический метод). Формальные грамматики (Формальная) грамматика (порождающая грамматика, грамматика составляющих Н. Хомски): G = < T, N, S, P>, где: T – множество терминальных символов (терминалов); N – множество нетерминальных символов (нетерминалов); S N – стартовый нетерминал; P – множество продукций (правил вывода) вида . Классификация грамматик по Хомски – по виду продукций из P: Грамматики типа 0 (с фразовой структурой общего вида): , где , (TN)*. Грамматики типа 1 (контекстно-зависимые, КЗ-грамматики, КЗГ): , где (TN)*N(TN)*, (TN)*. Грамматики типа 2 (контекстно-свободные, КС-грамматики, КСГ): A , где A N, (TN)*. Грамматики типа 3 (регулярные, РГ): A aB или A a, где A, B N, a T. В практике автоматического синтаксического анализа применяются только классы КСГ и РГ. Основа продукции A – её правая часть . Сокращенная форма записи продукций КСГ: A 1, A 2 , ... , A n A 1 | 2 | ... | n . L – (формальный) язык над алфавитом – произвольное множество 1 Соглашения об обозначениях Язык, порождаемый грамматикой Следующие обозначения используются в дальнейшем для сокращения уточнений о множествах символов или цепочек: Строчные буквы начала латинского алфавита: a, b, c, ... T – одиночные терминалы; Строчные буквы конца латинского алфавита: ... u, v, x, y, z T* – цепочки терминалов; Заглавные буквы начала латинского алфавита: A, B, C, ... N – одиночные нетерминалы; Заглавные буквы конца латинского алфавита: ... X, Y, Z TN – одиночные терминалы или нетерминалы; Строчные греческие буквы (кроме – пустой цепочки): , , … (TN)* – смешанные цепочки терминалов и нетерминалов. Сентенциальная форма грамматики – цепочка, выводимая из S: : S * Предложение – терминальная сентециальная форма: w : S * w Фраза – терминальная цепочка, выводимая из нетерминала: v : A * v Язык, порождаемый грамматикой, – множество выводимых в ней предложений: L(G) = {w | S G* w} Вывод по грамматике Продукция грамматики рассматривается как правило переписывания, при применении которого к некоторой цепочке вхождение левой части заменяется правой (развертка) или вхождение правой части заменяется левой (свертка). Вывод (разверткой) (за 1 шаг, непосредственный) по грамматике G: G = 12, = 12, P. (индекс G опускается, когда ясно, о какой грамматике идет речь) Вывод за k шагов: k , k-1 . ( 0 ) Вывод за произвольное число шагов: * k 0: k Вывод за ненулевое число шагов: + k > 0: k Для КСГ на каждом шаге вывода заменяется один нетерминал. Левосторонний вывод для КСГ: l заменяется самый левый нетерминал в . Правосторонний вывод для КСГ: r заменяется самый правый нетерминал в . k k + l , l*, l , r , r*, r+ определяются аналогично. Деревья синтаксического разбора Деревья разбора – это другой механизм порождения терминальных цепочек по грамматике. Дерево (синтаксического) разбора для грамматики G – это корневое ориентированное упорядоченное помеченное дерево (в смысле теории графов), в котором: – корень помечен S; – узлы помечены нетерминалами из N; – листья помечены или терминалами из T; – сыновья каждого нетерминального узла A образуют упорядоченное множество X1, … , Xn, если A X1 … Xn P . Сечение дерева – цепочка пометок некоторых узлов дерева, удовлетворяющая условиям: 1) каждый путь из корня к некоторому листу содержит один и только один узел, пометка которого входит в сечение; 2) пометки выписаны в порядке прямого левостороннего обхода дерева в глубину. Крона дерева – cечение, содержащее только пометки листьев. Цепочка w выводима деревом в G, если существует дерево разбора с корнем S и кроной w. Теорема о корректной выводимости Выводимость деревом равносильна выводимости разверткой: 1. Для любого вывода S 1 … n w можно построить дерево разбора, в котором 1…n – сечения, а w – крона. 2. Для любого дерева разбора с кроной w существует вывод S * w. 2 Следствия: 1. Каждое сечение дерева разбора – сентенциальная форма. 2. Каждая сентенциальная форма – cечение некоторого дерева разбора. Два различных дерева вывода, иллюстрирующие неоднозначность G: 1: Неоднозначность разбора Грамматика неоднозначна, если для некоторой выводимой цепочки существуют два различных дерева разбора. Неоднозначность синтаксической структуры цепочки часто приводит и к неоднозначности семантических связей между объектами, которые обозначены символами цепочки. Поэтому неоднозначные грамматики непригодны для реализации языков программирования. E /|\ E + E | /|\ a E * E | | a a E /|\ E * E /|\ | E + E a | | a a семантически соответствуют: 1: a+(a*a) 2: (a+a)*a выделено сечение: a+E*E Нисходящий вывод деревом для цепочки a+a*a: E Нисходящий и восходящий способы разбора Нисходящий разбор (деревом) цепочки w – это процесс построения дерева разбора для w, который начинается с корня, и на каждом шаге (развертки) в дерево добавляются все сыновья одного нетерминального узла. Восходящий разбор (деревом) цепочки w – это процесс построения дерева разбора для w, который начинается с листьев (тривиальных деревьев), и на каждом шаге (свертки) в дерево добавляется узел, сыновьями которого являются вершины уже построенных поддеревьев. Эти два способа лежат в основе практических методов синтаксического анализа. 2: E /|\ E + E E /|\ E + E | a E /|\ E + E | /|\ a E * E E /|\ E + E | /|\ a E * E | a E /|\ E + E | /|\ a E * E | | a a Восходящий вывод деревом для цепочки a+a*a: a + a*a E | a + a*a E E | | a + a*a E E E | | | a + a*a E /|\ E E|E | ||| a + a*a E /|\ / / E | | /|\ E | E|E | | ||| a + a*a Примеры КС-грамматика, описывающая язык арифметических выражений над переменными a: G = < T N S P = = = = { a, +, -, *, /, (, ) }, { E, T, F }, E, { E E+E | E–E | E*E | E/E | (E) | a } > Выводы разверткой для цепочки a+a*a: – произвольный: – левосторонний: – правосторонний: E E*E E*a E+E*a a+E*a a+a*a E l E+E l a+E l a+E*E l a+a*E l a+a*a E r E+E r E+E*E r E+E*a r E+a*a r a+a*a 3 Недетерминированный синтаксический анализ для КСГ Недетерминированный нисходящий разбор Нисходящий анализатор с левосторонним выводом разверткой для КСГ нетерминалов в этой цепочке. Такое построение дерева сверху вниз отвечает названию способа разбора: нисходящий. Цепочка уже прочитанных (и сравненных) терминалов и цепочка активных узлов вместе образуют некоторое сечение дерева – сентенциальную форму. Если входная цепочка исчерпывается одновременно со стеком, разбор завершается допуском входной цепочки. В противном случае, а также при невозможности применения какого-либо правила на очередном шаге, автомат не может продолжить работу, что равносильно ошибке разбора. Недетерминизм (конфликты) при нисходящем разборе Пусть G = < T, N, S, P > – КС-грамматика. Нисходящий анализатор с левосторонним выводом для G – следующий МА: ML = < = T, Q = {q0, qF}, Г = NT, 0 = S, F = {qF}, > Функция переходов строится исходя из следующих правил: 1. Начальная конфигурация: (q0, S, ) 2. Правила развертки: A P: (q0, A, w) (q0, , w) 3. Правила сравнения: a T: (q0, a, aw) (q0, , w) 4. Правило допуска: (q0, , ) (qF, , ) Если грамматика содержит более одной продукции для некоторого нетерминала, то правила развертки разрешают недетерминированный выбор между ними при обнаружении этого нетерминала на вершине стека. Этот выбор назовем конфликтом «развертка-развертка». Если грамматика содержит только по одной продукции для каждого нетерминала, то она порождает язык, состоящий из единственной цепочки, который едва ли представляет практический интерес. Поэтому недетерминированность нисходящего разбора неизбежна для любого содержательного языка и является следствием существенного и неустранимого свойства любой его грамматики. Отсюда, предложенный недетерминированный нисходящий анализатор не может работать детерминированно ни для какого содержательного языка. Требуется введение дополнительных механизмов решения конфликтов. Анализатор начинает работу имея в стеке S и на следующем шаге должен заменить S основой некоторой продукции для S. Для этого автомата удобно считать, что стек растет справа налево, т.е. после замены верхним (левым) в стеке окажется первый символ основы. Пока верхние символы стека – терминалы, они поочередно сравниваются с очередными символами входной цепочки и, в случае успеха, удаляются из стека. Как только верхним символом стека вновь оказывается какой-то нетерминал A, он должен быть заменен основой одной из продукций вида A и т.д. На каждом шаге в стеке находятся еще «недоразвернутые» суффиксы основ всех инициированных продукций (в порядке обратном инициации), а развертке подвергается самый первый (левый) нетерминал. Поэтому анализатор реализует левосторонний вывод разверткой. В терминах построения дерева разбора, анализатор начинает построение дерева с корня S, хранит в стеке цепочку «активных» узлов (еще требующих сравнения или развертки) и при каждой развертке достраивает дерево сыновьями самого левого из еще нераскрытых Недетерминированный восходящий разбор Пополненные грамматики Для восходящего разбора будет удобно, чтобы стартовый нетерминал грамматики S не встречался в правых частях продукций. Если грамматика не обладает этим свойством, её можно эквивалентно преобразовать: Пополненная (расширенная) грамматика, полученная из G = < T, N, S, P >: G’ = < T, N{S’ }, S’, P {S’ S} > В дальнейшем все грамматики, используемые при восходящем разборе, считаются пополненными. Восходящий анализатор с правосторонним выводом сверткой для КСГ Пусть G = < T, N, S, P > – КС-грамматика. Восходящий анализатор с правосторонним выводом для G’ – следующий 4 расширенный МА: MR = < = T, Q = {q0, qF}, Г = NT, 0 = , F = {qF}, > Функция переходов строится исходя из правил: 1. Начальная конфигурация: (q0, , ) 2. Правила переноса: a T: (q0, , aw) (q0, a, w) 3. Правила cвертки: A P: (q0, , w) (q0, A, w) 4. Правило допуска: (q0, S’, ) (qF, , ) Пусть в грамматике есть продукции вида A 1, B 12, C 2 и пусть 2 r* w. Конфликт «перенос-свертка» (shift-reduce conflict, SR-конфликт) – это состояние анализатора, в котором стековая цепочка оканчивается основой некоторой продукции и следующим шагом могут быть как свертка по этой продукции, так и продолжение переноса: (q0, A, w…) * (q0, A2, …) (q0, AC, …) свертка A 1 (q0, 1, w…) перенос * (q0, 12, …) (q0, B, …) { Конфликт «cвертка-свертка» (reduce-reduce conflict, RR-конфликт) – это состояние анализатора, в котором возможна свертка по двум продукциям: свертка B 12 (q0, …B, …) (q0, …12, …) свертка C 2 (q0, …1C, …) { RR-конфликты не неизбежны: они не возникают, если в грамматике нет двух таких продукций, что одна основа является суффисом другой. SR-конфликты кажутся неизбежными, поскольку перенос возможен в любом состоянии, в том числе и при допустимой свертке. Однако если в грамматике нет пары продукций, где одна основа является частью другой, то SR-конфликт можно решить приоритетом свертки. Итак, оба типа конфликтов теоретически возможно решить модификацией грамматики, а значит, недетерминированный анализатор вполне может работать детерминированно на некоторых грамматиках. Сравнение схем восходящего и нисходящего разборов Следующий пример иллюстрирует последовательность шагов левостороннего и правостороннего выводов и состояния стека при нисходящем и восходящем разборах. Нисходящий разбор с левосторонним выводом: Грамматика: шаг: 1) S SaSb развертка 1 2) S развертка 2 сравнение развертка 1 вывод стек S a a a a S S S S b b b ввод a a a a a a a b b b b левосторонн ий вывод Анализатор начинает работу с пустым стеком. Один из режимов его работы – простой перенос символов из входной цепочки в стек. Для этого автомата удобно считать, что стек растет слева направо, т.е. последний из перенесенных символов оказывается верхним (правым) в стеке. Как только суффиксом стековой цепочки становится основа некоторой продукции A, возможно произвести свертку: замену на A. Переносы и свертки чередуются в порядке, необходимом для вывода входной цепочки. На каждом шаге в стеке находятся еще «недосвернутые» префиксы основ всех инициированных продукций (в порядке инициации), а свертка выполняется на правом крае стековой цепочки. Поэтому анализатор реализует правосторонний вывод сверткой. Так как грамматика пополненная, продукции вида S’ применимы только на последнем шаге вывода, поэтому появление S’ в стеке анализатора равносильно допуску входной цепочки. В терминах построения дерева, анализатор начинает построение дерева с листьев (терминалов входной цепочки), хранит в стеке цепочку «временных» корней, еще требующих свертки, при переносах пополняет эту цепочку новыми одиночными листьями из входной цепочки, а при свертках связывает несколько самых правых временных корней новым общим корнем. Такое построение дерева снизу вверх отвечает названию способу разбора: восходящий. Цепочка уже построенных корней и цепочка несчитанных листьев вместе образуют некоторое сечение итогового дерева разбора – сентенциальную форму. Когда входная цепочка исчерпывается, на стеке должен остаться один корень S. В этом случае разбор завершается допуском. Другое состояние стека считается ошибкой. Заметим, что до исчерпания входа у этого автомата никогда не возникает проблем с очередным шагом, потому что всегда применимо хотя бы правило переноса. Недетерминизм (конфликты) при восходящем разборе b b b b 5 развертка 2 cравнение развертка 1 cравнение cравнение допуск a a a a a a S a a a a a a S S S b b b b b b b b b b b a a b b b b b b b b b b сентенциал.форма остат. ввод Узлы дерева разбора соответствуют прямоугольникам в стеке. Сыновья каждого раскрываемого нетерминала находятся в стеке под ним и левее. Восходящий разбор с правосторонним выводом: Грамматика: шаг: ввод S S S S S S S S S a a a a a a a a a S S S S S S a a a S S b a a a a b b b b b b b b b b b b b b b правосторонний вывод 1) S SaSb cвертка 2 2) S перенос (1) свертка 2 перенос свертка 2 перенос свертка 1 перенос свертка 1 допуск стек основа ост.ввод актив.префикс ввод сентенц.форма (1) Данное состояние не является допускающим, так как ввод не пуст. Узлы дерева разбора соответствуют прямоугольникам в стеке. Сыновья каждого сворачиваемого нетерминала находятся в стеке над ним и правее. Нисходящий синтаксический разбор без возвратов Метод рекурсивного спуска Анализ методом рекурсивного спуска (resursive-descent parsing) – разновидность нисходящего синтаксического анализа, при котором для обработки входной цепочки выполняется ряд рекурсивных процедур. Каждому нетерминалу грамматики сопоставляется отдельная процедура, которая реализует разбор для всех продукций грамматики, имеющих этот нетерминал в левой части. Выбор между продукциями осуществляется по k очередным входным символам – k-предиктору. Требуемая однозначность выбора накладывает дополнительные ограничения на вид грамматики. Построение рекурсивного анализатора для регулярной грамматики Пусть задана детерминированная РГ. Для каждого нетерминала A выпишем серию всех продукций: A a1B1 | a2B2 | … | akBk | b1 | b2 | … | bm Детерминированность РГ означает, что внутри серии все терминалы ai и bj различны. Теперь для каждого нетерминала построим по следующему шаблону процедуру разбора: procedure ParseA; -- A a1B1|a2B2|...|akBk|b1|b2|...|bm begin select Scanner() of {a1}: ParseB1; {a2}: ParseB2; ... {ak}: ParseBk; {b1,b2,...,bm}: -- нет действий else Error ("Ошибка разбора A"); end select; end; Альтернативы с одинаковыми Bi можно объединить, слив их направляющие множества {ai}. Корректность выбора обеспечивается непересечением направляющих множеств. Функция Scanner сканирует очередную лексему во входном потоке и возвращает её токен. Процедура Error выдает ошибку и завершает 6 исполнение программы. Эти действия должны программироваться отдельно. Построение процедур разбора для РГ по указанной схеме может быть автоматизировано. Поэтому возможно создание универсального генератора синтаксических анализаторов для класса регулярных языков. Работа рекурсивного РГ-анализатора Вызов процедуры ParseS, соответствующей начальному нетерминалу грамматики, запускает нисходящий анализ методом рекурсивного спуска. Каждая процедура считывает очередной токен и либо выбирает подходящую продукцию и рекурсивно вызывает процедуру разбора для хвостового нетерминала, либо выдает ошибку, если подходящей продукции нет, Динамическое дерево вызовов процедур ParseX в процессе разбора входной строки топологически совпадает с бинарным деревом разбора этой строки по РГ, разрастающемуся вглубь по правым ветвям. Входная строка читается в процессе анализа строго слева направо, без заглядывания вперед и без возвратов. Таким образом, время работы анализатора линейно по длине входной цепочки. Построение рекурсивного анализатора для КС-грамматики КСГ имеет более общий вид продукций. Анализ методом рекурсивного спуска применим к ней с рядом ограничений. Для каждого нетерминала A заданной КСГ выпишем серию всех продукций: A 1 | 2 | … | n , где i = Xi1 Xi2 … Ximi, и построим процедуры, похожие на процедуры РГ-анализатора: procedure ParseA; -- A 1|2|...|n begin select LookAhead(k) of Firstk(A1): Match(X11); Match(X12); ... ParseX1m1; Firstk(A2): ... Firstk(An): default: end select; end; ParseX21; Match(X22); ... ParseX2m1; ParseXn1; ParseXn2; ... Match(Xnmn); Error ("Ошибка разбора A"); Каждая альтернатива выбора, как и прежде, отвечает за разбор по одной из продукций A i . Её составляют вызовы процедур, последовательно распознающие символы Xij из правой части продукции. Если Xi – нетерминал, вызывается ParseXij; если Xij – терминал, вызывается cледующая процедура Match(Xij): procedure Match (t: token); begin if t Scanner() then Error ("Ожидается символ ", t); end if; end; Для продукции A с пустой основой последовательность действий пуста. LookAhead – это функция, аналогичная Scanner, которая просматривает цепочку-предиктор, состоящую из k очередных входных лексем, но не сдвигает позицию ввода. Её можно реализовать с помощью буферизации cканера. Чтобы выбор альтернатив работал корректно, направляющие множества (здесь они обозначены Firstk (Ai) и состоят из цепочек длины не больше k), как и в РГ-анализаторе, не должны попарно пересекаться. Однако для КСГ общего вида это вызывает ряд трудностей: – для КСГ не запрещено, чтобы несколько i начинались с одного и того же терминала или даже с одинаковой цепочки терминалов; – в КСГ первым символом цепочки i может быть нетерминал. Применение данного метода требует разрешения этих проблем каким-либо способом. Далее мы рассмотрим подкласс КСГ: LL(k)грамматики, для продукций которых направляющие множества Firstk (Ai) различаются и эффективно строятся. Построение направляющих множеств и процедур разбора указанного вида для LL(k)-грамматик может быть автоматизировано. Поэтому для класса LL(k)-языков также возможно создание универсального генератора синтаксических анализаторов. Особенность выбора по пустой цепочке Некоторое (и, по условию непересечения, не более, чем одно) направляющее множество может содержать пустую цепочку . Сравнение предиктора с пустой цепочкой считается успешным лишь в том случае, если он не совпадает ни с одной непустой цепочкой ни в одном направляющем множестве. По логике алгоритма в этом случае выбирается альтернатива отказа (default). Если не является допустимым значением предиктора, то отказ при сравнении с допустимыми непустыми цепочками равносилен неуспеху 7 разбора: следует диагностировать ошибку и прервать разбор. Именно это предусмотрено в написанной выше схеме генерации. Однако если входит в некоторое направляющее множество, то отказ при сравнении не является ошибкой. В этом случае действия по разбору соответствующей продукции следует вписать в альтернативу отказа, а само множество из выбора исключить (его варианты при этом тоже попадут в отказные): default: ParseXi1; ... ParseXimi; -- если Firstk(Ai) Работа рекурсивного КСГ-анализатора Запуск КСГ-анализатора производится вызовом процедуры ParseS, соответствующей начальному нетерминалу. Она (и также все остальные процедуры), сначала заглядывает вперед на k очередных лексем и на их основании выбирает продукцию или выдает ошибку, если подходящей продукции нет. Затем последовательно подтверждаются элементы правой части выбранной продукции, для чего вызываются рекурсивные процедуры ParseX для нетерминалов и процедура сравнения Match для терминалов. Входная строка читается в процессе разбора слева направо, с заглядыванием вперед на k лексем. Буферизация сканера позволяет реализовать заглядывание без возвратов по входной строке. Таким образом, время работы анализатора линейно по длине входной цепочки. Динамическое дерево вызовов процедур рекурсивного анализатора в процессе разбора топологически совпадает с бинарным деревом разбора по КСГ. Анализатор осуществляет левосторонний вывод входной цепочки: т.е., дерево вывода разрастается от корня вглубь путем развертки самого первого нетерминала в порядке левостороннего префиксного обхода дерева в глубину. Сравнение схем КСГ- и РГ-анализаторов Если применить схему построения КС-анализатора к РГ (как к частному случаю КСГ), то полученная программа будет немного отличаться от полученной по схеме построения РГ-анализатора. Для РГ направляющее множество продукции состоит из первого терминала в его правой части. В КСГ-схеме, первым действием всегда будет вызов Match от направляющего символа, нужный для фактического считывания kпредиктора. Это считывание можно убрать, заменив LookAhead на вызов сканера, что и сделано в РГ-схеме. Дополнение рекурсивного КСГ-анализатора семантическими действиями Построенную схему синтаксического анализа можно дополнить семантической нагрузкой: произвольными действиями, выполняемыми по ходу разбора. Порядок этих действий зависит от места размещения: так, действия, вписанные до самого первого вызова Match или ParseX в процедуре, будут выполняться в префиксном (прямом древесном) порядке, после самого последнего их вызова – в постфиксном (обратном древесном) порядке, а между ними – в одной из разновидностей инфиксного (внутреннего древесного) порядка. В качестве примеров семантической нагрузки анализатора арифметического выражения рассмотрим четыре задачи и общие планы их решений. 1. Вычисление арифметического выражения Преобразовать каждую процедуру разбора в функцию, возвращающую результатом значение подвыражения, соответствующего данному узлу дерева вывода. Лексемы-литералы и лексемыидентификаторы порождают исходные значения (соответствующие листьям дерева вычислений). Операции в узлах дерева порождают промежуточные значения. Окончательное значение выдается результатом заглавной функции ParseS. Вычисления производятся в постфиксном порядке. 2. Построение явного представления структуры выражения Описать структуру объекта данных для представления узла вычислений: литерального значения, идентификатора или операции, аргументы которой заданы ссылками на другие узлы. Преобразовать каждую процедуру разбора в функцию, возвращающую ссылку на динамический объект. Вписать в каждую процедуру действия по созданию объекта и заполнению его полей (создание узлов для объектов, порождаемых литералами, удобнее внести в процедуру Match). Заполнение полей объекта кодом операции и ссылками на аргументы в структуре узла выполнять по мере поступления лексем и данных от рекурсивных вызовов. Ссылка на построенное дерево вычислений выдается результатом заглавной функции ParseS. Создание объектов производится в префиксном порядке, заполнение – в инфиксном и постфиксном. 8 3. Контроль и преобразование типов в выражении Для каждого объекта данных в выражении определить тип данных (1-, 2-, 4-байтовое знаковое или беззнаковое целое). Составить иерархию типов по включению. Ввести дополнительный атрибут в структуру объекта представления (задача 2): тип значения узла. Для терминальных объектов: тип идентификатора считается заданным в таблице имен, тип литерала определяется как минимальный тип, включающий заданное значение. Для операции тип результата определяется как минимальный тип, включающий типы аргументов. Если тип аргумента(-ов) не совпадает с типом результата, перед выполнением операции следует произвести преобразование аргумента, которое может быть рассмотрено как неявная унарная операция toType(x). Эти операции необходимо в явном виде встроить в дерево вычислений (для задачи 2) или включить в поток выходных команд (для задачи 4). 4. Генерация программы вычисления выражения для стековой машины (Данная задача равносильна задаче перевода выражения в постфиксную (обратную польскую) запись.) Вписать в каждую процедуру разбора вывод в выходной поток (файл) некоторой последовательности команд виртуальной стековой машины. Для литералов и идентификаторов (в процедуре Match) выводится одноадресная команда записи значения в стек push(x). Для операции выводится безадресная команда (add, sub, div, mul), снимающая нужное число аргументов со стека и помещающая реезультат в стек. Генерация команд производится в постфиксном порядке. Развитие задачи: реализовать эту схему "стековых вычислений" командами реальной двухадресной регистровой машины (типа i86). Для каждой арифметической команды вида op(r1,r2), вычисляющей r := r1 op r2, необходимо явно снимать аргументы со стека и принимать их в некоторые регистры: pop(r1), pop(r2), а также явно записывать результат операции в стек из регистра-результата: push(r1). (Здесь в первом приближении возникает задача распределения регистров, а полученный выходной код может быть поучительной иллюстрацией к задаче оптимизации вычислений.) LL(k)-грамматики Название этого класса грамматик обозначает тип синтаксического разбора, для которого они предназначены: L – левосторонний ввод входной строки, L – левосторонний вывод разверткой, k – c распознаванием продукций по k-предиктору. Определение направляющих множеств Пусть (TN)*. Firstk () – множество k-префиксов всех терминальных цепочек, выводимых из : Firstk () = {x T* | ( * x, |x|=k) ( * x , |x| < k) } Followk () – множество k-префиксов всех терминальных цепочек, выводимых после : Followk () = {x T* | S * w, x Firstk ( ) } Firstk (A) – множество k-префиксов всех терминальных цепочек, выводимых из нетерминала A продукцией A : Firstk (A) = Firstk ( Followk (A)) Определение и свойства LL(k)-грамматик Грамматика G – (нестрогая) LL(k)-грамматика: G LL(k), если из условий: следует: где: 1. S l* wA l w l* wy w, y, z T* 2. S l* wA l w l * wz = AN 3. Firstk (y) = Firstk (z) , , (NT)* Грамматика G – строгая LL(k)-грамматика: G SLL(k), если выполнено усиленное условие 2: 2+. S l* vA l v l* vz v T*; , (NT)* Следствия определений, удобные для проверки LL(k)-свойства: G LL(k) Firstk (i) Firstk (j) = Ai, Aj P, i j; : S * wA G SLL(k) Firstk (Ai) Firstk (Aj) = Ai, Aj P, i j Cмысл LL(k)-свойства: если Ai и Aj – две различные продукции, то множества k-префиксов терминальных цепочек, выводимых с их помощью (и на последующих шагах), не пересекаются. 9 Значит, если k-префикс непрочитанной части входа известен, выводящая продукция для A определяется однозначно. В нестрогой LL(k)-грамматике основанием для выбора продукции являются уже прочитанная часть входной строки w и множество kпрефиксов непрочитанной части, а в строгой – только последнее. LL(0)-грамматики существуют, но не применяются: при k = 0 First0 () = { }, и поэтому для каждого нетерминала в грамматике разрешена только одна продукция. Такие грамматики описывают языки, состоящие из единственной цепочки. Свойства LL(k)-грамматик: 1. LL(k)-грамматика однозначна. 2. Леворекурсивная грамматика не является LL(k) ни для какого k. Соотношения классов LL(k)-грамматик: 1. k1: LL(k) LL(k+1) , SLL(k) SLL(k+1) . 2. k>1: LL(k) SLL(k) . 3. LL(1) = SLL(1) . Последнее свойство делает особенно привлекательным класс LL(1): множества Firstk (Ai) для SLL(k) вычисляются легче, чем для LL(k), а при k=1 они совпадают. Реализация нисходящего LL(1)-анализатора упрощается также тем, что 1-предиктор – это одиночная лексема, а не цепочка. При невозможности построения LL(1)-грамматики для языка, можно пытаться найти решение в классах LL(2) и выше, т.е. различать продукции по двум очередным лексемам и т.д. Алгоритм вычисления Follow1 (A): 1. Инициализация: X N: Follow1(X ) = . Follow1(S) = {} ( – символ конца ввода) 2. X N применять следующие правила, пока какое-либо Follow1(Y ) изменяется: а) Если A B P First1() \ { } Follow1(B) б) Если A B P или A B и First1() Follow1(A) Follow1(B) Пример. Для грамматики: E E' T T' F TE' First1(E) = First1(T) = First1(F) = {(,id} +TE'| First1(E') = {+,} FT' First1(T') = {*,} *FT'| Follow1(E) = Follow1(E') = {),} (E) |id Follow1(T) = Follow1(T') = {+,),} Follow1(F) = {*,+,),} First1(E+TE') = {+} First1(E) = First1( Follow1(E)) = {),} First1(T*FT') = {*} First1(T) = First1( Follow1(F)) = {*,+,),} First1(F(E)) = {(} First1(Fid) = {id} Направляющие множества для каждого из нетерминалов E, T, F не пересекаются, значит это LL(1)-грамматика. Приемы приведения грамматики к классу LL(k) Построение направляющих множеств для LL(1)-грамматики Устранение левой рекурсии Алгоритм вычисления First1 (A): Пусть A A1 | ... | An | 1 | ... | k – все продукции для A и ни одно i не начинается с A. Тогда замена этих продукций следующими: A 1B | ... | kB B | 1B | ... | nB где B – новый нетерминал, устраняет левую рекурсию нетерминала A и не изменяет множества порождаемых цепочек: (1 | ... | k)(1| ... |n)*. Однако это преобразование меняет топологию дерева вывода цепочки с леворекурсивной на праворекурсивную. Семантическое различие видно, например, для дерева разбора арифметического выражения a–b– c+d: порядок ассоциации операций меняется с правильного ((a–b)– c)+d на неправильный a–(b–(c+d). 1. Инициализация: X T: First1(X) = {X }. X N: First1(X) = . 2. X N применять следующие правила, пока First1(X ) изменяется: а) Если X P { }First1(X ) б) Если X Y1Y2…Yk P: – если j < i First1(Yj) и a First1(Yi) {a}First1(X) – если j .k First1(Yj) { }First1(X) 10 Факторизация Пока в грамматике существуют две или более продукций, имеющих общий префикс правых частей, выполнять следующее преобразование факторизации: A 1 | 2 A B B 1 | 2 Факторизация позволяет отложить распознавание ветвления на более позднее время, когда, возможно, удастся различить направляющие множества по предиктору меньшей длины. Свойства: однозначная, с разделением операций по группам приоритетов, нелеворекурсивная, LL(1), но правоассоциативная в каждой группе. Эта грамматика допускает разбор методом рекурсивного спуска, но при построении дерева вычислений необходима перегруппировка левоассоциативных операций. В строго рекурсивной схеме, изложенной выше, это может быть с некоторыми усилиями достигнуто только семантическими действиями, изменяющими информационные связи в дереве, индуцированные рекурсией. Однако возможно расширение рекурсивной схемы разбора, решающее эту проблему. Грамматики для арифметического выражения Усовершенствованный метод рекурсивного спуска При рассмотрении способов разбора арифметического выражения ограничимся для краткости выражениями, состоящими из идентификаторов, бинарных операций двух уровней приоритета (высший – умножение и деление, низший – сложение и вычитание) и выражений в скобках. Добавление других видов аргументов, операций и уровней приоритета в дальнейшем возможно по аналогии. Появление нетерминалов E’ и T’ и праворекурсивных продукций в грамматике G3 в результате избавления от левой рекурсии – явление несколько искусственное: эти нетерминалы не имеют значимого смысла в семантике выражений, а служат лишь для реализации некоторой техники разбора в рамках строго рекурсивной нотации грамматических правил. Но мы можем расширить эту нотацию в интересах дела! Рассмотрим стандартную схему замены хвостовой рекурсии циклом, используя расширенную нотацию регулярных выражений, допускающую повторители в определениях: "Наивная" грамматика G1: E E+E | E-E | E*E | E/E | (E) | id Свойства: леворекурсивная, неоднозначная (допускает различный порядок ассоциации операций, в том числе с нарушением приоритетов), т.е. не LL(k) по обоим признакам. "Однозначная" грамматика G2: E E+T | E-T | T T T*F | T/F | F F (E) | id Свойства: однозначная, с разделением операций по группам приоритетов, левоассоциативная в каждой группе, но леворекурсивная, т.е. не LL(k). LL(1)-грамматика G3: E E' T T' F TE' +TE' | -TE' | FT' *FT' | /FT' | (E) | id A’ | 1A’ | 2A’ | … | nA’ где Firstk (i) A {1 | 2 | … | n}* С её помощью избавимся от искусственных нетерминалов E’ и T’ и хвостовой рекурсии в G3: "Псевдорегулярная" грамматика G4: E T {+T | -T}* T F {*F | /F}* F (E) | id В дереве разбора выражения по этой грамматике последовательности операций одного приоритета находятся на одном уровне рекурсии и никак не ассоциированы, в отличие от лево- и правоассоциативных каскадов, который они образовывали в G2 и G3. Для реализации этого подхода расширим схему генерации процедур разбора в методе рекурсивного спуска, добавив в неё циклы: procedure ParseA; -- A 0 {1|2|…|n}* begin действия по разбору 0 do while LookAhead(1) in Firstk(Ai)\{} 11 select LookAhead(k) of Firstk(A1): действия по разбору 1 Firstk(A2): действия по разбору 2 ... Firstk(An): действия по разбору n end select; end do; end; Пример. Процедура разбора нетерминала E с семантической нагрузкой (выделена) для вычисления выражения. Левая ассоциативность реализуется семантическими средствами. procedure ParseE(): int; -- E T {{+|-} T}* var res: int; begin res := ParseT(); do while LookAhead(1) in {'+','-'} -- First1(E')\{} select LookAhead(1) of '+': res := res + ParseT(); -- First1(E'+TE') '-': res := res – ParseT(); -- First1(E'-TE') end select; end do; return res; end; Восходящий синтаксический разбор без возвратов Методы автоматной реализации К восходящему синтаксическому разбору неприменимы рекурсивные методы, нисходящие по своей сути. Обычно его реализуют с помощью автомата типа «перенос-свертка», за основу которого берется рассмотренный выше недетерминированный восходящий анализатор MR, который детерминизируется некоторым механизмом решения RR- и SRконфликтов, настраиваемым по заданной грамматике. Устранение недетерминированности, как и в случае нисходящего разбора, накладывает ограничения на свойства грамматики. Мы рассмотрим два наиболее изученных класса грамматик, пригодных для восходящего разбора: 1. Грамматики предшествования: на множестве символов грамматики вводятся специальные отношения предшествования, которые позволяют детерминированно находить в стеке границы сворачиваемой основы. Однако класс грамматик предшествования сравнительно невелик. 2. LR(k)-грамматики: конфликты решаются просмотром непрочитанной части входной цепочки и анализом k-предикторов. Этот подход аналогичен LL(k)-подходу для нисходящего разбора. Но LL(k)анализатор выбирает следующий шаг разбора, зная только прочитанную часть цепочки и k-префикс самой основы, а LR(k)анализатор – зная прочитанную часть цепочки, всю основу и еще k символов после неё. Поэтому возможности LR(k)-анализа шире, а класс LR(k)-грамматик описательно мощнее класса LL(k)-грамматик. В действительности, он с избытком покрывает потребности анализа современных языков программирования. Грамматики предшествования В курсе для ФИТ не изучаются. 12 LR(k)-грамматики (Материал в работе) LR(k)-метод LR(k)-метод восходящего разбора (L – левосторонний ввод, R – правосторонний вывод сверткой, k – c k-предиктором) основан на том же принципе, что и LL(k)-метод нисходящего разбора: знание k-предиктора должно однозначно определять следующий шаг анализатора. Теоретические основы метода: – LR(k)-грамматики: класс грамматик, свойства которых исключают неоднозначность вывода на любом шаге LR(k)-разбора; в LR(k) выделяются также более простые подклассы SLR(k) и LALR(k). – система активных префиксов грамматики: описывает состояния стека LR(k)-анализатора в процессе разбора и структуру переходов между ними; – система допустимых LR(k)-ситуаций: описывает состояния вывода входной цепочки по грамматике и соотносит их с состояниями стека LR(k)-анализатора; – критерии проверки LR(k)-свойства грамматики, эквивалентные её определению: позволяют проверить внутреннюю непротиворечивость каждого из множеств LR(k)-ситуаций, определить по ним подкласс, которому принадлежит грамматика, и построить для нее подходящий анализатор. Для реализации метода строятся: – множество состояний LR(k)-разбора и функция перехода между ними – по сути, конечный автомат: его структура индуцируется структурой системы допустимых LR(k)-ситуаций; – функция действия: для каждого состояния разбора по множеству допустимых в нем LR(k)-ситуаций однозначно определяет, какое действие: свертку, перенос, допуск или ошибку, – должен выполнить LR(k)-анализатор. – LR(k)-анализатор: детерминированный магазинный автоматпреобразователь, переключением состояний которого управляет функция переходов КА, а выводом цепочки – функция действия. Далее все эти аспекты построения универсального LR(k)-анализатора описываются подробнее. Схема универсального LR(k)-анализатора LR(k)-анализатор – это магазинный автомат типа «перенос-свертка», управляемый вспомогательными функциями перехода и действия. Пусть G = < T, N, S, P > – LR(k)-грамматика: Множество состояний LR(k)-разбора: V={V0 , …,Vn} Функция переходов: goto: V (NT) V Функция действия: do: V Tk {перенос, допуск, ошибка} {cвертка p | p P} (Функции goto и do также называются управляющими таблицами LR(k)-анализатора. Они, а также множество V, строятся по грамматике G согласно её подклассу.) Множество V с функцией перехода goto можно рассматривать как вспомогательный конечный автомат, управляющий переключением состояний LR(k)-разбора: K = < K = NT, QK = V, qK0=V0, K = goto > LR(k)-анализатор для G – следующий (расширенный) МА: MLR(V, goto, do) = < M = T, QM = {q0, qF, qe}, ГM = (NT{$})V, 0 = [$,V0], FM = {qF}, M >, где функция переходов M задана на конфигурациях: 1. Начальная конфигурация: (q0, [$,V0], ) ; 2. Правила переноса: если do(V, u) = перенос, u = av: (q0, …[c,V], av…) (q0, …[c,V][a, goto(V, a)], v…) ; 3. Правила cвертки: если do(Vn, u) = свертка A X1…Xn: (q0, …[c,V][X1,V1] …[Xn,Vn], u…) (q0, …[c,V][A, goto(V, A)], u…); 4. Правило допуска: если do(V, ) = допуск: (q0, …[c,V], …) (qF, , ) – допуск; 5. Правила ошибки: если do(V, u) = ошибка: (q0, …[c,V], u…) (qe, …, u…) – ошибка. Сравним это определение с определением недетерминированного анализатора MR. Элементы стека расширены до пар, где вторыми элементами являются состояния из V: таким образом, в стеке сохраняется история состояний разбора (обратите внимание, что это внутренние состояния 13 автомата K, а не автомата MLR!). Последнее (в стеке) из этих состояний управляет функциями действия и перехода. Функция перехода goto по последнему состоянию V и новому символу в стеке определяет новое состояние разбора, которое также сохраняется в стеке. Функция действия do по последнему состоянию V и k-предиктору u однозначно определяет следующий шаг анализатора. Именно в её определении заложено решение конфликтов. Поэтому автомат MLR – детерминированный. Описываемый им алгоритм является универсальным для всего класса LR(k) c точностью до V, goto и do, которые зависят от конкретной грамматики. Переходим к их построению. Пусть S r* Aw r w. Активный префикс грамматики G – это любой префикс цепочки . Это определение исключает из содержания понятия стековые цепочки, которые не ведут к свертке (они могут появляться в стеке, например, при разборе недопустимой входной цепочки). На множестве активных префиксов введем частичный порядок по отношению включения: Задача описания состояний LR(k)-разбора Для реализации анализатора надо подходящим образом определить конечное множество возможных состояний разбора и функцию перехода между ними. Каждое состояние в соответствующий ему момент времени должно так или иначе отражать сумму знаний о текущей ситуации разбора, достаточную для выбора следующего шага анализатора (перехода в следующее состояние). Построение модели состояний для LR(k)-анализатора требует привлечения вспомогательного понятийного аппарата. Cистему < , > можно изобразить в виде орграфа, узлы которого помечены префиксами из , и если (1, X, 2), то из узла 1 в узел 2 ведет дуга, помеченная символом X. Этот граф является корневым деревом c корнем и ветвями неограниченной длины (т.к. длины входной цепочки и активных префиксов неограниченны). Будучи потенциально бесконечной, система активных префиксов сама не может быть принята за систему состояний LR(k)-анализатора, но она может быть преобразована в нее путем факторизации по отношению неразличимости активных префиксов при разборе. Система активных префиксов грамматики Допустимые LR(k)-ситуации Вспомним, что восходящий анализатор MR выполняет (в недетерминированном порядке) шаги переноса очередного входного символа в стек и свертки в нетерминал суффикса стековой цепочки, образующей основу некоторой продукции. Состояние анализатора MR (пока еще не LR(k)!) на каждом шаге полностью определяется текущим содержимым стека и непрочитанной частью входной цепочки (другой информации у него нет): ( || v) – cостояние восходящего анализатора, где: – активный префикс: считанная и частично свернутая часть входной цепочки, находящаяся в стеке; v – остаток ввода: еще не считанная часть входной цепочки. Цепочка v является правильной сентенциальной формой: S * v. Активный префикс – это «история» правостороннего разбора. Строгое определение: Следующее понятие формализует представление о допустимых состояниях вывода по грамматике: LR(k)-ситуация для грамматики G и k 0: [ A 1 2 | u ], где A 12 P, u Tk. < , > – система активных префиксов G, где: – множество активных префиксов G; – отношение «2 является расширением 1 посредством X»: (1, X, 2) 2 = 1X, X TN LR(k)-ситуация указывает, какая часть продукции (до разделителя ) уже распознана анализатором, а какую еще ожидается распознать. Допустимость LR(k)-ситуации в некоторый момент разбора означает, что: – анализатор находится в состоянии (…1 || wu…), т.е. начало основы 1 уже сформировано в стеке; – ожидается, что последующими шагами переносов и сверток входная подцепочка w будет преобразована в окончание основы 2 в стеке, что соответствует переходу анализатора в состояние (…12 || u…); при этом k-префиксом остатка ввода будет u; 14 после этого станет возможна свертка основы в стеке в A – т.е. переход в состояние (…A || u…). Все LR(0)-ситуации имеют вид [A 1 2 | ], т.е. информацией о символах, следующих за основой, LR(0)-анализатор не располагает. Множество всех LR(k)-ситуаций можно получить комбинируя конечное множество возможных расстановок разделителя по всем основам продукций из P с конечным числом всех терминальных kпрефиксов u. Это множество конечно. Однако, не все LR(k)-ситуации встречаются при разборе. Выделим только те, которые допустимы в возможных состояниях анализатора: Допустимая LR(k)-ситуация для активного префикса: [ A 1 2 | u ] допустима для = 1 S r* Aw r 12 w, u Firstk (w) Vk ( ) отвечает за начало разбора: в стеке пустой префикс – корень системы < , >. Инициируется единственная продукция для стартового нетерминала пополненной грамматики и строится замыкание. 2. Повторять, пока образуются новые непустые множества: X: Vk () Vk (X) = goto (Vk (), X ): – если [A X | u ] Vk (), то: [A X | u] Vk (X ) ; – построить closure (Vk (X )). Обход системы < , > в порядке расширения префиксов: для всех , для которых Vk () уже построены ранее, отрабатываются все возможные переходы X. Из Vk () в Vk (X ) переносятся только ситуации, ожидающие X (X ); при этом разделитель переносится через X ( X). Cтроится замыкание нового множества. Теперь каждому активному префиксу из можно сопоставить множество допустимых для него ситуаций: Каноническая система множеств допустимых LR(k)-ситуаций для G: k = {Vk () | Vk () – мн-во допустимых LR(k)-ситуаций для акт.префикса } 1. closure (Vk ()): цикл итераций до окончания изменений; на каждой итерации просматриваются все ситуации в множестве; 2. Построение системы k как графа с вершинами Vk и дугами goto: алгоритм «бегущей волны» с пометкой обработанных множеств и рабочим списком для необработанных; обработка множества Vk () заключается в построении Vk (X) = goto (Vk (), X) для всех X; каждое новое множество добавляется в рабочий список, если оно не cовпадает ни с одним из уже построенных. – Каждое из множеств Vk () содержит ситуации, возможные при разборе, когда в стеке анализатора лежит активный префикс . Хотя множество активных префиксов может быть бесконечным, множество k конечно, поскольку оно состоит из подмножеств конечного множества. Это позволяет эффективно строить каждое Vk () и k в целом. Построение канонической системы множеств LR(k)-ситуаций Метод построения: Процедура closure (Vk ()): – повторять, пока образуются новые ситуации: если B P и [A B | u ] Vk (), то: x Firstk ( u): [B | x ] Vk (). Эта процедура вычисляет «замыкание» множества ситуаций: для всех ситуаций, в которых ожидается вывод нетерминала (B), инициируются все продукции для этого нетерминала (B). Повторения нужны, т.к. новые ситуации могут содержать новые ожидания. 1. Построение Vk ( ): – [S’ S | ] Vk ( ) ; – построить closure (Vk ( )). Способ реализации: Анализ сходимости: Алгоритм заканчивает работу за конечное время, поскольку: 1. Вызов closure (Vk ()) добавляет не более |Vk ()||P| новых ситуаций. На каждой итерации цикла добавляется не менее одной ситуации; 2. Каждое из множеств Vk () появляется в рабочем списке не более одного раза; число этих множеств конечно. Управляющий КА: LR(k)-состояния и функция перехода Именно система множества принимаются за состояния управляющего конечного автомата LR(k)-анализатора. Функция переходов этого КА – это функция goto(Vk(), X) из алгоритма построения k. Классы активных префиксов, неразличимых по выводу Неразличимость по выводу для активных префиксов: 1 2 Vk (1) = Vk (2) 15 Свойства: 1. Одинаковые расширения неразличимых префиксов – неразличимы: : 1 2 1 2 (по построению k) 2. Неразличимость является отношением эквивалентности на . 3. < k, goto > < /, / > Для всех неразличимых между собой активных префиксов информация о допустимых состояниях вывода, которую несет общее для них множество Vk, одинакова. Исходя из нее, при любом из этих префиксов в стеке, анализатор выполнит один и тот же следующий шаг, снова получит одно и то же множество допустимых ситуаций в следующем состоянии (по свойству 3) и т.д. Иными словами, дальнейшее поведение анализатора из состояний, в которых активные префиксы были неразличимы, одинаково. Состояния анализатора, неразличимые по множествам допустимых ситуаций, могут быть приняты за одно. Одинаковое знание влечет один и тот же шаг продолжения, а значит, и все последующие состояния и действия будут одинаковы. В этом смысле множество классов эквивалентности неразличимых активных префиксов / является подходящей моделью искомых состояний для LR(k)-анализатора. Свойство 3 означает, что это множество классов изоморфно системе канонических множеств k, которую мы уже умеем строить. Таким образом, система k может быть принята в качестве искомого множества состояний LR(k)-анализатора, потому что она удовлетворяет исходным требованиям к состояниям. Построение множества ситуаций для всех активных префиксов вовсе не требует перебора всех префиксов. Данный алгоритм выполняет обход корневого дерева < , >, делая при этом два вида отсечений ветвей: 1. Отсечение по активному префиксу , для которого Vk () пусто: ни сам этот префикс, ни все его расширения никогда не встречаются при разборе, т.к. им не соответствует ни одна допустимая LR(k)ситуация. 2. Отсечение по активному префиксу , для которого Vk () совпадает с некоторым другим Vk (1): для всех его расширений будет истинно Vk () = Vk (1) – т.е. эти ветви порождают одни и те же последовательности Vk . Благодаря этим отсечениям, обход бесконечного дерева активных префиксов заканчивается за конечное число шагов с образованием конечной системы k. …. LR(k)-грамматики Все рассмотренные выше понятия и построения, хотя и содержат в названиях “LR(k)”, но применимы к произвольной КСГ и пока не исключают конфликтов. Собственно LR(k)-подход предусматривает выделение класса грамматик, свойства которых изначально исключают конфликты, так что нет необходимости решать их во время анализа. Формальное определение класса LR(k)-грамматик, для которого реализуется этот подход: Пополненная грамматика G – LR(k)-грамматика: G LR(k), k 0, если из условий: следует: где: 1. S r* A w r w w, v, x T* 2. S r* B x r v = , A = B, x = v A, B N 3. Firstk (w) = Firstk (v) , , (NT)* Условие 1 означает возможность свертки по A в состоянии ( || w). Условие 2 допускает: – возможность свертки по B 1 в состоянии ( 1 || v) (если x = v, = 1); – перенос в стек терминала a в состоянии ( 1 || azx), имея в виду дальнейшее применение продукции B 1 аz (если v = azx, = 1). Совместность условий 1 и 2 означала бы возможность SR- или RRконфликта (при w = v), но условие 3 запрещает им осуществляться одновременно при совпадении k-префиксов w и v. Грамматики класса LR(0) – это грамматики с исключенным условием 3, т.е. решающие конфликты без предпросмотра. В отличие от класса LL(0), класс LR(0)-грамматик описывает нетривиальные языки. Доказано, что любой LR(k)-язык при k > 0 описывается LR(1)грамматикой, однако такая грамматика может быть очень велика. Даже для LR(1)-грамматик могут получаться очень громоздкие и практически неудобные анализаторы. Поэтому в классе LR(k) выделены и изучаются более узкие, но более практичные подклассы; два из них: Simple LR(k) – SLR(k), и Lookahead LR(k) – LALR(k), – будут рассмотрены ниже. 16 Системы множеств допустимых LR(k)-ситуаций Следующие cистемы множеств используются для построения подклассов LR(k): SLR(k)-система множеств для грамматики G: 0 LALR(k)-система множеств для грамматики G: ’k = { V’ | Vk: V’=Wk: core(W) = core(V) }, где core(V) = {[A | ] | [A | u ]V} (Каждый элемент ’k получается слиянием всех множеств из k, ядра которых совпадают. Ядро множества получается стиранием информации о предикторах u из всех ситуаций.) Управляющий конечный автомат LR(k)-анализатора Конечная система множеств k может быть представлена как конечный автомат с заданной функцией переходов goto: K = <K = NT, QK = k, q0 = Vk( ), F = {Vk() | [A | u] Vk()}, K = goto >. Любая цепочка из терминалов и нетерминалов, переводящая автомат из состояния q0 = Vk() в состояние Vk(), является одним из возможных префиксов. «Допускающими» состояниями этого КА являются множества состояний, в которых возможна свертка по одной из продукций. Этот автомат теперь будет управлять работой LR(k)-анализатора. Свойства LR(k)-грамматик: 1. LR(k)-грамматика однозначна. Соотношения классов LR(k)-грамматик: 1. k: LR(k) LR(k+1) . 2. k>0: SLR(k) LALR(k) LR(k) . 17