
- •Теория языков программирования и методы трансляции
- •2.3 Грамматики
- •2.4. Распознаватели
- •2.5 Регулярные выражения и синтаксические диаграммы
- •26. Понятие синтаксического анализа. Левые и правые анализаторы. Реализация левого анализатора на базе к- предсказывающего алгоритма разбора.
- •Ll(k) - грамматики
26. Понятие синтаксического анализа. Левые и правые анализаторы. Реализация левого анализатора на базе к- предсказывающего алгоритма разбора.
Основная функция анализатора в фазе синтаксического анализа состоит в получении и представлении этой информации в виде дерева разбора или другой структуры. Эта структура называется промежуточной программой и используется для генерации кода.
Синтаксический анализ, в ходе которого уясняется структура анализируемой цепочки лексем, называется синтаксическим разбором. Выделяют два вида разбора. Введем их формальное определение.
Определение. Пусть G=(N,,P,S) - КС-грамматика, правила которой пронумерованы 1,2, ...,р, (NU)*. Тогда левым разбором цепочки называется последовательность правил, примененных при левом выводе цепочки из S;правым разбором цепочки называется обращение последовательности правил, примененных при правом выводе цепочки из S.
Эти разборы можно представить в виде последовательности номеров из множества {1, 2, . . . , р}.
Пример 5.1. Рассмотрим грамматику с такой нумерацией правил: (1) E E+T,(2) E T,(3) T T*F,(4) T F,(5) F (E),(6) F a,
а так же цепочку a*(a+a) и ее левый и правый разборы. Имеем:
левый вывод: Е2 Т3 Т*F4 F*F6a*F5 a*(E)1 a*(E+T)2 a*(T+T)4 a*(F+T)6 a*(a+T)4a*(a+F)6 a*(a+a) ;левый разбор: 23465124646;
правый вывод: Е 2Т 3Т*F 5 T*(E) 1 T*(E+T) 4 T*(E+F) 6 T*(E+a) 2 T*(T+a) 4 T*(F+a) 6 T*(a+a) 4 F*(a+a) 6 a*(a+a);правый разбор: 64642641532.
Большинство методов синтаксического анализа делят на два класса разбора, – нисходящего и восходящего. Анализаторы, реализующие методы нисходящего разбора, выдают на выходе левые разборы и называются левыми анализаторами, а реализующие методы восходящего разбора, выдают правые разборы и называются правыми анализаторами. Оказывается, что эти анализаторы можно моделировать при помощи соответствующих МП - преобразователей.
Будем рассматривать только нисходящие методы, а восходящие методы рассмотрим только на уровне стратегии. Рассмотрим стратегию нисходящего разбора.
Пусть = i1 ... in — левый разбор цепочки L(G), где G — КС-грамматика. Зная можно построить левый вывод и соответствующее дерево вывода цепочки .
Ll(k) - грамматики
Выше анализаторы были определены как недетерминированные МП-преобразователи. Попытка их реализации для широкого класса КС-грамматик приводит к так называемым алгоритмам с “возвратами” которые требуют слишком больших затрат времени. На практике обычно ограничивают классы грамматик таким образом, чтобы сделать процесс разбора полностью детерминированным. Оказывается, что эти ограниченные классы КС-грамматик адекватно отражают все синтаксические черты языков программирования и пригодны для описания проблемно-ориентированных языков. Требованиям детерминированности левых анализаторов наилучшим образом удовлетворяют так называемые LL(k)-грамматики, для которых левый анализатор работает детерминировано, если позволить ему принимать во внимание k входных символов, расположенных справа от текущей входной позиции. Входная цепочка считывается таким анализатором один раз слева направо и в процессе анализа не происходит возвратов к уже прочитанной части цепочки. Такие анализаторы называются однопроходными.
Для определения LL(k)-грамматики введем функцию FIRSTk().
Определение. Для КС-грамматики G=(N,,P,S) определим функцию
FIRSTk()={x* | l* xB и |x|=k или * x и |x| < k}
Иначе говоря, множество FIRSTk() состоит из всех терминальных префиксов длины k (или меньше, если из выводится терминальная цепочка длины, меньшей k) терминальных цепочек, выводимых из .
Пример 5.4. Пусть грамматика имеет одно правило SSa|b. Определим FIRST3(Sa).
Из Sa можно вывести следующие цепочки: ba, baa, baaa,... и т.д.
Из определения следует:
FIRST3(Sa)={ba, baa}.
Определение. КС-грамматика G=(N, , Р, S) называется LL(k)-грамматикой для некоторого фиксированного k, если любые два левых вывода(1) S l* A l * x,S l * A l * y
связаны условием: если FIRSTk(x) = FIRSTk(y), то =.
Говоря менее формально, G будет LL(k)-грамматикой, если для данной цепочки A(NUE)* и первых k символов (если они есть), выводящихся из A, существует не более одного правила, которое можно применить к A, чтобы получить вывод какой-нибудь терминальной цепочки, начинающейся с и продолжающейся упомянутыми k терминальными символами.
Грамматика называется LL-грамматикой, если она LL(k)-грамматика для некоторого k.
Определение. КС-грамматика G=(N,,Р,S) без e-правил называется простой LL(1)-грамматикой (или разделенной грамматикой), если для каждого A N все его альтернативы начинаются различными терминальными символами.
Условия (признаки), при которых КС-грамматика является LL(k) грамматикой, формулируются в форме следующей теоремы.
Теорема 5.1. КС-грамматика G=(N, , Р, S) является LL(k) - грамматикой тогда и только тогда, когда для двух различных правил А и А из множества P пересечение
FIRSTk() FIRSTk() = для всех таких A, что S * A.
Здесь — произвольная цепочка ( (NU)*) , которая может появиться в выводах справа от А.
Разбор для LL(k)-грамматики удобно осуществлять с помощью так называемого k-предсказывающего алгоритма разбора, k-предсказывающий алгоритм А для КС-грамматики G=(N,,P,S) использует входную ленту, магазин и выходную ленту и, по существу, моделирует работу МП-преобразователя, опустошающего магазин (рис. 5.2).
Этот алгоритм пытается проследить левый вывод цепочки, записанной на его входной ленте.
При чтении анализируемой цепочки, находящейся на входной ленте, входная головка может “заглядывать вперед” на k очередных символов (отсюда число k в названии k-предсказывающего алгоритма). Эту цепочку из k символов, увиденную впереди входной головкой, будем называть аванцепочкой. На рис. аванцепочкой служит подцепочка u входной цепочки wux.
Магазин содержит цепочку X$, где Х - цепочка магазинных символов из алфавита Г, $ – специальный символ для маркировки дна магазина. Выходная лента содержит цепочку , состоящую из номеров правил.
Определение. Конфигурацией предсказывающего алгоритма А называется тройка (x, X, ), где
1) x — неиспользованная часть входной цепочки;
2) X — содержимое магазина (X — верхний символ);
3) — выходная цепочка.
Работу алгоритма А определяет управляющая таблица М:
(Г {$}) *k {(, i), выброс, допуск, ошибка},
где Г * — цепочка, построенная из магазинных символов;
i — номер правила из множества Р.
На каждом такте сначала определяется аванцепочка u и верхний символ магазина Х. Затем рассматривается элемент М(Х, u) управляющей таблицы и в соответствии с его содержанием выполняется одно из 4 действий:
1)
если М(Х, u) =
(,
i), то
(x, X,
)
(x, ,
i), т.е. верхний
символ магазина заменяется цепочкой
и к выходу добавляется номер правила
i,
входная головка не сдвигается;
2)
если М(а, u)=
«выброс»
и x=a
,
то (x, a,
)
(
,
,
),
т.е. если верхний символ магазина
совпадает с текущим входным символом,
то он выбрасывается из магазина и входная
головка сдвигается на один символ
вправо;
3) если алгоритм достигает конфигурации (e, $, ), работа прекращается и выходная цепочка называется разбором первоначальной входной цепочки. Будем считать, что M($, e) = «допуск» и (e, $, ) — допускающая конфигурация;
4) если M(Х, u)= «ошибка», то разбор прекращается и выдается сообщение об ошибке, а соответствующая конфигурация (x, X, ) называется ошибочной.
Для входной цепочки и ее разбора , получаемого на выходе алгоритма А, будем писать А()=.
Определение. Переводом, определяемым алгоритмом А, называется множество:
(А) = { (, ) | A() = }.
Определение. Алгоритм разбора А для КС-грамматики G называется корректным, если:
1) L(G) = { | A() определено};
2) если A() = , то — левый разбор цепочки .
Управляющая таблица такого алгоритма называется корректной управляющей таблицей для грамматики G.
6. Основные функции семантического анализа проверка семантических
соглашений, распределение памяти, ведение таблицы идентификаторов,
представления промежуточной программы.
Применение моделей
синтаксически управляемой трансляции для генерации кода интерпретации
Понятие генерации кода . Форма представления промежуточной программы. Характеристика алгоритмов генерации кода (Обход деревьев, получение префиксных и постфиксных форм записи цепочек).
После синтаксического анализа промежуточная программа отображается генератором кода в объектную программу. В процессе генерации кода используется информация из таблицы имен для того, чтобы правильно выбрать последовательность команд (например, код для А+В зависит от типа переменных А и В, а информация о типе находится в таблице имен ) и правильно распределить память (т. е. выяснить, является переменная скаляром или структурированным объектом ).
Представления промежуточной программы. Промежуточная программа должна, с одной стороны, отражать синтаксическую структуру исходной программы, а с другой стороны, каждый оператор промежуточной программы должен относительно просто транслироваться в машинный код.
Наиболее распространенными способами представления промежуточной программы являются :
1) постфиксная польская запись ;
2) префиксная польская запись ;
3) списочные структуры, представляющие деревья ;
4) многоадресный код с явно именуемыми результатами ;
5) многоадресный код с неявно именуемыми результатами.
Рассмотрим эти способы представления на примере оператора присваивания :
A:=B+C ( D ) ,
постфиксная и префиксная формы польской записи этого оператора имеют вид :
A B C D + ; постфиксная форма ;
A + B C D ; префиксная форма .
Правило построения синтаксического дерева следующее :
внутренние вершины дерева представляют операции , операндами которых служат их (вершины) прямые потомки .
Можно использовать три способа обхода дерева : слева - направо, сверху - вниз, снизу - вверх . Легко видеть, что между этими способами обхода и формами записи существует соответствие :обход слева - направо инфиксная форма (обычная)
A B C D ;
обход сверху - вниз префиксная форма A B C D ;
обход снизу - вверх постфиксная форма A B C D .
Синтаксическое дерево можно закодировать списочной структурой. Тогда эта структура будет являться промежуточной программой.
Другой метод кодирования синтаксического дерева применение многоадресного кода с явно именуемыми результатами. Используя этот код, синтаксическое дерево (рис.6.1) можно представить в виде последовательности следующих элементарных кодов :
T1 D,
T2 C T1,
T3 B T2,
A T3 .
Код вида А B1Bn означает, что n - местную операцию необходимо применить к текущим значениям переменных (операндов) B1Bn, и полученное значение присвоить переменной А. В таком коде используется ряд промежуточных переменных для запоминания результатов. Можно избежать их использования, если применить многоадресный код с неявно именуемыми результатами. Для этого каждый элементарный код помечается числом, затем левая часть кода убирается и обращение к промежуточному результату происходит при помощи этого приписанного числа. Для рассматриваемого примера последовательность кодов с неявно именуемыми результатами выглядит следующим образом :
-
1 : D,
2 : C (1),
3 : B (2),
4 : = A (3).
Число в скобках здесь означает адрес выражения, помеченного этим числом. Проиллюстрируем еще раз технику представления промежуточной программы на примере оператора “ если ” , следующего вида :
if i = j then S1 else S2 ,
где S1 и S2 некоторые произвольные операторы.
Возможное
постфиксное польское представление
этого оператора имеет вид i j EQUAL L2
JFALSE
L JAMP
,
где
и
постфиксное представление операторов
S1
и S2
соответственно ;
EQUAL бинарная операция проверки на равенство операндов (принимает значение истина или ложь ) ;
L2
метка начала
;
JFALSE бинарная операция , вызывающая переход по второму аргументу , если значение первого ложь , и не вызывающая никаких действий , если значение первого истина ;
L
метка первой команды следующей за
;
JUMP унарная операция, вызывающая переход на точку, задаваемую аргументом .
Левый анализатор построил бы для данного оператора “если” его левый разбор , которому соответствует дерево вывода, показанное на рис. 6.2.
Обработав
это дерево и оставив в нем только
существенную информацию об операциях
и операндах, анализатор выдаст
синтаксическое дерево (промежуточную
программу), показанное на рис. 6.3.
Сравнивая рис. 6.2 и 6.3 легко видеть , что дерево вывода и синтаксическое дерево тесно связаны. Как правило, синтаксическое дерево является деревом вывода , в котором удалены цепные правила . В то же время анализатор или генератор кода легко может заменить номера правил разбора на команды построения синтаксического дерева, поэтому разборы , порождаемые анализаторами, можно считать компактными представлениями промежуточных программ.
Преобразование промежуточной программы в ассемблерный код
Фаза генерации кода так же как и предшествующие ей фазы, является процессом перевода промежуточной программы в объектную. Поэтому многие конструкции промежуточной программы могут быть преобразованы в последовательность кодов при помощи уже известной нам модели транслятора — МП-преобразователя, а также различных ее модификаций, в частности, МП - процессора. Кроме того, в качестве моделей могут использоваться различные схемы синтаксически управляемых переводов (СУ-схемы), реализуемые при помощи соответствующих алгоритмов генерации кода.
Чтобы дать некоторое представление об этих алгоритмах, мы вернемся к примеру, рассмотренному в п.1.1 и приведем алгоритм генерации кода для простых операторов присваивания, содержащих в правой части операции “+” и “”. После обработки оператора такого типа, синтаксический анализатор формирует на выходе синтаксическое дерево, которое может иметь только три типа внутренних вершин — а, б и в соответственно (рис. 6.6).
Рис. 6.6
Так, для оператора присваивания S:= (A+B) 0.17
анализатор
построит дерево, показанное на рис.6.7,
и его внутренние вершины n3,
n2,
и n1
будут иметь типы а, б и в соответственно.
Будем считать, что каждой вершине n
дерева приписано целое число, называемое
ее уровнем. Все листья дерева имеют
уровень 0, непосредственные предки
листьев — уровень 1 и т. д., как показано
на рис. 6.7.
Рис. 6.7
Свяжем с каждой внутренней вершиной n дерева цепочку кодов (ассемблерных команд) — С(n). Цепочка кодов будет иметь следующую интерпретацию:
1. Если n — вершина типа а, то такой вершине будет соответствовать цепочка С(n), которая вычисляет значение выражения, соответствующего правому поддереву, и помещает результат в ячейку соответствующей переменной, именем которой помечено левое поддерево.
2. Если n — вершина типа б или в, то цепочка LOAD C(n) будет содержать последовательность команд, которая засылает в регистр значение выражения, соответствующего поддереву, над которым доминирует вершина n.
Приведем теперь формальное описание алгоритма.
Алгоритм А (генерация ассемблерного кода).
Вход А (помеченное упорядоченное дерево, представляющее оператор присваивания с операциями “+” и “”).
Выход А (ассемблерный код, вычисляющий этот оператор).Описание А.
1. Обработать листья дерева:
а) если лист помечен “<пер>“, то соответствующий код этой вершины — это просто имя этой переменной , т.е.С(n) = имя;
б) если вершина помечена как <const>, то С(n) = const;
в) если вершина помечена =, , +, то С(n) — пусто.
2. Обработать вершины уровня 1,затем вершины уровня 2 и т.д.:
а) если вершина имеет тип а, а ее прямые потомки — вершины n1,n2 и n3 , то будет сгенерирован такой код С(n):
LOAD C(n3), STORE C(n1);
б) если n — вершина типа б с прямыми потомками n1, n2 и n3, то будет сгенерирован такой код С(n): C(n3); STORE $L; LOAD C(n1); ADD $L ;
в) если n - вершина типа в, то будет сгенерирован такой код: C(n3); STORE $L; LOAD C(n2); MPY $L.Здесь $L обозначает имя временной ячейки, состоящее из символа $ и значения уровня вершины L.В результате обработки дерева, показанного на рис. 6.7, на первом шаге А1 будут сформированы коды, соответствующие ячейкам: S, A, B, 0.17.
На втором шаге будут сформированы коды для внутренних вершин:
тип б C(n1)=B; STORE $1; LJAD A; ADD$1;
тип в C(n2)=0.17; STORE $2; LOAD B; STORE &1;
LOAD A;ADD$1; MPY $2;
тип а C(n3)=LOAD 0.17; STORE$2; LOAD B; STORE$1; LOAD A;ADD$1; MPY$2; STORE S.
Записывая каждый код с новой строки, получим:
LOAD 0.17 STORE $2 LOAD B STORE $1 LOAD A ADD $1 MPY $2 STORE S
В качестве упражнения получите ассемблерный код при помощи А для оператора S:=(AB+CD) 0.5.