1. языки и грамматики

advertisement
КОНСПЕКТ ЛЕКЦИЙ
По дисциплине «Теория автоматов и формальных языков»
Исполнитель
д. т. н., профессор ТГУ
Ю.Л. Костюк
Томск 2010
ОГЛАВЛЕНИЕ
1. ЯЗЫКИ И ГРАММАТИКИ .......................................................................................... 3
1.1. Язык как множество ................................................................................................. 3
1.2. Порождающая грамматика ...................................................................................... 3
1.3. Процесс порождения ................................................................................................ 4
1.4. Классификация грамматик по Хомскому .............................................................. 4
1.5. Классификация языков по Хомскому .................................................................... 5
1.6. Задача распознавания цепочек языка ..................................................................... 6
2. ПРИНЦИПЫ ТРАНСЛЯЦИИ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ ................. 7
3. ЛЕКСИЧЕСКИЙ АНАЛИЗ.......................................................................................... 9
3.1. Конечный автомат .................................................................................................... 9
3.2. Построение детерминированного конечного автомата........................................ 9
3.3. Недетерминированный конечный автомат .......................................................... 11
3.4. Преобразование неоднозначной А-грамматики к однозначной ....................... 12
3.5. Удаление из грамматики бесполезных нетерминалов ....................................... 13
3.6. Лексический анализатор ........................................................................................ 14
4. СИНТАКСИЧЕСКИЙ АНАЛИЗ .............................................................................. 18
4.1. Дерево порождения для КС-грамматики ............................................................. 18
4.2. Автомат с магазинной памятью ............................................................................ 18
4.3. LL-разбор КС-грамматики автоматом с магазинной памятью ......................... 20
4.4. Левая рекурсия и ее устранение ........................................................................... 22
4.5. Преобразование КС-грамматики к обобщенной нормальной форме Грейбах 23
4.6. Детерминированный LL-разбор ........................................................................... 23
5. ОБРАТНАЯ ПОЛЬСКАЯ СТРОКА, КАК ПРОМЕЖУТОЧНАЯ ФОРМА
ПРОГРАММЫ ........................................................................................................ 26
5.1. Обратная польская строка для арифметических выражений ............................ 26
5.2. Генерация ОПС для арифметических выражений .............................................. 27
5.3. Вычисление ОПС для присваиваний и арифметических выражений с
индексами ....................................................................................................................... 30
5.4. Генерация ОПС для присваиваний и арифметических выражений с
индексами ....................................................................................................................... 32
5.5. ОПС для условных, циклических и составных операторов .............................. 34
5.6. ОПС для стандартных операторов ....................................................................... 38
5.7. Распределение памяти и описание переменных ................................................. 39
5.8. Обработка ошибок ................................................................................................. 41
6. ГЕНЕРАЦИЯ КОМАНД В КОМПИЛЯТОРЕ ....................................................... 43
6.1. Распределение памяти при генерации команд .................................................... 43
6.2. Генерация команд для присваиваний и арифметических выражений ............. 44
6.3. Генерация команд с индексными выражениями................................................. 47
6.4. Генерация команд сравнения и перехода ............................................................ 52
2
1. ЯЗЫКИ И ГРАММАТИКИ
1.1. Язык как множество
Пусть задано некоторое конечное множество символов, называемое алфавитом.
Из этих символов можно конструировать последовательности – цепочки символов.
Пусть алфавит Σ содержит m символов. Из символов алфавита можно образовывать
последовательности – цепочки символов. Количество символов в цепочке называют
длиной. Пусть имеется некоторая цепочка α. Запись |α| обозначает длину этой
цепочки. Две цепочки α и β, записанные подряд: αβ, обозначают цепочку, в которой
вначале идут символы цепочки α, а затем символы цепочки β. Такое склеивание
цепочек называют конкатенацией. Длина полученной цепочки равна сумме длин
отдельных цепочек: |αβ| = |α| + |β|. Для общности удобно считать, что существует
пустая цепочка длиной 0, для нее вводится особое обозначение λ, |λ| = 0. Заметим,
что αλ = λα = α.
Из алфавита, содержащего m символов, можно составить ровно m различных
всевозможных цепочек длиной 1, m2 цепочек длины 2, и т.д., в общем
случае количество различных цепочек длины n равно mn. Множество всех цепочек
длины 1 есть алфавит Σ, множество всех цепочек длины 2 обозначим как Σ2, и т.д.,
множество всех цепочек длины n обозначим как Σn. Множество всевозможных
цепочек произвольной ненулевой длины, обозначаемое как Σ+, называется
положительным транзитивным замыканием множества Σ. Оно равно объединению
бесконечного количества множеств:
Σ+ = Σ U Σ2 U Σ3 U … U Σn U …
Полное транзитивное замыкание множества Σ, обозначаемое как Σ*,
определяется как объединение Σ+ и множества из одной пустой цепочки λ:
Σ* = Σ+ U {λ}.
Язык L над алфавитом Σ определяется как некоторое, возможно бесконечное,
множество цепочек, являющееся подмножеством Σ*, L  * .
1.2. Порождающая грамматика
Существуют разные способы задания языков. Если язык конечен, то его в
принципе можно задать перечислением всех принадлежащих ему цепочек. Для
бесконечного языка такой способ невозможен, в этом случае язык задается
некоторой грамматикой. Порождающая грамматика G, задающая язык L,
определяется как четверка множеств:
G(L) = {Σ, N, S, P},
где Σ – алфавит (множество) символов языка; N – алфавит (множество) символов
грамматики; S – подмножество символов грамматики, называемых начальными,
S  N ; P – множество порождающих правил вида:
α → β,
где α и β – цепочки символов из алфавитов языка Σ и грамматики N, причем в
цепочке α должно быть не менее одного символа из алфавита N. Каждое из правил
задает подстановку, т.е. замену цепочки α на цепочку β.
3
Цепочку α называют левой, а цепочку β – правой частью порождающего
правила. Если выполняется неравенство |α| ≤ |β|, то такое порождающее правило
называют неукорачивающим. Если же |α| > |β|, то порождающее правило называется
укорачивающим. В общем случае допустимо, чтобы в некоторых правилах цепочка
β была пустой.
Далее в примерах заглавными латинскими буквами будут обозначаться
символы грамматики (из алфавита N), строчными буквами и другими знаками –
символы языка (из алфавита Σ), строчными греческими – цепочки символов (из
обоих алфавитов N и Σ).
1.3. Процесс порождения
Порождающая грамматика позволяет получить (породить) все цепочки
символов определяемого ею языка следующим образом.
Порождение всегда начинается с одного из начальных символов грамматики,
пусть это будет S1. Среди всех правил множества P применяется правило, в котором
левая часть (слева от стрелки) состоит только из одного символа S1, и эта левая
часть заменяется на правую. Затем среди полученной цепочки символов находится
такая ее часть (подцепочка), которая совпадает с левой частью какого-либо из
правил множества P, и делается еще одна подстановка и т.д. Процесс заканчивается,
если в полученной после всех подстановок цепочке больше не найдется ни одной
части, совпадающей с левой частью ни одного из правил множества P.
Если в полученной таким образом цепочке все символы принадлежат алфавиту
языка Σ, то считается, что вся порожденная цепочка принадлежит языку L. Если же
в этой цепочке присутствует хотя бы один символов алфавита грамматики N, то
такая цепочка не может принадлежать языку L. В этом случае говорят, что процесс
порождения зашел в тупик. Может случиться, что процесс порождения никак не
может остановиться, какие бы правила ни применялись на очередном шаге. Тогда
говорят, что процесс порождения зациклил (попал в бесконечный цикл).
Символы алфавита языка Σ имеют еще одно название – терминальные символы
(или просто терминалы), потому что получившаяся цепочка, состоящая только из
них, прекращает процесс порождения. В свою очередь, символы алфавита
грамматики N также имеют другое название – нетерминальные символы (или просто
нетерминалы).
1.4. Классификация грамматик по Хомскому
Сложность грамматики определяется ограничениями на порождающие правила.
Если на правила не накладываются никакие дополнительные ограничения, то
грамматика относится к классу 0, или к общему классу. Наложение все более
строгих ограничений позволяет определить иерархию классов грамматик по
убыванию сложности.
Грамматика относится к классу 1, если все порождающие правила имеют вид:
αAβ → αγβ,
4
где α и β – цепочки из терминальных и (или) нетерминальных символов, одна из
них или они обе могут быть пустыми; A – нетерминальный символ, γ – цепочка из
терминальных и (или) нетерминальных символов, она не должна быть пустой.
Грамматика класса 1 имеет еще одно название – контекстно-зависимая грамматика
(или сокращенно КЗ-грамматика), потому что при порождении нетерминал A
заменяется цепочкой γ в контексте цепочек α и β. Таким образом, все правила КЗграмматики должны быть неукорачивающими.
Грамматика относится к классу 2, если все порождающие правила имеют вид:
A → γ,
где A – нетерминальный символ,
γ – цепочка из терминальных и (или)
нетерминальных символов. Грамматика класса 2 имеет еще одно название –
бесконтекстная или контекстно-свободная грамматика (сокращенно КСграмматика). В КС-грамматике допускаются правила, правая часть которых состоит
из пустой цепочки, т.е. правила, которые являются укорачивающими.
Грамматика относится к классу 3, если все порождающие правила имеют вид:
A → aB или A → a,
где A,B – нетерминальные символы, а – терминальный символ. Грамматика
класса 3 имеет еще одно название – автоматная грамматика (сокращенно Аграмматика).
Кроме классификации по Хомскому, грамматики делятся на однозначные и
неоднозначные. Чтобы ввести такую классификацию, определим левое
каноническое порождение. Рассмотрим процесс порождения на некотором шаге.
Пусть полученная к этому шагу цепочка из терминальных и нетерминальных
символов имеет следующий вид:
ω1αω2,
такой, что α является такой самой левой частью всей цепочки, которая совпадает с
левой частью какого-либо из правил грамматики. Пусть это правило: α → β, тогда
после его применения получится цепочка:
ω1βω2.
Если на каждом шаге порождение применяется только таким образом, то оно
называется левым каноническим.
Грамматика называется однозначной, если для любой полученной ею цепочки
языка существует единственная последовательность применения правил при левом
каноническом порождении.
Таким образом, чтобы обнаружить неоднозначность грамматики, достаточно
привести пример хотя бы одной цепочки языка, которую можно получить более чем
одним вариантом левого канонического порождения.
Свойством однозначности или неоднозначности могут обладать грамматики
любого класса: от общего класса 0 до А-грамматик класса 3.
1.5. Классификация языков по Хомскому
Если две различные грамматики порождают один и тот же язык, т.е.
совпадающие множества терминальных цепочек, то они называются
эквивалентными. Такие грамматики должны иметь одинаковые алфавиты символов
5
языка Σ, но могут иметь разные алфавиты символов грамматики N и (или)
множества порождающих правил P.
Если задана некоторая грамматика, то легко построить другую эквивалентную
ей грамматику. Пусть, например, в грамматике имеется правило α → β. Добавив в
грамматику новый нетерминал B, и заменив указанное правило на два новых: α → B,
B → β, мы не изменим множество порождаемых грамматикой терминальных
цепочек, т.е. язык. Легко привести примеры и других изменений в грамматике, не
изменяющих порождаемый ею язык.
Более того, две эквивалентные грамматики могут принадлежать различным
классам по Хомскому. В этом случае класс языка определяется классом самой
простой из порождающих его эквивалентных грамматик.
Язык является однозначным, если существует хотя бы одна однозначная
порождающая его грамматика.
1.6. Задача распознавания цепочек языка
Пусть задана грамматика G(L) = {Σ, N, S, P} и цепочка γ терминальных
символов алфавита Σ. Задача распознавания состоит в том, чтобы ответить на
вопрос, принадлежит ли цепочка γ языку, порождаемому грамматикой G(L). Эту
задачу можно
решить
следующим
образом:
попробовать построить
последовательность применений правил грамматики, порождающих цепочку γ. Если
удается найти такую последовательность, то ответом будет «да», в противном
случае – «нет». Построенную при ответе «да» последовательность применений
правил называют грамматическим разбором цепочки. Грамматический разбор
необходим, если требуется произвести трансляцию цепочки, т.е. ее перевод в
некоторую другую форму.
Сложность задачи распознавания зависит от сложности грамматики. Доказано,
что для грамматики класса 0 в общем случае эта задача алгоритмически
неразрешима, т.е. не существует универсального алгоритма, который для любой
грамматики класса 0 и любой входной цепочки после конечного числа шагов выдаст
ответ «да» или «нет». Алгоритмы распознавания построены для грамматик классов
1, 2 и 3, причем как для однозначных, так и для неоднозначных грамматик.
Сложность алгоритмов распознавания тем меньше, чем проще грамматика. Самый
простой и эффективный алгоритм существует для распознавания однозначных
грамматик класса 3 (А-грамматик).
Порождающие грамматики применяются для решения задач распознавания
самых различных языков. Для естественных языков (английского, русского и др.) их
применение ограниченно: в настоящее время удалось построить грамматики (КЗграмматики) только для упрощенных подмножеств естественных языков. В то же
время для большинства языков программирования (языков высокого уровня)
однозначные КС-грамматики явились мощным инструментом для создания
трансляторов. Что же касается машинных языков, а также автокодов, ассемблеров,
командных языков, то для их описания оказалось достаточным применение
однозначных А-грамматик.
6
2. ПРИНЦИПЫ ТРАНСЛЯЦИИ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ
Программу, написанную на языке программирования высокого уровня
(исходный модуль), необходимо преобразовать в такой вид, чтобы она могла быть
исполнена на компьютере, т.е. произвести трансляцию. Программы-трансляторы
делятся на два основных вида: интерпретаторы и компиляторы. Интерпретатор
последовательно обрабатывает каждое предложение (оператор) исходного модуля и
немедленно его исполняет. Компилятор преобразует исходный модуль в машинную
программу (исполняемый модуль), состоящий из машинных команд. После этого
исполняемый модуль может выполняться на компьютере многократно. Возможен
также промежуточный вид транслятора, транслятор интерпретирующего типа, когда
вначале исходный модуль преобразуется в программу на некотором промежуточном
языке и только потом исполняется интерпретатором.
Эффективность транслятора зависит от его типа. Интерпретатор относительно
быстро обрабатывает каждое предложение языка программирования, а затем его
исполняет. Если сравнить время обработки и исполнения некоторого предложения
языка и время исполнения группы машинных команд, эквивалентных этому
предложению, то разница может достигать нескольких сот раз. В трансляторе
интерпретирующего типа время исполнения команд промежуточного языка может
быть в десятки раз больше времени исполнения эквивалентной группы машинных
команд. Время исполнения программы, полученной компилятором, обычно больше,
чем время исполнения эквивалентной программы в машинных командах,
написанной вручную квалифицированным программистом, разница может
составлять от полутора до трех раз и более. Однако ввиду чрезмерно большой
трудоемкости написания программ непосредственно в машинных командах в
настоящее время в подавляющем числе случаев используют языки
программирования. Компиляторы более эффективны, чем интерпретаторы, однако
их создание требует в десятки раз большего труда, чем создание интерпретаторов.
Описание языка программирования состоит из синтаксиса и семантики.
Синтаксис, представляющий формальные правила записи предложений языка,
задается порождающей грамматикой. Семантика придает смысл предложениям
языка, она может быть задана разными способами, в частности, описанием на
русском (или английском) языке. Для того чтобы реализовать семантику в
трансляторе, достаточно описать только ту ее часть, которую можно задать в виде
семантических программ, дополняющих алгоритм грамматического разбора и
выполняющих собственно трансляцию.
Порождающая грамматика для задания синтаксиса языка программирования
(КС-грамматика) обычно имеет довольно большой размер, в частности, количество
нетерминальных символов может доходить до несколько сот. В то же время
отдельные конструкции языка могут описываться гораздо более простой Аграмматикой. Это лексемы – имена (идентификаторы), служебные слова, константы
и др. Поэтому всю грамматику описывают в виде двухуровневой: на нижнем уровне
А-грамматика, начальные нетерминалы которой суть распознанные лексемы, а на
верхнем уровне КС-грамматика, в которой лексемы используются как терминальные
7
символы. Это позволяет существенно упростить наиболее сложную КС-грамматику
и за счет этого не только облегчить создание транслятора, но и ускорить процесс
трансляции.
В целом работа компилятора содержит следующие этапы:
1) лексический анализ, выделение лексем и формирование таблиц, содержащих
информацию о лексемах (типы имен переменных, значения констант);
2) синтаксический анализ, перевод программы во внутреннюю форму;
3) оптимизация внутренней формы программы;
4) генерация машинных команд;
5) оптимизация машинных команд.
В простом компиляторе могут отсутствовать этапы оптимизации. Кроме того,
этап лексического анализа может выполняться не сразу весь, а совмещаться с
синтаксическим анализом, формируя на выходе очередную распознанную лексему.
Многие современные компиляторы разрабатываются таким образом, чтобы
была возможность генерации машинных команд для нескольких типов процессоров
и различных операционных систем. В этом случае для каждого типа процессора
достаточно реализовать дополнительно 4-й и 5-й этапы трансляции.
Кросс-компилятор реализуется как программа, выполняющаяся на некотором
рабочем процессоре (и операционной системе), но генерирующая команды для
другого (объектного) процессора. При этом, чтобы отладить такую программу,
необходима программа-эмулятор, которая могла бы на рабочем процессоре
моделировать выполнение команд объектного процессора. Программа-эмулятор
необходима еще и потому, что на ней можно произвести детальную диагностику во
время выполнения программы. Это особенно важно, если объектный процессор
является встроенным, и в нем и его операционной системе не предусмотрены
средства отладки. Заметим, что описать машинный язык, являющийся входным для
программы-эмулятора, можно средствами А-грамматик.
Транслятор интерпретирующего типа при прочих равных условиях гораздо
проще компилятора. Такой транслятор содержит этапы:
1) лексического анализа и выделения лексем;
2) синтаксического анализа и генерации программы на промежуточном языке;
3) исполнения (интерпретации) программы на промежуточном языке.
В принципе программу на промежуточном языке можно скомпоновать в виде
модуля на промежуточном языке и затем многократно исполнять, запуская ее
выполнение под управлением программы-интерпретатора (реализующего 3-й этап
трансляции). Тогда такой транслятор можно считать кросс-компилятором
промежуточного языка.
8
3. ЛЕКСИЧЕСКИЙ АНАЛИЗ
3.1. Конечный автомат
В А-грамматике все порождающие правила имеют вид:
A → aB или A → a,
где A,B – нетерминальные символы, а – терминальный символ.
В процессе порождения, начинающегося с начального нетерминала, цепочка
всегда имеет очень простой вид: γA, где γ – терминальная цепочка, A – нетерминал.
И только на самом последнем шаге этот единственный нетерминал заменяется
терминальным символом. Процесс грамматического разбора должен повторять
процесс порождения, его можно реализовать с помощью алгоритма, называемого
конечным автоматом (КА).
Конечный автомат задается пятеркой множеств:
{Σ, Q, q0, F, δ},
где Σ – множество (алфавит) входных символов; Q – множество состояний КА; q0 –
начальное состояние, q0  Q ; F – множество заключительных состояний, F  Q ;
δ – множество правил перехода, каждое правило имеет вид:
(a, qi) → qj,
где a  , qi  Q, q j  Q . Правило перехода задает переход из состояния qi , когда
на входе читается символ a, в состояние qj.
КА в цикле прочитывает входную цепочку слева направо, на каждом шаге
читается очередной символ. В начале работы КА находится в начальном состоянии.
На каждом шаге производится переход в новое состояние в соответствии с
правилами перехода. Работа КА завершается, когда цепочка прочитана до конца.
Если при этом автомат находится в одном из заключительных состояний, то такая
входная цепочка считается успешно распознанной. Если же цепочка прочитана до
конца, но КА не находится ни в одном из заключительных состояний, то такая
входная цепочка считается нераспознанной. В процессе работы может оказаться,
что для некоторого очередного символа текущего состояния КА нет
соответствующего правила перехода. В этом случае КА попадает в тупик, и входная
цепочка также считается нераспознанной.
Если множество правил перехода таково, что для каждой пары (a, qi) имеется не
более одного правила, то КА называется детерминированным (ДКА) или
однозначно определенным. Если же найдется хотя бы два разных правила перехода
с одинаковыми парами (a, qi), то КА называется недетерминированным (НКА), его
работа существенно усложняется, так как придется одновременно отслеживать не
одно, а несколько текущих состояний КА.
3.2. Построение детерминированного конечного автомата
Вначале изменим грамматику таким образом, чтобы в конце любой
порождаемой ею цепочки был концевой символ ┴, отличающийся от всех символов
9
алфавита языка. Рассмотрим все правила грамматики вида: A → a. Заменим это
правило двумя правилами: A → aR, R → ┴ , где R – новый нетерминальный символ.
Нетрудно видеть, что после всех таких замен в грамматике останутся только
правила вида A → aB и одно единственное правило R → ┴ , при этом в конце всех
порождаемых цепочек появится дополнительный концевой символ ┴ .
Алфавит входных символов КА будет совпадать с алфавитом символов языка
грамматики, включая концевой символ ┴, множество состояний КА будет включать
все множество нетерминалов (символов грамматики), а также дополнительное
заключительное состояние F. Тогда каждое правило грамматики вида A → aB,
преобразуется в правило перехода КА: (a, A) → B, а правило грамматики вида R → ┴
преобразуется в правило перехода: (┴, R) → F.
Построенный КА будет детерминированным (ДКА), если А-грамматика
однозначна. В свою очередь, А-грамматика однозначна, если для любой пары (A, a)
имеется не более одного правила вида A → aB. В противном случае А-грамматика
будет неоднозначной, и будет построен НКА.
Множество правил перехода ДКА удобно записать в виде таблицы, каждая
строка в которой соответствует одному состоянию ДКА, а каждый столбец –
символу из алфавита входных символов.
Далее везде для сокращения записи группы правил с одним и тем же
нетерминалом в левой части будем объединять: левую часть в них будем записывать
один раз, а правые части разделять вертикальной чертой. Так, вместо:
A → γ1, A → γ2, …, A → γn
будем записывать:
A → γ1 | γ2 | … | γn .
Пример 1. А-грамматика задана правилами: S → 0A| 1A, A → 0A| 1A| 2. Здесь
нетерминал S – начальный. Цепочки языка, порождаемые этой грамматикой, будут
состоять из непустых последовательностей нулей и единиц, в конце которых
имеется цифра 2.
После изменения грамматика будет содержать правила: S → 0A| 1A,
A → 0A| 1A| 2R, R → ┴. Табл. 1 содержит правила перехода ДКА.
Табл. 1
0 1 2 ┴
S A A
A A A R
R
F
Если на входе этого КА будет цепочка 01102┴, то его состояния в процессе
работы будут изменяться следующим образом: S, A, A, A, A, R, F. Так как цепочка
прочитана вся и ДКА находится в заключительном состоянии, то такая входная
цепочка считается распознанной, т.е. она принадлежит языку. При входной цепочке
0110┴ состояния будут такими: S, A, A, A, A, и возникнет тупик: для состояния A и
входного символа ┴ перехода не задано. Это значит, что такая цепочка не
10
принадлежит языку. Входная цепочка 2┴ также приводит к тупику: на первом же
шаге из состояния S при входном символе 2 переход не задан.
Конец примера.
3.3. Недетерминированный конечный автомат
Среди правил перехода НКА для некоторых пар (a, qi) имеется по два или более
правил перехода в различные новые состояния. Это значит, что КА должен на
каждом шаге одновременно находиться в нескольких состояниях из всего
множества возможных состояний, и каждый раз выполнять одновременный переход
из всех этих состояний в новое множество состояний. Может случиться, что
некоторые из возможных переходов попадают в тупики. Если же получится хотя бы
один нетупиковый переход, то работа НКА продолжается. Цепочка, анализируемая
НКА, считается распознанной, если она прочитана до конца, и если хотя бы одно из
текущих состояний НКА является заключительным.
Таким образом, недетерминированность КА, возникающая при его построении
из неоднозначной А-грамматики, означает, что одновременно отслеживаются
несколько вариантов грамматического разбора, и для успешного распознавания
достаточно, чтобы хотя бы один вариант разбора оказался успешным. При этом
количество вариантов разбора не может превышать числа всех возможных
состояний НКА, т.е. числа нетерминалов в А-грамматике.
Пример 2. А-грамматика задана правилами:
S → 0A| 1A, A → 0A| 1A| 0 | 1.
Здесь нетерминал S – начальный. Цепочки языка, порождаемого этой
грамматикой, будут состоять из последовательностей нулей и единиц длиной не
менее чем 2.
После изменения грамматика будет содержать правила:
S → 0A| 1A, A → 0A| 1A| 0R | 1R, R → ┴.
Табл. 2 содержит правила перехода НКА.
Табл. 2
0
1
┴
S
A
A
A A/R A/R
R
F
При поступлении на вход этого КА цепочки 010┴, состояния КА в процессе
работы будут изменяться следующим образом:
S, A, (A,R), (A,R), F.
Здесь на 3-м шаге работы КА параллельно выполняются два действия:
(0,A) → (A,R) и (0,R) → ?
При этом второе действие попадает в тупик, однако работа КА продолжается,
так как первое действие переводит КА опять в два состояния A и R. На 4-м шаге
работы КА также параллельно выполняются два действия:
(┴, A) → ?, (┴, R) → F.
11
Первое действие приводит к тупику, а второе – к заключительному состоянию
F. Так как цепочка прочитана вся, то она считается распознанной, т.е. цепочка
принадлежит языку.
При входной цепочке 0┴ на втором шаге из состояния A возникнет тупик: для
состояния A и входного символа ┴ перехода не задано. Это значит, что такая цепочка
не принадлежит языку. Аналогичная ситуация возникнет и для входной цепочки 1┴.
Конец примера.
3.4. Преобразование неоднозначной А-грамматики к однозначной
Неоднозначную А-грамматику можно преобразовать к однозначной, расширив
допустимые виды порождающих правил, а именно, наряду с правилами видов
A → aB и A → a, разрешив правила вида A → λ, т.е. разрешив порождение пустой
цепочки в правой части.
Пусть задана неоднозначная А-грамматика. Вначале все правила вида A → a
заменим правилами A → aF, где F – новый нетерминал, и добавим правило F → λ.
Порождаемый преобразованной грамматикой язык при этом не изменится.
В неоднозначной грамматике имеется по крайней мере одна группа правил
вида:
A → aB1 | … | aBn.
(*)
Обозначим B’ = {B1, …, Bn}, т.е. новый нетерминал B’ соответствует множеству
B1, …, Bn нетерминалов. Вместо группы правил (*) включим в грамматику правило:
A → aB’,
(**)
а для всех порождающих правил вида:
B1 → α1, …, Bn → αn
(***)
добавим правила:
B’ → α1 | … | αn .
(****)
Если в грамматике имеется (или появится после преобразования) еще одна
группа правил вида (*), то с ней проделаем аналогичные преобразования, и так до
тех пор, пока не останется ни одной группы правил вида (*). После каждого такого
преобразования язык не изменяется, поэтому язык не изменится и после всех этих
преобразований. При этом получившаяся грамматика будет однозначной.
Заметим также, что после каждого такого преобразования в грамматике
появляется еще один новый нетерминал и несколько новых порождающих правил.
При этом все новые нетерминалы по сути являются подмножествами первоначально
заданного множества нетерминалов. Т.е. если вначале было k нетерминалов, то в
преобразованной грамматике их будет меньше, чем 2k. Правда, при большом числе k
количество нетерминалов может оказаться чрезмерно большим.
Таким способом можно преобразовать любую неоднозначную А-грамматику,
сохранив порождаемый ею язык. Отсюда следует, что все автоматные языки
однозначны.
Заметим, что в преобразованной грамматике появится одно или несколько
правил с пустой правой частью: F → λ. При построении КА по этой грамматике
каждое правило грамматики вида F → λ необходимо заменить правилом F → ┴., а
12
каждому из нетерминалов F должно соответствовать свое заключительное
состояние.
Пример 3. А-грамматика задана правилами:
S → 0A| 1A, A → 0A| 1A| 0 | 1.
Здесь нетерминал S – начальный. Цепочки языка, порождаемого этой
грамматикой, будут состоять из последовательностей нулей и единиц длиной не
менее чем 2.
После преобразования правил A → 0 | 1 грамматика будет следующей:
S → 0A| 1A, A → 0A| 1A| 0F | 1F, F → ┴.
Из-за правил A → 0A| 1A| 0F | 1F грамматика неоднозначна, поэтому обозначим:
B = {A, F}. После всех изменений грамматика будет следующей:
S → 0A| 1A, A → 0 B| 1B, B → 0B | 1B | ┴ , F → ┴ .
В полученной грамматике правило F → ┴ бесполезно, т.к. при любом
порождении, начинающемся с нетерминала S нетерминал F не может появиться в
конце цепочки и это правило никогда не будет применено. Поэтому из грамматики
правило F → ┴ и нетерминал F необходимо удалить. После этого можно построить
табл. 3 с правилами перехода ДКА. Заметим также, что состояние B здесь будет
заключительным.
Табл. 3
0
1
┴
S
A
A
A
B
B
B
B
B
B
Если на входе этого КА будет цепочка 010┴, то его состояния в процессе работы
будут изменяться следующим образом: S, A, B, B, B. Так как цепочка прочитана вся
и ДКА находится в заключительном состоянии, то такая входная цепочка считается
распознанной, т.е. она принадлежит языку. При входной цепочке 0 ┴ на втором шаге
из состояния A возникнет тупик: для состояния A и входного символа ┴ перехода не
задано. Это значит, что такая цепочка не принадлежит языку.
Конец примера.
3.5. Удаление из грамматики бесполезных нетерминалов
В произвольно заданной грамматике могут присутствовать такие нетерминалы
(и порождающие правила, где эти нетерминалы присутствуют), которые
бесполезны: они или никогда не могут появиться при любом процессе порождения,
начинающегося с начального нетерминала, или никогда не могут породить
терминальной цепочки. Назовем их в первом случае бесполезными 1-го типа, а во
втором – бесполезными 2-го типа.
Для того чтобы избавиться от бесполезных нетерминалов 1-го типа, пометим
начальный нетерминал, как полезный. Далее для всех порождающих правил, у
которых в левой части имеется полезный нетерминал, пометим все нетерминалы из
правой части, как полезные. Процесс продолжаем, пока не останется
13
непросмотренных порождающих правил. Все нетерминалы, оставшиеся
непомеченными, являются бесполезными 1-го типа, их следует удалить вместе с
теми правилами грамматики, где они встречаются.
Для того чтобы избавиться от
бесполезных нетерминалов 2-го типа,
просмотрим порождающие правила, у которых правая часть пуста или состоит
только из терминалов. Пометим нетерминалы в левой части этих правил, как
полезные. Далее для всех порождающих правил, у которых в правой части имеются
только полезные нетерминалы и (возможно) терминальные символы, пометим все
нетерминалы в левой части, как полезные. Процесс продолжаем, пока не останется
непросмотренных порождающих правил. Все нетерминалы, оставшиеся
непомеченными, являются бесполезными 2-го типа, их следует удалить вместе с
теми правилами грамматики, где они встречаются.
Очищенная от бесполезных нетерминалов грамматика называется приведенной.
Заметим, что рассмотренный способ удаления бесполезных нетерминалов можно
применить не только к А-грамматикам, но и к КС-грамматикам.
3.6. Лексический анализатор
Лексемы языка программирования, которые можно задавать А-грамматикой и
которые должен распознавать лексический анализатор:
1) служебные слова;
2) имена (идентификаторы);
3) изображения констант (целых и вещественных чисел, символьных строк и
др.);
4) составные символы (:=, <=, >= и др.);
6) одиночные символы языка программирования (. , ; : [ ] + – / и др.);
5) пробелы, символы перевода строк и другие символы редактирования текста;
6) комментарии.
При этом некоторые лексемы (пробелы, комментарии) должны пропускаться, а
для остальных лексем должна быть сформирована семантическая информация.
Удобнее всего лексический анализатор реализовать в виде процедуры, которая
сканирует входной текст, начиная с некоторого заданного символа, выделяет и
распознает очередную лексему (пропуская пробелы, комментарии и другие,
несущественные для синтаксиса языка, лексемы), формирует для этой лексемы
семантическую информацию и вычисляет номер символа входного текста, начиная с
которого следует распознавать следующую лексему.
Для распознавания лексем нужно составить однозначную А-грамматику и
построить для нее таблицу переходов ДКА. При этом с каждым переходом в
таблице необходимо связать свою семантическую программу, которая должна
вызываться всякий раз при выполнении этого перехода. Кроме того, в грамматике
надо учесть, что в лексемах могут быть ошибки. В этом случае анализатор должен
пропустить все символы после ошибки до того места, где можно начинать
выделение следующей лексемы, а в качестве лексемы выдать вид ошибки и ее
местоположение в тексте (номер строки и номер символа в строке текста).
14
При составлении грамматики следует учесть, что символ, находящийся во
входном тексте сразу за лексемой, является ограничителем этой лексемы, а иногда
одновременно и началом следующей лексемы.
Для сокращения размера таблицы переходов некоторые, неразличимые с точки
зрения грамматики символы, удобно сгруппировать. К таким символам относятся
буквы, цифры и др.
Пример 4. Составим грамматику, таблицу переходов ДКА и семантические
программы для распознавания следующих лексем:
1) имен и/или служебных слов, вначале будет формироваться имя, а затем
проверяться по таблице совпадение его с каким-либо служебным словом, буквы в
них – заглавные латинские;
2) целых чисел без знака;
3) составного символа «:=»;
4) строк символов (в кавычках);
5) пробелов;
6) одиночных символов языка;
7) концевого символа ┴ .
Входные символы при этом объединим в следующие группы (в кавычках «»
записано обозначение группы в таблице):
– заглавные латинские буквы «б»;
– цифры 0, 1, …, 9 «ц»;
– двоеточие «:»;
– равно «=»;
– кавычка «’»;
– пробел « »;
– другие символы языка (запятая, точка, + и др.) «с»;
– концевой символ «┴»;
– все другие символы, не являющиеся символами языка, «др».
Среди нетерминалов (состояний ДКА) особо выделим:
S – начальное состояние;
F – правильное заключительное состояние;
О – ошибочное заключительное состояние.
Получившаяся таблица переходов представлена в табл. 4. Благодаря
использованию второго заключительного состояния в построенном ДКА нет
тупиков – для всех пар (текущее состояние, входной символ) предусмотрены
переходы.
Табл. 4
«б» «ц» «:» «=» «’» « » «c» «др» «┴»
S
I
C
A
F
T
S
F
O
F
I
I
I
F
F
F
F
F
F
F
C
D
C
F
F
F
F
F
F
F
A
F
F
F
F
F
F
F
F
F
T
T
T
T
T
F
T
T
T
O
15
D
D
D
O
O
O
O
O
O
O
В семантических программах будут использоваться следующие переменные:
C – массив символов, содержащий входной текст;
i – номер очередного символа входного текста;
x – распознанное целое число;
st – накопленная строка символов или имя;
typ – тип распознанной лексемы:
0 – концевой символ «┴»;
1 – имя,
2 – целое число,
3 – строка символов,
11 – символ :=,
40, …, 93 – другие символы языка, указан код ASCII,
101, …, 200 – служебные слова языка,
–1 – ошибка в лексеме.
Таблица номеров семантических программ представлена в табл. 5.
Табл. 5
«б» «ц» «:» «=» «’» « » «c» «др» «┴»
S
1
3
4
5
6
4
5
7
8
I
2
2
9
9
9
9
9
9
9
C
4
11
12
12
12
12
12
12
12
A
13
13
13
14
13
13
13
13
13
K
15
15
15
15
16
15
15
15
10
D
4
4
10
10
10
10
10
10
10
Далее в кратком виде приведены семантические программы с комментариями.
1. st:=C[i]; i:=i+1; //Начало имени или служебного слова.
2. st:=st+C[i]; i:=i+1; //Продолжение имени или служебного слова.
3. x:=ord(C[i])-ord(’0’); i:=i+1; //Начало числа.
4. i:=i+1;
//Пропуск символа входной строки.
5. typ:=ord(C[i]); i:=i+1; //Распознан символ языка.
6. st:=’’; i:=i+1; //Начало строки символов.
7. typ:=-1; i:=i+1; //Ошибочный символ входной строки.
8. typ:=0;
//Концевой символ входной строки.
9. . . . //Сравнение st с таблицей служебных слов. При совпадении typ
содержит номер служебного слова +100, при несовпадении – число 1.
10. typ:=-1;
//Ошибка во входной строке.
11. x:=x*10+ord(C[i])-ord(’0’); i:=i+1; //Продолжение числа.
12. typ:=2;
//Распознано число.
13. typ:=ord(’:’);
//Распознан символ «:».
14. typ:=11; i:=i+1; //Распознан символ «:=».
15. st:=st+C[i]; i:=i+1; //Продолжение строки символов.
16
16. typ:=3; i:=i+1;
//Распознана строка символов.
В целом программа, реализующая лексический анализатор, содержит цикл,
который повторяется, пока текущее состояние не станет заключительным. На
каждом шаге цикла вычисляется номер столбца для таблиц, по паре (текущее
состояние, номер столбца) из таблицы 5 выбирается номер семантической
программы (процедуры), далее эта программа вызывается на исполнение, после чего
по паре (текущее состояние, номер столбца) из таблицы 5 выбирается новое текущее
состояние.
Для эффективной реализации лексического анализатора номер столбца
вычисляется по заданному в виде констант массиву перекодировки. В нем по
индексу (коду очередного символа входной строки) выбирается номер столбца.
Вызов семантической программы (процедуры) по ее номеру осуществляется с
помощью заранее подготовленного массива указателей. Тогда на каждом шаге
цикла исполняется небольшое количество действий, не зависящее от размера
грамматики и, следовательно, от размера таблиц.
Конец примера.
Замечание. Рассмотренный пример не претендует на полноту, он показывает
лишь, как можно разработать эффективный лексический анализатор для реального
языка программирования.
17
4. СИНТАКСИЧЕСКИЙ АНАЛИЗ
4.1. Дерево порождения для КС-грамматики
В соответствии с классификацией по Хомскому грамматика является
контекстно-свободной, если все порождающие правила имеют вид:
A → γ,
где A – нетерминальный символ, γ – цепочка из терминальных и нетерминальных
символов.
Процесс порождения в КС-грамматике наглядно представляется в виде дерева
порождения – дерева с корнем вверху. Вершинами дерева служат терминальные и
нетерминальные символы. Порождение начинается с одного из правил, в левой
части которого находится начальный нетерминал. Поэтому в корень дерева всегда
помещается начальный нетерминал, вниз от него идут ребра, в концах которых
размещаются символы правой части правила. При этом должен сохраняться порядок
следования вершин в концах ребер. Далее от каждой из вершин, являющейся
нетерминалом, достраиваются вниз ребра с вершинами – символами в правой части
примененного порождающего правила. Если какое-то из примененных правил
содержит пустую правую часть, то ребро заканчивается символом пустой цепочки.
Этот процесс продолжается до тех пор, пока среди висячих вершин дерева не
останется хотя бы один нетерминал. После этого обход слева направо по висячим
вершинам дерева будет цепочкой языка, получившейся в процессе порождения.
Каждому построенному дереву порождения соответствует некоторое левое
каноническое порождение и наоборот. Поэтому справедливо следующее
утверждение: если для порождения некоторой цепочки языка возможно построение
двух (или более) различных деревьев порождения, то грамматика является
неоднозначной. При этом неоднозначность грамматики не означает
неоднозначности порождаемого ею языка: язык неоднозначен, если для него не
существует ни одной однозначной грамматики. К сожалению, в общем случае
доказать или опровергнуть неоднозначность языка является алгоритмически
неразрешимой задачей. Понятно, что язык программирования должен быть
однозначен, поэтому при составлении для него конкретной грамматики достаточно
установить однозначность грамматики (а не языка), что обычно вполне выполнимо.
4.2. Автомат с магазинной памятью
Грамматический разбор цепочки символов для КС-грамматики сводится к
построению дерева порождения (или левого канонического разбора) для этой
цепочки. Для осуществления такого разбора будем использовать алгоритм
распознавания, называемый автоматом с магазинной памятью (МПА). Также как
КА, он прочитывает входную цепочку слева направо. В отличие от КА, МПА
содержит дополнительную память, работающую по принципу магазина (стека).
Автомат с магазинной памятью задается семеркой множеств:
{Σ, Q, q0, F, Γ, Z0, δ},
18
где Σ – множество (алфавит) входных символов; Q – множество состояний МПА;
q0 – начальное состояние, q0  Q ; F – множество заключительных состояний,
F  Q ; Γ – множество (алфавит) магазинных символов; Z0 – начальный магазинный
символ, Z 0   ; δ – множество правил перехода.
Действие каждого из правил перехода определяется входным символом,
верхним символом магазина и текущим состоянием МПА. Действие может
содержать в различных сочетаниях такие шаги: а) удаление из магазина верхнего
символа, б) запись в магазин нескольких символов, в) переход к чтению следующего
входного символа, г) изменение текущего состояния. Так, запись правила перехода в
виде:
(a, B, qi) → (λ, γB, qj),
где a  , qi  Q, q j  Q , а символ B и символы из цепочки γ все принадлежат
алфавиту Γ, означает, что в магазин, содержащий верхний символ B, записывается
цепочка символов γ, текущее состояние становится qj , после чего делается переход к
чтению следующего входного символа. Правило:
(a, B, qi) → (a, λ, qj),
означает, что из магазина удаляется верхний символ B, текущее состояние
становится qj , переход к чтению следующего входного символа не производится.
До начала функционирования МПА в магазине имеется начальный магазинный
символ, МПА находится в начальном состоянии. На каждом шаге работы читается
очередной символ входной цепочки, начиная с первого, читается верхний символ
магазина, и выполняются действия по такому правилу перехода, которое
соответствует этим символам и текущему состоянию. Работа МПА заканчивается,
когда не будет ни одного подходящего правила, чтобы можно было продолжать
работу. Если при этом выполнены три условия: цепочка прочитана вся, магазин
пуст, МПА находится в заключительном состоянии, то цепочка считается
распознанной. Если же хотя бы одно из этих условий нарушено, цепочка считается
нераспознанной. Цепочка считается нераспознанной и в том случае, если МПА
никак не может остановиться, т.е. когда он зациклил, и нет продвижения по входной
цепочке.
МПА будет детерминированным (ДМПА), если на каждом шаге возможно
применение не более одного правила перехода. Если же на каком-либо шаге можно
выполнить действия по двум или более правилам перехода, то МПА будет
недетерминированным (НМПА), тогда он должен выполнять действия в
соответствии со всеми этими правилами параллельно. Гипотетически такое
одновременное выполнение нескольких переходов можно представить так:
Создаются копии МПА в количестве, соответствующем количеству одновременно
выполняемых правил, после чего каждая копия независимо от других выполняет
действия по своему правилу. Некоторые из копий могут попадать в тупики, их
работа заканчивается, но это не влияет на работу других копий. Если, в конце
концов, хотя бы одна из копий распознает входную цепочку, то считается, что
входная цепочка распознана недетерминированным МПА. Входная цепочка
считается нераспознанной, если ни одна из копий МПА не распознала цепочку.
Таким образом, НМПА проверяет все возможные варианты действий на каждом
19
шаге работы. Следует заметить, что количество всех работающих копий может
оказаться таким большим, что превысит любое наперед заданное число, поэтому
функционирование НМПА можно представить чисто гипотетически.
При грамматическом разборе дерево порождения может строиться двумя
основными способами: сверху вниз и снизу вверх. В обоих случаях входная цепочка
символов читается слева направо. При разборе сверху вниз вначале строится корень
дерева – начальный нетерминал. Затем, в соответствии с левым каноническим
порождением, подбираются такие порождающие правила, чтобы, в конце концов,
висячие вершины дерева совпали с символами входной цепочки. Такой вариант
разбора называют LL-разбором (от англ. left – левый), так как цепочка символов
читается слева направо, а дерево строится, как при левом каноническом
порождении.
При разборе снизу вверх дерево строится снизу – начиная от символов входной
цепочки. При этом, в соответствии с порождающими правилами на очередных
шагах производится сворачивание – переход от правой части правила к нетерминалу
левой части. Полностью дерево получается на самом последнем шаге, когда в нем
будет построен корень. Здесь мы не будем рассматривать алгоритмы разбора снизу
вверх, хотя они тоже используются на практике.
4.3. LL-разбор КС-грамматики автоматом с магазинной памятью
Пусть задана КС-грамматика. Построим по ней МПА, выполняющий
грамматический LL-разбор. Множество входных символов Σ в МПА будет
совпадать с множеством терминальных символов грамматики. Множество
состояний Q будет состоять из единственного состояния q0, оно же будет начальным
и заключительным. Множество магазинных символов Γ будет содержать все
терминальные и нетерминальные символы грамматики: Γ = Σ U N. Начальный
магазинный символ Z0 будет совпадать с начальным символом грамматики S.
Правила перехода δ для МПА будем строить следующим образом. Для каждого
порождающего правила вида A → γ будет правило перехода:
(λ, A, q0) → (λ, γ–1, q0),
(*)
–1
где γ – правая часть правила γ в обратном порядке, а для каждого терминального
символа a правило перехода:
(a, a, q0) → (λ, λ, q0).
(**)
Этот МПА будет повторять левое каноническое порождение. Действительно, до
начала работы в магазине находится единственный символ S – начальный символ
грамматики, который при построении дерева помещается в корень. Далее S в
магазине заменяется символами правой части правила порождения (по правилу вида
(*)), при занесении в магазин их можно одновременно помещать в дерево
порождения. Как только на вершине магазина появится терминальный символ, и
если он совпадет с очередным входным символом, то он удаляется из магазина (по
правилу вида (**)), при этом далее будет читаться следующий входной символ.
Таким образом, МПА полностью повторяет левое каноническое порождение, и если
получающиеся при этом терминальные символы совпадают с символами входной
цепочки, то в конце работы магазин будет пуст, и вся цепочка прочитана. Это
20
значит, что входная цепочка распознана. Если же входная цепочка не принадлежит
языку, т.е. для нее не существует левого канонического порождения, то на какомлибо шаге работы верхний в магазине терминальный символ не совпадет с
очередным входным символом, и МПА попадет в тупик.
Обычно в КС-грамматике имеются группы различных порождающих правил с
одним и тем же нетерминалом в левой части. Тогда МПА будет
недетерминированным, в этом случае он должен параллельно проверять все
варианты левого канонического порождения. Если при этом некоторые из вариантов
будут попадать в тупики, то работа по ним будет прекращаться. Если при этом хотя
бы при одном варианте левого канонического порождения разбор дойдет до конца
входной цепочки и стек будет пуст, то это значит, что найден такой вариант разбора,
который соответствует этой входной цепочке.
Таким образом, входная цепочка будет распознана тогда и только тогда, когда
для нее существует левое каноническое порождение. Заметим, что на КСграмматику не накладывалось никаких дополнительных ограничений, в частности,
она может быть даже неоднозначной. В этом случае НМПА отследит все варианты
левого канонического порождения входной цепочки.
Пример 5. Пусть задана грамматика простых арифметических выражений (S –
начальный нетерминал):
S→S+T|T
T→T*F|F
F → (S) | a
Пусть цепочка на входе: a + a. В табл. 6 представлен тот вариант шагов LLразбора, который приводит к успешному распознаванию.
Табл. 6
№ Входные Содержимое Порождающее
шага символы
магазина
правило
1
a+a
S
S→S+T
2
a+a
S+T
S→T
3
a+a
T+T
T→F
4
a+a
F+T
F→a
5
a+a
a+T
6
+a
+T
7
a
T
T→F
8
a
F
F→a
9
a
a
10 λ
λ
В табл. 6 не приведены другие варианты шагов разбора, которые приводят в
тупик или зацикливают. Для этого примера неудачных вариантов оказывается
бесконечно много. Так, при применении переходов с многократным повторением
одного и того же порождающего правила S → S + T содержимое магазина будет
неограниченно увеличиваться: S, S + T, S + T + T, S + T + T + T, … Применение
21
переходов с порождающими правилами S → T, T → F, F → a приведет к тому, что
содержимое магазина будет изменяться так: S, T, F, a, λ. После этого во входной
цепочке останутся непросмотренными символы: + a, т.е. возникнет тупик.
Конец примера.
4.4. Левая рекурсия и ее устранение
Правило порождения вида A → Aγ в КС-грамматике называется
леворекурсивным, и тогда говорят, что в грамматике имеется левая рекурсия. В
дальнейшем будем считать, что грамматика приведенная, т.е. в ней обнаружены и из
нее удалены все бесполезные нетерминалы.
При функционировании МПА, построенного по КС-грамматике с левой
рекурсией, когда имеется правило порождения вида A → Aγ, выполнение
соответствующего правила перехода (λ, A, q0) → (λ, Aγ–1, q0) будет повторяться
бесконечно. Для НМПА это не страшно, так как другие варианты выполнения
НМПА все равно найдут (если оно существует) левое каноническое порождение
анализируемой цепочки символов. Если же требуется построить ДМПА, который
может проверять только единственный вариант левого канонического порождения,
такая ситуация недопустима. Поэтому рассмотрим устранение левой рекурсии без
изменения порождаемого грамматикой языка.
Пусть имеется правило вида A → Aγ. Но тогда должно быть также правило вида
A → α, где первый символ цепочки α не совпадает с A, так как в противном случае
символ A – бесполезный. Цепочки символов, порождаемые этими двумя правилами
следующие: α, αγ, αγγ и т.д. Эти же цепочки можно получить другими правилами:
A → αA’, A’ → γA’| λ, где A’ – новый нетерминал.
Рассмотренный случай называют непосредственной рекурсией, ее легко
обнаружить. Более сложен случай косвенной рекурсии, когда имеется совокупность
правил вида A → B1γ1, B1 → B2γ2, …, Bn → Aγn. Тогда вначале косвенную рекурсию
нужно свести к непосредственной, сделав следующую замену этой группы правил
на правило: A → Aγn … γ2γ1. При этом надо учесть и другие совокупности правил,
аналогичные рассмотренным, в которые входят какие-либо из нетерминалов B1, …,
Bn.
Пример 6. Пусть задана грамматика простых арифметических выражений (S –
начальный нетерминал):
S→S+T|T
T→T*F|F
F → (S) | a
Устранив левую рекурсию рассмотренным способом, получим правила:
S → TU
U → + TU | λ
T → FV
V → * FV | λ
F → (S) | a
Конец примера.
22
4.5. Преобразование КС-грамматики к обобщенной нормальной форме Грейбах
КС-грамматика представлена в нормальной форме Грейбах, если правые части
всех ее порождающих правил начинаются с терминала. Обобщенная нормальная
форма Грейбах допускает, кроме того, пустые правые части правил. Пусть задана
приведенная КС-грамматика, в которой устранена (если она была) левая рекурсия.
Преобразование начинается с нахождения тех порождающих правил, правые
части которых начинаются с терминалов или пусты. Эти порождающие правила
остаются без изменения. Пусть имеется правило вида A → Bα, тогда нетерминал B
надо заменить совокупностью правых частей правил B → γ1 | … | γn. В результате
получатся правила:
A → γ1α | … | γnα.
Если какие-либо из цепочек γ1α, …, γnα начинаются с нетерминального
символа, то аналогичную замену правых частей правил для нетерминала A следует
продолжать. Так как леворекурсивных правил в грамматике нет, то такой процесс
обязательно закончится, и в преобразованных правилах правые части будут
начинаться с терминальных символов.
После этого надо отыскать оставшиеся правила вида A’ → B’α’, и продолжить
процесс замен. Когда все подобные замены будут сделаны, все правые части правил
будут начинаться с терминальных символов или будут пустыми.
Заметим, что при устранении левой рекурсии и преобразования грамматики к
обобщенной нормальной форме Грейбах на КС-грамматику не накладывались
никакие дополнительные ограничения, из чего следует, что любой язык представим
в КС-грамматике без левой рекурсии и, более того, в обобщенной нормальной
форме Грейбах.
Пример 7. Пусть задана грамматика простых арифметических выражений с
устраненной левой рекурсией:
S → TU
U → + TU | λ
T → FV
V → * FV | λ
F → (S) | a
После ее преобразования к обобщенной нормальной форме Грейбах получим:
S → (S)VU | aVU
U → + TU | λ
T → (S)V | aV
V → * FV | λ
F → (S) | a
Конец примера.
4.6. Детерминированный LL-разбор
КС-грамматика, преобразованная к обобщенной нормальной форме Грейбах,
допускает однозначный LL-разбор, если для каждой группы порождающих правил с
23
одним и тем же нетерминалом в левой части правые части будут однозначно
различимы по нескольким первым терминальным символам. В самом простом
случае они должны различаться по одному символу, тогда на каждом шаге разбора
проверяется на совпадение один верхний символ магазина (если он терминал) и
очередной символ на входе. Такой разбор называется LL(1)-анализом, а алгоритм
разбора – LL(1)-анализатором.
Будем считать, что входная цепочка символов, также как для лексического
анализатора, всегда заканчивается ограничителем ┴. Для работы LL(1)-анализатора
необходимо построить таблицу, в которой столбцы помечены терминальными
символами (включая ┴), а строки – нетерминалами преобразованной грамматики.
Для каждого из правил вида A → aγ, правая часть заносится на пересечение строки,
помеченной нетерминалом A, и столбца, помеченного терминалом a. Если же правая
часть правила пустая, то во все клетки строки, не занятые другими правилами,
записывается λ.
До начала работы в магазин записывается ограничитель ┴ , а затем начальный
нетерминал. На каждом шаге анализатор проверяет, допустим ли очередной входной
символ, и если да, то выполняет одно из двух действий:
1) если на вершине магазина нетерминал, то, в зависимости от того, каков
очередной входной символ, этот нетерминал заменяется символами правой части
соответствующего правила, причем символы записываются в обратном порядке.
Если для очередного входного символа в таблице записано λ, то нетерминал
удаляется из магазина, а если в таблице пустая клетка, то анализатор прекращает
цикл из-за ошибочного символа во входной цепочке;
2) если на вершине магазина терминал, то он сравнивается с очередным
входным символом. При совпадении терминал удаляется из магазина и делается
переход к следующему символу входной цепочки. При несовпадении анализатор
прекращает цикл из-за ошибочного символа во входной цепочке.
Цикл прекращается, когда входная цепочка символов оказалась вся
просмотрена. Если при этом магазин пуст, то входная цепочка символов считается
распознанной, если не пуст – то нераспознанной.
Пример 8. Пусть задана грамматика простых арифметических выражений,
преобразованная к обобщенной нормальной форме Грейбах:
S → (S)VU | aVU
U → + TU | λ
T → (S)V | aV
V → * FV | λ
F → (S) | a
В табл. 7 приведены действия LL(1)-анализатора.
24
Табл. 7
S
U
T
V
F
+
*
+ TU
λ
λ
* FV
(
(S)VU
λ
(S)V
λ
(S)
)
λ
λ
a
aVU
λ
aV
λ
a
┴
λ
λ
Пусть на вход анализатора поступает цепочка: a * (a + a) ┴ . Шаги работы
анализатора представлены в табл. 8.
Табл. 8
№
Входные Содержимое Порождающее
шага символы
магазина
правило
1
a*(a+a) ┴
S┴
S → aVU
2
a*(a+a) ┴
aVU ┴
3
*(a+a) ┴
VU ┴
V → * FV
4
*(a+a) ┴
*FVU ┴
5
(a+a) ┴
FVU ┴
F → (S)
6
(a+a) ┴
(S)VU ┴
7
a+a) ┴
S)VU ┴
S → aVU
8
a+a) ┴
aVU)VU ┴
9
+a) ┴
VU)VU ┴
V→λ
10
+a) ┴
U)VU ┴
U → + TU
11
+a) ┴
+TU )VU ┴
12
a) ┴
TU )VU ┴
T → aV
13
a) ┴
aVU )VU ┴
14
)┴
VU )VU ┴
V→λ
15
)┴
U )VU ┴
U→λ
16
)┴
)VU ┴
17
VU ┴
V→λ
┴
18
U┴
U→λ
┴
19
┴
┴
Пусть на вход анализатора поступает ошибочная цепочка: a) ┴ . Шаги работы
анализатора представлены в табл. 9.
Табл. 9
№
Входные Содержимое Порождающее
шага символы
магазина
правило
1
a) ┴
S┴
S → aVU
2
a) ┴
aVU ┴
3
)┴
VU ┴
V→λ
4
)┴
U┴
U→λ
5
)┴
<ошибка>
┴
Конец примера.
25
5. ОБРАТНАЯ ПОЛЬСКАЯ СТРОКА, КАК ПРОМЕЖУТОЧНАЯ ФОРМА
ПРОГРАММЫ
5.1. Обратная польская строка для арифметических выражений
Обратная польская строка (ОПС) является бесскобочной записью
арифметических выражений. В отличии от обычной (инфиксной) записи, когда знак
двуместной операции записывается между операндами, участвующими в операции,
в ОПС вначале пишутся операнды, а затем – знак операции. При этом операнды
могут быть простыми (переменными, константами), или сложными, т.е.
результатами других операций. В табл. 10 приведены примеры формул в обычной
записи и в виде ОПС.
Табл. 10
№
Формула
ОПС
1
a+b
ab+
2
a*(c + d)
acd+*
3
(x + y)*(a*x – b*y)
xy+ax*by*–*
Если в обычной записи формул необходимо учитывать приоритеты операций,
то порядок исполнения действий в ОПС полностью определяется местоположением
в ней знаков операций. Следует учесть, что при переходе от формулы к ОПС
порядок простых операндов не изменяется.
В записи ОПС могут использоваться операции с различным числом операндов:
с одним, двумя, тремя и т.д., и даже без операндов. При этом количество операндов
определяется знаком операции. Поэтому, в частности, операция «унарный минус» и
«бинарный минус» должны обозначаться различающимися знаками.
Вычисление по заданной ОПС можно выполнить интерпретатором,
использующим магазин. В магазине сохраняются значения операндов и результаты
вычислений. Принцип работы интерпретатора следующий. ОПС просматривается
слева направо. Если очередной элемент в ОПС – операнд, то его значение
записывается в магазин. Если очередной элемент в ОПС – операция, то из магазина
считываются операнды для этой операции, после чего операция выполняется, а ее
результат записывается в магазин. Если ОПС корректная, то после всех действий в
магазине будет записано единственное значение – результат вычислений.
Пример 9. Дана формула a*(c + d). В табл. 11 приведены шаги алгоритма
вычисления ОПС.
26
№
Входные
шага
символы
1
acd+*
2
cd+*
3
d+*
4
+*
5
*
6
Конец примера.
Табл. 11
Содержимое
магазина
a
a c
a c d
a c+d
a*(c+d)
Пример 10. Дана формула (x + y)*(a*x – b*y). В табл. 11 приведены шаги
алгоритма вычисления ОПС.
Табл. 12
№
Входные
Содержимое
шага
символы
магазина
1
xy+ax*by*–*
2
y+ax*by*–*
x
3
+ax*by*–*
x y
4
ax*by*–*
x+y
5
x*by*–*
x+y a
6
*by*–*
x+y a x
7
by*–*
x+y a*x
8
y*–*
x+y a*x b
9
*–*
x+y a*x b y
10 – *
x+y a*x b*y
11 *
x+y a*x– b*y
12
(x+y)*(a*x–b*y)
Конец примера.
5.2. Генерация ОПС для арифметических выражений
Преобразование арифметического выражения в ОПС можно выполнить в
процессе работы анализатора. Например, в LL(1)-анализаторе для этого необходимо
кроме основного магазина, куда в процессе работы записываются правые части
порождающих правил, использовать синхронно работающий с ним дополнительный
магазин. В этот дополнительный магазин будем записывать последовательность
семантических действий, генерирующих элементы ОПС. Сами же действия по
генерации ОПС будут выполняться при извлечении элементов из дополнительного
магазина.
Для реализации такого алгоритма наряду с основной таблицей действий LL(1)анализатора необходимо задать семантическую таблицу. Ее размеры в точности
27
совпадают с основной таблицей, более того, для каждой непустой клетки основной
таблицы, где находится правая часть какого-либо правила порождения, в
семантической таблице помещается последовательность действий, количество
которых равно длине правой части.
При этом надо учесть, что терминальные символы в КС-грамматике на самом
деле являются лексемами, распознаваемыми лексическим анализатором. Некоторые
из лексем, например имена переменных и константы, содержат дополнительную
семантическую информацию – ссылки на таблицы переменных или таблицы
констант, т.е. одинаковые терминальные символы в КС-грамматике семантически
могут различаться.
Удобнее всего показать использование семантической таблицы на примере.
Пример 11. Пусть задана грамматика простых арифметических выражений,
преобразованная к обобщенной нормальной форме Грейбах, та же самая, что в
примере 8.
Табл. 13 в каждой клетке содержит действия LL(1)-анализатора, а ниже –
синхронно выполняемые с ними семантические действия по генерации ОПС. В них
знаком □ отмечены пустые действия, символом a – запись в ОПС операнда из
входной цепочки (переменной или константы), символы + и * обозначают запись в
ОПС знаков операций. Сами эти действия будут выполняться в момент
выталкивания их из дополнительного магазина.
Табл. 13
+
*
(
)
a
┴
S
(S)VU
aVU
□□□□□
a□□
U + TU
λ
λ
λ
λ
λ
□□+
T
(S)V
aV
□□□□
a□
V
λ
* FV
λ
λ
λ
λ
□□*
F
(S)
a
□□□
a
Так как одинаковые терминальные символы a в КС-грамматике семантически
могут различаться, будем во входной цепочке различные операнды обозначать
разными символами: a, b и др., при этом все они будут соответствовать одному и
тому же терминальному символу a. При этом операнды могут быть как именами
переменных, так и константами.
Лексический анализатор, непосредственно анализирующий входную цепочку
символов, на каждом шаге работы LL(1)-анализатора выдает ему тип лексемы. Если
эта лексема – имя, а в магазине верхний символ a, то анализатор ищет это имя в
таблице переменных, и если оно там будет найдено, то генерируется в ОПС тип
переменной и ее номер в таблице. Если же такого имени в таблице нет, то это
28
ошибка в анализируемом тексте программы, и анализатор должен выдать об этом
диагностическое сообщение. Если очередная лексема – константа, то ее значение
записывается в таблицу констант, а ее тип и номер в таблице констант генерируется
в ОПС.
Пример работы LL(1)-анализатора с генерацией ОПС для входной цепочки a*(c
+ d) ┴ представлен в табл. 14.
Табл. 14
№
Входные Содержимое Дополнит. Порождающее
ОПС
шага символы
магазина
магазин
правило
1
a*(c + d) ┴
S┴
□□
S → aVU
2
a*(c + d) ┴
aVU ┴
a□□□
a
3
*(c + d) ┴
VU ┴
□□□
V → * FV
a
4
*(c + d) ┴
*FVU ┴
□□*□□
a
5
(c + d) ┴
FVU ┴
□*□□
F → (S)
a
6
(c + d) ┴
(S)VU ┴
□□□*□□
a
7
c + d) ┴
S)VU ┴
□□*□□
S → aVU
a
8
c + d) ┴
aVU)VU ┴
a□□□*□□
ac
9
+ d) ┴
VU)VU ┴
□□□*□□
V→λ
ac
10
+ d) ┴
U)VU ┴
□□*□□
U → + TU
ac
11
+ d) ┴
+TU )VU ┴ □□+□*□□
ac
12
d) ┴
TU )VU ┴
□+□*□□
T → aV
ac
13
d) ┴
aVU )VU ┴ a□+□*□□
acd
14
)┴
VU )VU ┴
□+□*□□
V→λ
acd
15
)┴
U )VU ┴
+□*□□
U→λ
acd+
16
)┴
)VU ┴
□*□□
acd+
17
VU ┴
*□□
V→λ
acd+*
┴
18
U┴
□□
U→λ
acd+*
┴
19
□
acd+*
┴
┴
Рассмотренная грамматика простых арифметических выражений позволяет
задавать формулы не только с операциями + и *, но и с некоторыми другими
операциями. Тогда под обозначением + надо понимать и другие операции с таким
же приоритетом, в частности, операцию – (минус), а под обозначением * еще и
операцию / (делить). Заметим, что в заданной грамматике операция + имеет
меньший приоритет, чем операция *. При этом конкретная операция, которая
генерируется в ОПС, должна переписываться из анализируемой цепочки в
дополнительный магазин, а уже оттуда – в ОПС.
Эту грамматику можно расширить для того, чтобы можно было записывать
унарные операции (+ и –) перед операндом. Для этого достаточно добавить еще
один нетерминал и порождающие правила:
F → +G
G → (S) | a
При работе анализатора по правилу F → +G в магазин следует записывать
цепочку: +G□, а в дополнительный магазин: □□+, где под знаком + следует
29
понимать группу унарных операций (+ и –), которые должны отличаться от
бинарных.
Конец примера.
5.3. Вычисление ОПС для присваиваний и арифметических выражений с
индексами
Присваивание можно определить как операцию, у которой первый операнд
должен быть переменной, а второй – арифметическим выражением. При этом
значение переменной хранится в некоторой области памяти (например, в таблице
переменных), а результатом операции должно быть изменение этого значения. В
отличие от других операций, после вычисления операции присваивания результат
операции не должен сохраняться в магазине, т.е. магазин будет пустым.
Чтобы интерпретатор правильно вычислял как арифметические операции, так и
операцию присваивания, для каждой ячейки магазина требуется хранить признак,
указывающий, что в ней хранится – переменная или значение. Если на вход
интерпретатора поступает операнд-константа, то должно записываться значение, а
если переменная – то ссылка на ее размещение (например, на таблицу переменных).
При поступлении на вход интерпретатора этот признак должен учитываться, и
значения для операции должны либо извлекаться из магазина, либо по ссылке из
таблицы переменных.
Пример 12. Дано присваивание x := a*(c + d). ОПС для него:
x a c d + *:=
В табл. 15 приведены шаги алгоритма вычисления ОПС.
Табл. 15
№
Входные
Содержимое
шага
символы
магазина
1
x a c d + * :=
2
a c d + * :=
x
3
c d + * :=
x a
4
d + * :=
x a c
5
+ * :=
x a c d
6
* :=
x a c+d
7
:=
x a*(c+d)
8
Конец примера.
Выражения с индексами необходимы для доступа к элементам массивов. Для
одномерного массива операция индексирования вычисляется следующим образом.
Первый операнд такой операции – ссылка на таблицу переменных, где находится
описание (паспорт) массива. В паспорте, в свою очередь, есть ссылка на
расположение начального элемента массива с индексом 0, количество элементов,
длина или тип каждого элемента. Второй операнд операции индексирования –
30
значение индекса. Результатом операции является ссылка на индексируемый
элемент массива. При этом вычисление ведется по формуле:
M + d*i,
где M – ссылка на элемент массива с индексом 0, d – длина элемента массива, i –
значение индекса.
Для двумерного массива операция индексирования требует трех операндов:
1) ссылка на паспорт массива, 2) значение индекса по первому измерению,
3) значение индекса по второму измерению. При этом вычисление ведется по
формуле:
M + d*(i*m + j),
где M – ссылка на элемент массива с индексом 0, d – длина элемента массива, m –
количество элементов в массиве по второму измерению, i , j – значения индексов по
первому и второму измерению соответственно.
По аналогичным формулам проводится вычисление в операции индексирования
по трем и более индексам. Далее в примерах операцию индексирования по одному
индексу будем обозначать <i>, а индексирование по двум индексам – <i2>.
При выполнении операции индексирования перед вычислением ссылки на
индексируемый элемент массива можно выполнить проверку корректности
индексов, и при выходе их за пределы массива останавливать вычисления с
сигнализацией ошибки.
Пример 13. Дано присваивание с индексацией M[i] := a*L[i, j + d]. ОПС для
него:
M i <i> a L i j d + <i2> * :=
В табл. 16 приведены шаги алгоритма вычисления ОПС. Для сокращения
таблицы шаги 5 – 8 в ней пропущены.
Табл. 16
№
Входные
Содержимое
шага
символы
магазина
1
M i <i> a L i j d + <i2> * :=
2
i <i> a L i j d + <i2> * :=
M
3
<i> a L i j d + <i2> * :=
M i
4
a L i j d + <i2> * :=
M[i]
9
+ <i2> * :=
M[i] a L i j d
10 <i2> * :=
M[i] a L i j+d
11 * :=
M[i] a L[i, j+d]
12 :=
M[i] a*L[i, j + d]
13
Конец примера.
31
5.4. Генерация ОПС для присваиваний и арифметических выражений с
индексами
Вначале надо расширить грамматику арифметических выражений так, чтобы из
начального нетерминала порождалось присваивание, и чтобы операндами могли
быть индексируемые элементы массивов.
Пример 14. Грамматика присваиваний и арифметических выражений с
индексами для одно- и двумерных массивов (A – начальный нетерминал) будет
такой:
A → aH := S
S→S+T|T
T→T*F|F
F → (S) | aH
H → [S] | [S, S] | λ
Последняя группа порождающих правил (для нетерминала H) вносит
неопределенность: две различные правые части начинаются с одного и того же
терминального символа. Поэтому заменим эту группу правил на следующие
правила, добавив в грамматику новый нетерминал:
H → [SK | λ
K → ] | , S]
После этого в грамматике устраним левую рекурсию и преобразуем ее к
обобщенной нормальной форме Грейбах, в результате получим правила:
A → aH := S
S → (S)VU | aHVU
U → + TU | λ
T → (S)V | aHV
V → * FV | λ
F → (S) | aH
H → [SK | λ
K → ] | , S]
В табл. 17 представлен построенный по этим правилам LL(1)-анализатор,
дополненный семантическими действиями для генерации ОПС. При этом правая
часть правила A → aH := S в конце дополнена пустым символом □ для того, чтобы
знак операции := генерировался после ОПС для выражения в правой части
присваивания.
32
+
[
]
,
λ
a
aH:=S□
a□□□:=
aHVU
a□□□
λ
Табл. 17
:=
┴
λ
λ
λ
λ
λ
λ
aHV
a□□
λ
λ
λ
λ
λ
λ
λ
aH
a□
λ
[SK
□□□
λ
λ
λ
λ
*
(
)
λ
(S)VU
□□□□□
λ
A
S
U
+TU
□□+
T
V
λ
*FV
□□*
F
H
λ
λ
(S)V
□□□□
λ
(S)
□□□
λ
K
]
, S]
<i> □□<i2>
Пусть на вход анализатора поступает цепочка M[i] := a*L[i, j + d]. Шаги ее
анализа и генерации ОПС представлены в табл. 18. Для сокращения таблицы из нее
удалены некоторые шаги, при выполнении которых подряд удаляются из магазина
несколько нетерминалов.
Табл. 18
№
Входные
Содержимое Дополнит.
ОПС
шага
символы
магазина
магазин
1 M[i] := a*L[i, j + d]┴ A ┴
□□
2 M[i] := a*L[i, j + d]┴ aH:=S□┴
a□□□:=□
M
3 [i] := a*L[i, j + d]┴ H:=S□┴
□□□:=□
M
4 [i] := a*L[i, j + d]┴ [SK:=S□┴
□□□□□:=□
M
5 i] := a*L[i, j + d]┴
SK:=S□┴
□□□□:=□
M
6 i] := a*L[i, j + d]┴
aHVUK:=S□┴ a□□□□□□:=□ M i
7 ] := a*L[i, j + d]┴
HVUK:=S□┴ □□□□□□:=□ M i
10 ] := a*L[i, j + d]┴
K:=S□┴
□□□:=□
Mi
11 ] := a*L[i, j + d]┴
]:=S□┴
<i>□□:=□
M i <i>
12 := a*L[i, j + d]┴
:=S□┴
□□:=□
M i <i>
13 a*L[i, j + d]┴
S□┴
□:=□
M i <i>
12 a*L[i, j + d]┴
aHVU□┴
a□□□:=□
M i <i> a
13 *L[i, j + d]┴
HVU□┴
□□□:=□
M i <i> a
14 *L[i, j + d]┴
VU□┴
□□:=□
M i <i> a
15 *L[i, j + d]┴
*FVU□┴
□□*□:=□
M i <i> a
16 L[i, j + d]┴
FVU□┴
□*□:=□
M i <i> a
17 L[i, j + d]┴
aHVU□┴
a□*□:=□
M i <i> a L
18 [i, j + d]┴
HVU□┴
□*□:=□
M i <i> a L
33
19
20
21
22
25
26
27
28
29
30
32
33
34
35
36
38
39
40
42
43
[i, j + d]┴
i, j + d]┴
i, j + d]┴
, j + d]┴
, j + d]┴
, j + d]┴
, j + d]┴
j + d]┴
j + d]┴
+ d]┴
+ d]┴
+ d]┴
d]┴
d]┴
]┴
]┴
]┴
┴
[SKVU□┴
SKVU□┴
aHVUKVU□┴
HVUKVU□┴
KVU□┴
KVU□┴
, S]VU□┴
S]VU□┴
aHVU]VU□┴
HVU]VU□┴
U]VU□┴
+TU]VU□┴
TU]VU□┴
aHVU]VU□┴
HVU]VU□┴
U]VU□┴
]VU□┴
VU□┴
□┴
┴
┴
┴
□□□*□:=□
□□*□:=□
a□□□□*□:=□
□□□□*□:=□
□*□:=□
□*□:=□
□□<i2>*□:=□
□<i2>*□:=□
a□□□<i2> …
□□□<i2> …
□<i2>*□:=□
□□+<i2>…
□+<i2>*□:=□
a□□+<i2>…
□□+<i2>…
+<i2>*□:=□
<i2>*□:=□
*□:=□
:=□
□
M i <i> a L
M i <i> a L
M i <i> a L i
M i <i> a L i
M i <i> a L i
M i <i> a L i
M i <i> a L i
M i <i> a L i
M i <i> a L i j
M i <i> a L i j
M i <i> a L i j
M i <i> a L i j
M i <i> a L i j
M i <i> a L i j d
M i <i> a L i j d
M i <i> a L i j d +
M i<i>a L i j d+<i2>
M i<i>a L i j d+<i2>*
M i<i>a Li j d+<i2>*:=
M i<i>a Li j d+<i2>*:=
Конец примера.
5.5. ОПС для условных, циклических и составных операторов
В условных и циклических операторах должно вычисляться логическое
выражение. В самом простом случае это операция сравнения двух арифметических
выражений. Всего операций сравнения шесть: =, ≠, <, >, <=, >=. ОПС для любой
операции сравнения записывается и выполняется так же, как арифметическая
операция, отличие лишь в том, что результат операции сравнения – логическое
значение true или false. Далее рассмотрим пример грамматики с условными
операторами и генерацию соответствующей ОПС.
Пример 15. Расширим грамматику присваиваний и арифметических выражений
с индексами для одно- и двумерных массивов (A – начальный нетерминал),
рассмотренную в примере 14.
Порождающее правило для выражения, содержащего сравнение, можно
определить с помощью общего обозначения всей группы операций сравнения через
<c>:
C → S<c>S
Вначале преобразуем это правило, добавив новый нетерминал D, для того,
чтобы при входном символе <c> была замена в стеке нетерминала D, на правую
часть, начинающуюся с конкретной операции сравнения:
C → SD
34
D → <c>S
Затем первое правило преобразуем к нормальной форме Грейбах:
C → (S)VUD | aHVUD
При работе анализатора, если верхний символ в магазине будет C, то при
входном символе открывающей скобке в магазин будет копироваться
последовательность: (S)VUD, а при входном символе имя или константа –
последовательность: aHVUD. Соответственно в дополнительный магазин будет
записываться: □□□□□□ или a□□□□.
При верхнем символе D в магазине и при входном символе <c> в магазин будет
копироваться: <c>S□, а в дополнительный магазин: □□<c>, для того, чтобы
операция сравнения записывалась в ОПС после того, как в ОПС полностью
сгенерирован второй операнд.
Условный оператор в полной и сокращенной формах можно определить
порождающими правилами, начинающимися с нетерминала A:
A → if C then AE
E → else A | λ
Эти правила позволяют записывать не только условные операторы с
присваиваниями (так как нетерминал A может порождать присваивания), но и
вложенные условные операторы.
При работе анализатора, если верхний символ в магазине будет A, то при
входном символе if в магазин будет копироваться последовательность: if C then AE,
а в дополнительный магазин: □□<1>□□. Здесь и далее обозначение <1>, <2> и т.д.
означает номер семантической программы, генерирующей ОПС, и которая будет
выполняться при выталкивании этого элемента из дополнительного магазина.
При верхнем символе E в магазине и при входном символе else в магазин будет
копироваться: else A□, а в дополнительный магазин: <2>□<3>. Если же будет
другой входной символ (не else), то в магазин будет копироваться: □, а в
дополнительный магазин: <3>.
Семантические программы будут генерировать такие операнды, как метки
(<m1>, <m2> и т.д.), и операции условного (<jf> – переход при условии false) и
безусловного перехода (<j>). Метка представляет собой номер элемента в
генерируемой ОПС, куда будет производиться переход при выполнении операции
перехода. Для такой генерации будет использоваться еще один магазин – магазин
меток.
Рассмотрим эти семантические программы. В них будет использована
переменная k – счетчик (номер) очередного генерируемого элемента ОПС. Этот
счетчик увеличивается на 1 всякий раз, как только генерируется очередной элемент
ОПС.
Программа <1>.
1. В магазин меток записывается k.
2. В ОПС записывается пустой элемент – место для будущей метки.
3. В ОПС записывается операция <jf> – переход при условии false.
Программа <2>.
1. Через верхний элемент магазина меток, как ссылку на ранее заготовленное
место для метки, записывается k + 2.
35
2. В магазин меток записывается k.
3. В ОПС записывается пустой элемент – место для будущей метки.
4. В ОПС записывается операция <j> – безусловный переход.
Программа <3>.
1. Через верхний элемент магазина меток, как ссылку на ранее заготовленное
место для метки, записывается k.
Конец программ.
Если, например, на входе будет цепочка:
if a>b then a:=b else b:=a ┴
то будет сгенерирована следующая ОПС:
a b > <m1> <jf> a b := <m2> <j> b a :=
↑
↑
<m1> <m2>
Пусть, например, a = 5, b = 2. Тогда при вычислении этой ОПС содержимое
магазина интерпретатора будет изменяться таким образом:
a b
Операция >
true <m1>
Операция <jf> – перехода нет
a b
Операция :=
<m2>
Операция <j> – переход есть
В результате получится a = 2, b = 2 (изменилось a).
Если же a = 2, b = 5, то вычисление будет таким:
a b
Операция >
true <m1>
Операция <jf> – переход есть
b a
Операция :=
В результате получится a = 2, b = 2 (изменилось b).
Конец примера.
Рассмотрим пример грамматики с циклами и генерацию ОПС для них.
Пример 16. Расширим грамматику, рассмотренную в примере 15, задав цикл
порождающим правилом, начинающимся с нетерминала A:
A → while C do A
При работе анализатора, если верхний символ в магазине будет A, то при
входном символе while в магазин будет копироваться последовательность: while C
do A□, а в дополнительный магазин: <4>□<1>□<5>.
36
Рассмотрим семантические программы <4> и <5>.
Программа <4>.
1. В магазин меток записывается k.
Программа <5>.
1. В ОПС записывается метка, значение для которой читается из магазина
меток.
2. В ОПС записывается операция <j> – безусловный переход.
Конец семантических программ.
Если, например, на входе будет цепочка:
while a>b do a:=b ┴
то будет сгенерирована следующая ОПС:
a b > <m1> <jf> a b := <m0> <j>
↑
↑
<m0>
<m1>
Пусть, например, a = 5, b = 2. Тогда при вычислении этой ОПС содержимое
магазина интерпретатора будет изменяться таким образом:
a b
Операция >
true <m1>
Операция <jf> – перехода нет
a b
Операция :=
<m0>
Операция <j> – переход есть
a b
Операция >
true <m1>
Операция <jf> – переход есть
В результате получится a = 2, b = 2 (цикл выполнился один раз, и изменилось b).
Конец примера.
А теперь рассмотрим пример грамматики с составными операторами и
генерацию ОПС для них.
Пример 17. Расширим грамматику, рассмотренную в примере 16, задав
составной оператор порождающими правилами, начинающимися с нетерминала A:
A → begin Q end
Q→ A;Q|λ
Т.е. составной оператор представляет собой последовательность из
произвольного числа таких операторов, как присваивание, условный, цикл,
составной, которые записаны через точку с запятой и взяты в операторные скобки
begin – end.
37
Вторую группу правил (для нетерминала Q) преобразуем к обобщенной
нормальной форме Грейбах:
Q → aH := S ; Q | if C then AE ; Q | while C do A; Q | begin Q end A ; Q | λ
При работе анализатора, если верхний символ в магазине будет Q, то при
входном символе begin в магазин будет копироваться последовательность: begin Q
end ; Q, а в дополнительный магазин: □□□□□. Если же входной символ будет имя
или константа, или if, или while, то в магазин будут копироваться
последовательности, как для присваивания или условного оператора, или цикла,
дополненные двумя символами: ; Q. В дополнительный магазин при этом будут
копироваться соответствующие последовательности, дополненные двумя
символами: □□.
Конец примера.
5.6. ОПС для стандартных операторов
Наряду с рассмотренными типами операторов в языке программирования, как
правило, имеются такие стандартные операторы, как операторы ввода и вывода.
Рассмотрим пример таких операторов.
Пример 18. Расширим грамматику, рассмотренную в примере 17, задав
операторы ввода и вывода порождающими правилами, начинающимися с
нетерминала A:
A → read (aH) | write (S)
При работе анализатора, если верхний символ в магазине будет A, то при
входном символе read в магазин будет копироваться последовательность: read (aH),
а в дополнительный магазин: □□a□<r>. При входном символе read в магазин будет
копироваться последовательность: write (S), а в дополнительный магазин: □□□<w>.
Здесь <r> и <w> обозначают соответственно операции чтения входного значения с
устройства ввода в переменную (возможно, с индексом) и записи значения
арифметического выражения на устройство вывода. Обе такие операции при
вычислении требуют в магазине одного операнда.
Так, например, по входной цепочке:
begin read(i); read(M[i]); write(M[i]*i) end
будет сгенерирована ОПС:
i <r> M i <i> <r> M i <i> i * <r>
При вычислении по этой ОПС содержимое магазина интерпретатора будет
изменяться таким образом:
Операция <r> – чтение (ввод)
i
M
i
Операция <r> – чтение
M[i]
M
Операция <i> – индексирование
i
Операция <i> – индексирование
38
M[i] i
Операция * – умножение
M[i]*i
Операция <w> – запись (вывод)
Конец примера.
5.7. Распределение памяти и описание переменных
Для переменных, описываемых в программе, требуется выделение памяти,
чтобы можно было осуществить выполнение программы. В зависимости от
структуры программы и типа переменных используются различные способы
распределения памяти. Основных способов два: статический и динамический,
причем динамический способ подразделяется на несколько типов.
При статическом способе память распределяется еще в процессе трансляции,
при этом для каждой переменной вычисляется ее размещение в памяти, а при
использовании переменной подставляется ее адрес. Так как современные
операционные системы (ОС), запуская на исполнение программу, выделяют для нее
место в памяти, начиная с некоторой свободной области, то все эти адреса на самом
деле рассчитываются, как относительные, в виде смещения от начала выделяемой
области. Таким способом можно распределять память для глобальных переменных,
которые должны существовать от начала до самого конца выполнения программы.
При этом для каждой глобальной переменной еще в процессе трансляции должна
быть известна ее длина. Такие переменные могут быть простыми (скалярными),
описанными с определенным стандартным типом, а также массивами таких типов,
причем размер массива должен быть константой.
При динамическом способе распределение памяти производится при
выполнении программы. Так, например, глобальному массиву элементов
стандартного типа невозможно выделить память во время трансляции, если размер
массива вычисляется в процессе выполнения программы. Динамический способ
выделения памяти необходим также для переменных, описываемых внутри
процедур и функций, для списочных структур данных и др. Память выделяется
динамически по запросу к ОС, при этом она может освобождаться либо по запросу к
ОС, либо при завершении всей программы.
Рассмотрим в качестве примера статическое распределение памяти для простых
переменных и динамическое – для массивов, размеры которых вычисляются в
процессе выполнения программы. При этом все переменные считаются
глобальными.
Пример 19. Расширим грамматику, рассмотренную в примере 18, переопределив начальный нетерминал, который задает структуру всей программы, включая
описания. Новый начальный нетерминал P определяет программу, состоящую из
совокупности описаний, после которых записана последовательность операторов,
взятая в операторные скобки begin – end:
39
P → int R P | int1 R P | int2 R P | begin Q end
R→aM
M→,aM|;
Здесь в описаниях предусмотрены три типа переменных: целый (int),
одномерный массив целых (int1) и двумерный массив целых (int2). Размеры
массивов определяются в процессе выполнения программы. Для выделения памяти
предусмотрены следующие стандартные операторы:
A → mem1 (a, S) | mem2 (a, S, S)
Оператор mem1 выделяет память одномерному массиву, второй параметр –
размер массива, Оператор mem2 – двумерному массиву, второй и третий
параметры – размеры массива по первому и второму измерению соответственно.
Нумерация элементов массивов – с нуля.
При работе анализатора по этим порождающим правилам в магазин
записываются их правые части, а в дополнительный магазин следующие
последовательности:
Правая часть правила
Запись в доп. магазин
int R P
<11>□□
int1 R P
<12>□□
int2 R P
<13>□□
begin Q end
<14>□<15>
aM
<16>□
,aM
□<16>□
;
□
mem1 (a, S)
□□ a □□<m1>
mem2 (a, S, S)
□□ a □□□□<m2>
Рассмотрим выполняющиеся при этом семантические программы.
Программа <11>.
Переключение на заполнение таблицы переменных типа int (целочисленных), в
таблице будут записываться имена переменных.
Программа <12>.
Переключение на заполнение таблицы переменных типа одномерный массив
int1 (целочисленных), в таблице будут записываться имена массивов.
Программа <13>.
Переключение на заполнение таблицы переменных типа двумерный массив int2
(целочисленных), в таблице будут записываться имена массивов.
Программа <14>.
1. Завершение формирования таблиц переменных.
2. Генерация в ОПС операций выделения памяти блоками для каждого из типов
переменных:
– для типа int – в виде массива целых, каждая переменная в нем занимает
отдельный элемент и ей приписан номер;
– для типа int1 и int2 – в виде массива структур (паспортов), в которых
записывается размерность по одному или двум измерениям соответственно, а также
ссылка на адрес будущего размещения массива в памяти.
40
3. Генерация в ОПС операций обнуления массивов структур для переменных
типа int1 и int2.
Программа <15>.
Генерация в ОПС операций освобождения всех выделенных в процессе
выполнения программы блоков памяти.
Программа <16>.
1. Проверка имени переменной, поступающей из входной цепочки, на
совпадение с именами, ранее занесенными в таблицы. Если есть совпадение, то
сигнализация об ошибке «повторное описание переменной».
2. Если ошибки нет, то добавление имени переменной в одну из таблиц
переменных (на которую было ранее переключение).
Конец семантических программ.
Операции <m1> и <m2>, генерируемые в ОПС при анализе операторов mem1 и
mem2, при работе интерпретатора должны выполнять следующие действия. Вначале
проверяется, не выделялась ли раньше память для переменной, указанной в первом
операнде. Если да, то выдается сообщение об ошибке, а если нет, то выделяется (по
запросу к операционной системе) требуемая память, после чего в паспорт массива
записывается ссылка (адрес) на нулевой элемент массива, а также размерность по
одному или двум измерениям соответственно.
Конец примера.
5.8. Обработка ошибок
При обнаружении синтаксической ошибки в лексеме лексический анализатор
возвращает вместо номера распознанной лексемы номер ошибки. В свою очередь,
LL(1)-распознаватель должен выдать сообщение о характере ошибки и месте
ошибочного символа во входном тексте. Для этого лексический анализатор должен
отслеживать номер строки текста и номер символа в строке. При выдаче
диагностического сообщения нужно выдавать также кусок текста, где обнаружена
ошибка: так ее легче будет найти пользователю.
Аналогичным образом LL(1)-распознаватель должен выдать сообщение, если
лексемы были правильными, а ошибка обнаружена на уровне синтаксиса КСграмматики.
Ошибки могут также обнаружиться при работе семантических программ
распознавателя. Так, например, в ОПС вместо имени переменной записывается ее
тип и порядковый номер в таблице переменных. Но для этого предварительно
необходимо произвести поиск имен переменных в таблицах, сформированных при
анализе описаний переменных. Понятно, что если в программе обнаружится не
описанная ранее переменная, то это должно сигнализироваться, как ошибка. Могут
быть и такие ошибки, как недопустимая операция для типа переменной, к которой
эта операция применяется, например, индексирование простой переменной или
индексирование двумя индексами имени одномерного массива и др.
В любом случае продолжать генерацию ОПС не имеет смысла, и после выдачи
диагностического сообщения трансляцию можно прекратить. Во многих случаях это
41
оправдано, так как современные компьютеры работают в диалоговом режиме,
поэтому не сложно после исправления ошибки снова запустить транслятор. Однако
при первоначальной отладке, когда в программе ошибок бывает очень много, такая
технология не слишком удобна: лучше было бы выдать сообщения обо всех
ошибках сразу, чтобы затем их сразу все можно было бы исправить перед
повторной трансляцией.
Некоторые из видов ошибок можно обнаружить все (или почти все) за один
проход. Это ошибки, обнаруживаемые лексическим анализатором, так как лексемы
выделяются им по отдельности, независимо друг от друга. В то же время ошибки на
уровне синтаксиса КС-грамматики приводят, чаще всего, к невозможности
дальнейшего анализа входного текста. Ошибки, обнаруживаемые семантическими
программами анализатора (при генерации ОПС) не нарушают работу LL-анализа,
поэтому анализ (без генерации ОПС) можно продолжать.
Тогда, при обнаружении первой ошибки лексического анализа можно
прекратить LL-анализ, но повторно запускать лексический анализатор до конца
входного текста для обнаружения других ошибок в лексемах.
При обнаружении первой семантической ошибки LL-анализ можно продолжать
для того, чтобы можно было выявить и другие семантические ошибки.
Запуск программы на выполнение возможен только после того, как полностью
сгенерирована ОПС, т.е. когда синтаксических ошибок в программе не осталось, и
нет тех семантических ошибок, которые обнаруживаются семантическими
программами. Однако ошибки могут обнаруживаться при выполнении программы.
В частности, при выполнении операции индексации при проверке значений
индексов на предмет выхода их за допустимые пределы. Такие проверки следует
возлагать на интерпретатор.
При обнаружении подобных ошибок дальнейшее выполнение программы
становится невозможным, а от интерпретатора требуется выдать диагностическое
сообщение о месте ошибки, виде ошибки, а также значения переменных, из-за
которых возникла ошибка. Это можно сделать, так как интерпретатору доступны
таблицы переменных с их именами и значениями. Для того, чтобы определить место
возникновения ошибки в исходном тексте программы, при генерации ОПС можно
сформировать таблицу ссылок из исходного текста на ОПС, например, таким
образом: для каждой строчки исходного текста ссылка должна содержать номер
соответствующего ей элемента в ОПС.
42
6. ГЕНЕРАЦИЯ КОМАНД В КОМПИЛЯТОРЕ
6.1. Распределение памяти при генерации команд
Генерировать команды можно непосредственно при работе LL-анализатора,
задав соответствующие семантические программы. Однако схема работы будет
более гибкой, если генерировать команды на основе промежуточной формы
транслируемой программы, например, на основе ОПС. При этом перед генерацией
команд необходимо выполнить некоторые предварительные действия по
распределению памяти для создаваемой программы.
Машинная программа создается в виде файла (исполняемого модуля),
структура которого определяется той операционной системой, которая будет
запускать программу на выполнение. В этом файле кроме собственно машинных
команд содержится также служебная информация, необходимая для корректного
выполнения программы, в том числе информация о размере области памяти для
программы и используемых ею данных. Как минимум, необходимы области памяти
для размещения:
1) машинных команд программы;
2) констант, используемых в машинных командах;
3) статических переменных, размер которых может быть подсчитан при
трансляции программы;
4) динамических переменных, размер которых определяется в ходе исполнения
программы, так как зависит от входных данных и не может быть определен заранее.
Операционная система запускает исполняемый модуль на выполнение
следующим образом: считывает с внешнего носителя данных (диска) файл,
содержащий машинные команды (программу) и константы, в оперативную память,
выделяет область памяти для статических переменных и передает управление на
первую команду программы. В процессе работы программа может выполнять
специальные команды – запросы к операционной системе на выделение областей
памяти для динамических переменных. Последней командой, которую исполняет
программа, должна быть команда возврата управления в операционную систему.
При этом операционная система освобождает все занятые программой области
памяти. Возврат управления в операционную систему может случиться и
преждевременно, из-за ошибочных ситуаций, возникших в ходе исполнения
программы.
Таким образом, при генерации команд необходимо рассчитать три области
памяти: команд, констант и статических переменных. Память для команд можно
рассчитать только после генерации самих команд. Память для констант и
статических переменных состоит из двух частей каждая. 1-я часть констант
формируется заранее, на основе созданной анализатором таблиц констант, с учетом
их типов. 2-я часть, состоящая из дополнительных констант, формируется в
процессе генерации команд. 1-я часть памяти статических переменных
рассчитывается заранее, на основе сформированной анализатором таблиц
переменных, с учетом того, сколько байт требуется для каждой переменной в
43
зависимости от ее типа. 2-я часть, состоящая из дополнительных временных
переменных, рассчитывается в процессе генерации команд. Эти три области
рассчитываются независимо, при этом в каждой генерируемой команде, содержащей
адрес операнда, должен формироваться признак, к какой области памяти относится
адрес. После завершения генерации команд все эти три области памяти можно
«склеить», пересчитав адреса в командах таким образом, чтобы после команд сразу
же располагались константы, а далее – статические переменные.
Распределение памяти для динамических переменных рассмотрим на примере
массивов, количество элементов в которых вычисляется в процессе исполнения
программы. Будем считать, что нумерация элементов массива по каждому из
измерений начинается с нуля. Тогда для каждого массива при выделении ему
памяти требуется сформировать паспорт, в котором записывается:
1) адрес начального элемента массива в выделенной для него области памяти,
2) длина (в байтах) одного элемента массива
3) размер массива по 1-му измерению,
4) размер массива по 2-му измерению, и т.д.
Место для паспорта каждого из динамических массивов выделяется в области
статических переменных заранее, до генерации команд. Действия по заполнению
паспорта производятся при выполнении программы группой команд, реализующих
операцию выделения памяти массиву. Эти команды должны рассчитать размер
области памяти, сделать запрос на выделение памяти к операционной системе, после
чего заполнить паспорт массива.
6.2. Генерация команд для присваиваний и арифметических выражений
Рассмотрим генерацию машинной программы для простейшей системы
команд – одноадресной.
Пример 20. Определим модель процессора. Пусть в нем имеется специальный
регистр (регистр сумматора, или просто сумматор), который участвует при
выполнении каждой команды. В этом регистре может быть записано числовое
значение. Каждая команда будет иметь следующий вид:
<код операции> <операнд>
При этом одним из операндов всегда является сумматор, другим – операнд,
записанный в команде. Результат выполнения операции (если он будет вычислен)
всегда находится в сумматоре. Операнд в команде на самом деле представляет собой
номер ячейки памяти, где находится операнд (переменная или константа). Команды
будем записывать для наглядности в символьном виде, как код операции, так и
номер ячейки памяти.
Набор команд, достаточный для реализации четырех арифметических действий,
будет таким:
Add <операнд> – сложение содержимого сумматора с содержимым
операнда;
44
Sub <операнд> – вычитание из содержимого сумматора содержимого
операнда;
Mul <операнд> – умножение содержимого сумматора на содержимое
операнда;
Div <операнд> – деление содержимого сумматора на содержимое
операнда;
Load <операнд> – запись содержимого операнда в сумматор;
St <операнд> – запись содержимого сумматора в операнд.
Генератор команд по заданной ОПС можно реализовать в виде интерпретатора,
использующего магазин. В магазин записываются операнды (номера ячеек памяти),
в которых находятся значения переменных и результаты вычислений. Если
результат выполнения некоторой команды находится в сумматоре, то в магазин
записывается нуль. Кроме того, в генераторе имеется вспомогательная переменная
k, которая напрямую ссылается на тот элемент магазина, который содержит нуль.
Принцип работы генератора команд следующий. ОПС просматривается слева
направо. Если очередной элемент в ОПС – операнд, то он записывается в магазин.
Если очередной элемент в ОПС – операция, то из магазина считываются операнды
для этой операции, после чего генерируются команды, выполняющие данную
операцию. Если результат операции находится в сумматоре, то в магазин
записывается нуль, а в переменную k – ссылка на верхний элемент магазина.
Обозначим два верхних операнда в магазине как a и b. Генерируемые команды
зависят не только от операции ОПС, но и от содержимого k, a и b. Перед работой
генератора команд k := 0, магазин пуст.
1. Если k = 0, то генерируются команды:
Load a
Op b
где Op – команда Add, Sub, Mul или Div, в зависимости от операции. После этого в
магазин записывается нуль, а в переменную k – ссылка на верхний элемент
магазина.
2. Если k ≠ 0, a = 0, то генерируется команда:
Op b
где Op – команда Add, Sub, Mul или Div, в зависимости от операции. После
этого в магазин записывается нуль, а в переменную k – ссылка на верхний элемент
магазина.
3. Если k ≠ 0, b = 0, то генерируются команды:
Op b
где Op – команда Add или Mul, в зависимости от операции, или команды:
St
t
Load a
Op t
где Op – команда Sub или Div, в зависимости от операции, t – дополнительная
временная переменная. После этого в магазин записывается нуль, а в переменную
k – ссылка на верхний элемент магазина.
4. Если k ≠ 0, a ≠ 0, b ≠ 0, то генерируются команды:
45
St
t
Load a
Op b
где Op – команда Add, Sub, Mul или Div, в зависимости от операции, t –
дополнительная временная переменная. После этого в магазин записывается нуль, в
элемент магазина с номером k записывается t, после чего в переменную k – ссылка
на верхний элемент магазина.
5. Для операции :=.
– Если k = 0, то генерируются команды:
Load b
St
a
– Если k ≠ 0, b ≠ 0, то генерируются команды:
St
t
Load b
St
a
где t – дополнительная временная переменная. При этом в элемент магазина с
номером k записывается t.
– Если k ≠ 0, b = 0, то генерируется команда:
St
a
После этого переменной k присваивается нуль, в магазин ничего не записывается.
Рассмотрим пример генерации команд. Пусть задана ОПС:
x a b – c d e / + * :=
что соответствует оператору:
x := (a – b)*(c + d / e)
Шаги работы генератора команд будут такими (пропущены действия по записи
в магазин операндов):
Операция – , k = 0,
в магазине:
x a b
генерируемые команды:
Load a
Sub b
k := 2
Операция / , k = 2,
в магазине:
x 0 c d e
генерируемые команды:
St
t
Load d
Div e
2-я ячейка магазина := t, k := 4
Операция + , k = 4,
в магазине:
x
t
c 0
генерируемая команда:
Add c
k := 3
Операция * , k = 3,
в магазине:
x
t
0
генерируемая команда:
Mul t
k := 2
Операция := , k = 2,
в магазине: x 0
генерируемая команда:
St
x
46
k := 0, магазин пуст.
В результате получится такая программа:
Load a
Sub b
St
t
Load d
Div
e
Add c
Mul t
St
x
Когда при работе генератора команд в определенный момент требуется
дополнительная временная переменная, то она помещается в таблицу переменных и
рассчитывается адрес ее размещения в памяти, сразу за теми переменными, которые
были помещены в таблицу ранее.
Конец примера.
В рассмотренном примере предполагалось, что переменные в выражении все
имеют одинаковый тип, например, все они целые одинаковой разрядности. Если
система команд допускает использование различных числовых типов, например,
целых и вещественных одинарной и двойной точности, то требуется, чтобы типы
переменных были записаны в таблицу переменных еще при обработке анализатором
описаний в программе, при генерации ОПС. Тогда при работе генератора команд в
стек с каждым элементом должен записываться тип данных, и для каждого типа
требуется использовать соответствующий тип команд. Более того, если два
операнда одной команды имеют различающиеся типы, то вначале нужно
генерировать команды преобразования от одного типа к другому. Например, если
требуется сгенерировать команду сложения целочисленного и вещественного
значения, то перед этим надо сгенерировать команды преобразования целого типа в
вещественный, и только потом – команду сложения двух вещественных значений.
6.3. Генерация команд с индексными выражениями
Вначале рассмотрим генерацию команд, реализующих операцию выделения
памяти для динамических массивов.
Пример 21. Пусть, кроме рассмотренных команд, в процессоре имеется команда
прерывания, которая передает управление в особую область оперативной памяти,
где располагается операционная система. Пусть также, кроме регистра сумматора,
имеется еще один регистр, называемый регистром адреса. Формат команды
прерывания:
Itr <номер прерывания>
Вместо операнда в команде записан код – номер прерывания. Эта команда
передает управление в операционную систему, причем в ту ее область, которая
определяется номером прерывания. Кроме того, при этом в регистре адреса
47
запоминается адрес команды, которая расположена следом за командой прерывания.
Это необходимо для того, чтобы после обработки прерывания операционная
система вернула управление в программу. Перед тем, как выполнить команду
прерывания, необходимо, чтобы в регистр сумматора был записан параметр, смысл
которого определяется номером прерывания. Так, для прерывания, требующего
выделение памяти, в регистре сумматора должен быть записан размер области в
байтах, а после обработки прерывания и возврата в программу – в регистре
сумматора будет находиться адрес начала выделенной области памяти.
Рассмотрим генерацию команд, реализующей операцию выделения памяти для
одномерного массива. Пусть имеется ОПС:
M n <m1>
где M – ссылка на таблицу, содержащую описание массива M, а n – количество
элементов. Паспорт массива должен содержать:
M1 – адрес нулевого элемента массива;
d – длину одного элемента массива;
n1 – количество элементов массива.
Все эти три части паспорта располагаются в памяти подряд, и тогда вместо d
надо писать операнд в команде M1 + v, а вместо n1 – M1 + 2v, где v – длина в байтах
целого значения.
При работе генератора команд в магазин будут последовательно записаны M и
n, после чего будет интерпретироваться операция <m1>. В результате будут
генерироваться команды:
Load n
St
M1 + 2v
Mul <длина элемента>
Itr
<выделение памяти>
St
M1
Load <длина элемента>
St
M1 + v
Операнд <длина элемента> – константа, определяемая типом таблицы,
содержащей описание массива M.
Аналогично, ОПС с операцией выделения памяти для двумерного массива:
M n m <m2>
где M – ссылка на таблицу с описанием массива M, а n, m – количество строк и
столбцов в массиве. Паспорт двумерного массива содержит:
M2 – адрес нулевого элемента массива;
d – длина одного элемента массива (M1 + v);
n1 – количество строк в массиве (M1 + 2v);
n2 – количество столбцов в массиве (M1 + 3v).
При работе генератора команд будет создана последовательность:
Load n
St
M2 + 2v
Mul m
Mul <длина элемента>
Itr
<выделение памяти>
48
St
Load
St
Load
St
Конец примера.
M2
<длина элемента>
M1 + v
m
M1 + 3v
А теперь рассмотрим
индексирования.
генерацию
команд,
реализующих
операцию
Пример 22. Расширим модель процессора. Пусть в командах, кроме кода
операции и операнда, записывается признак косвенной адресации, который может
быть нулем или единицей:
<код операции> <признак> <операнд>
Если этот признак равен 0, то команда выполняется обычным образом, а если 1,
то используется косвенная адресация, когда операнд в команде – это адрес в памяти,
где находится адрес, ссылающийся на другую ячейку памяти, содержащую значение
операнда.
При генерации команд с использованием косвенной адресации необходимо,
чтобы в магазине интерпретатора для каждой ячейки магазина дополнительно
записывался признак (0 или 1). Тогда, при генерации любой команды с операндом
из магазина, в команду записывается операнд и этот признак.
Правила генерации команд, реализующих операцию <i1>, следующие.
Обозначим два верхних операнда в магазине как a и b. В переменной k находится
ссылка на элемент магазина, содержащий 0. Операнд а содержит ссылку на паспорт
массива, b – индекс элемента массива. В командах используются t1, t2 –
дополнительные временные переменные.
Если b = 0, то генерируются команды:
Mul 0
a+v
Add 0
a
St
0
t1
Если b ≠ 0, k = 0, то генерируются команды:
Load 0
b
Mul 0
a+v
Add 0
a
St
0
t1
В обоих случаях после генерации команд в магазин записывается t1 и признак
для нее 1, а в переменную k – нуль.
Если b ≠ 0, k ≠ 0, то генерируются команды:
St
0
t1
Load 0
b
Mul 0
a+v
Add 0
a
St
0
t2
49
После генерации команд в элемент магазина по ссылке k записывается t1, в
верхний элемент магазина записывается t2 и признак для него 1, а в переменную k –
нуль.
А теперь рассмотрим правила генерации команд, реализующих операцию <i2>.
Обозначим три верхних операнда в магазине как a, b и c. В переменной k находится
ссылка на элемент магазина, содержащий 0. Операнд а содержит ссылку на паспорт
массива, b – 1-й индекс элемента массива, c – 2-й индекс элемента массива. В
командах используются t1, t2 – дополнительные временные переменные.
Если b = 0, то генерируются команды:
Mul 0
a + 3v
Add 0
c
Mul 0
a+v
Add 0
a
St
0
t1
Если c = 0, то генерируются команды:
St
0
t1
Load 0
b
Mul 0
a + 3v
Add 0
t1
Mul 0
a+v
Add 0
a
St
0
t1
Если b ≠ 0, c ≠ 0, k = 0, то генерируются команды:
Load 0
b
Mul 0
a + 3v
Add 0
c
Mul 0
a+v
Add 0
a
St
0
t1
Во всех случаях после генерации команд в магазин записывается t1 и признак
для нее 1, а в переменную k – нуль.
Если b ≠ 0, c ≠ 0, k ≠ 0, то генерируются команды:
St
0
t1
Load 0
b
Mul 0
a + 3v
Add 0
с
Mul 0
a+v
Add 0
a
St
0
t2
После генерации команд в элемент магазина по ссылке k записывается t1, в
верхний элемент магазина записывается t2 и признак для него 1, а в переменную k –
нуль.
Рассмотрим пример генерации команд. Пусть задана ОПС:
M i j a + <i2> L i d – <i1> :=
что соответствует оператору:
50
M[i, j + a] := L[i – d]
Шаги работы генератора команд будут такими (пропущены действия по записи
в магазин операндов):
0 0 0 0
Операция + , k = 0,
в магазине:
M i
j
a
генерируемые команды:
Load 0
j
Add 0
a
k := 3
0 0 0
Операция <i2> , k = 3,
в магазине:
M i
0
генерируемые команды:
St
0
t1
Load 0
i
Mul 0
M + 3v
Add 0
t1
Mul 0
M+v
Add 0
M
St
0
t1
k := 0
1 0 0 0
Операция – , k = 0,
в магазине:
t1 L i
d
генерируемые команды:
Load 0
i
Sub 0
d
k := 3
1 0 0
Операция <i1> , k = 3,
в магазине:
t1 L 0
генерируемые команды:
Mul 0
L+v
Add 0
L
St
0
t2
k := 0
1 1
Операция := , k = 0,
в магазине:
t1 t2
генерируемые команды:
Load 1
t2
St
1
t1
k := 0, магазин пуст.
В результате получится такая программа:
Load 0
j
Add 0
a
St
0
t1
Load 0
i
Mul 0
M + 3v
Add 0
t1
Mul 0
M+v
Add 0
M
St
0
t1
51
Load
Sub
Mul
Add
St
Load
St
Конец примера.
0
0
0
0
0
1
1
i
d
L+v
L
t2
t2
t1
Замечания.
1. По аналогии с рассмотренными примерами реализации операций индексации
одно и двумерных массивов можно реализовать индексацию массивов большей
размерности.
2. Перед вычислением величины смещения относительно адреса нулевого
элемента массива, для безопасности, желательно генерировать также команды
проверки того, что индекс неотрицателен и меньше количества элементов по
соответствующему измерению. И если такая проверка даст отрицательный
результат, то нужно предусмотреть команду прерывания, которая бы передала в
операционную систему признак ошибки «неверный индекс», так как при этом
дальнейшее исполнение программы невозможно.
6.4. Генерация команд сравнения и перехода
При анализе таких конструкций языка программирования, как if–then–else и
while, в ОПС записываются операции сравнения, а также операции условного и
безусловного перехода. При генерации команд, реализующих эти конструкции,
необходимы также, кроме рассмотренных ранее команд процессора, команды
сравнения и перехода.
Пример 23. Расширим список команд процессора. Команда сравнения
C
<признак> <операнд>
сравнивает содержимое регистра сумматора с содержимым ячейки памяти, адрес
которой записан в операнде. Признак, как и в других командах, может указывать на
косвенную адресацию. Результат выполнения этой команды записывается в
специальный регистр – регистр состояний, который анализируется командами
условного перехода. Команд условного перехода всего шесть, имеется также одна
команда безусловного перехода. Команды выполняют переход на команду, номер
которой записан в операнде, в зависимости от результата сравнения, выполненного
предыдущей командой:
код операции
действие команды
J>
переход по условию больше
J≥
переход по условию больше или равно
J<
переход по условию меньше
J≤
переход по условию меньше или равно
J=
переход по условию равно
52
J≠
переход по условию не равно
J
безусловный переход
Правила генерации команд, реализующих операции сравнения рассмотрим на
примере операции «меньше» (<). Обозначим два верхних операнда в магазине как a
и b. В переменной k находится ссылка на элемент магазина, содержащий 0. В
командах используется дополнительная временная переменная t1, константы 0 и 1,
адреса которых будут записываться как «0» и «1», и признак косвенной адресации,
обозначаемый p, который может быть равен 0 или 1.
Если b = 0, то генерируются команды:
C
p
a
Load
0
«1»
J<
0
M:
Load
0
«0»
M:
«следующая команда»
Если a = 0, то генерируются команды:
C
p
b
Load
0
«1»
J≥
0
M:
Load
0
«0»
M:
«следующая команда»
Если a ≠ 0, b ≠ 0,k = 0, то генерируются команды:
Load
p
a
C
p
b
Load
0
«1»
J≥
0
M:
Load
0
«0»
M:
«следующая команда»
Во всех случаях после генерации команд в магазин записывается 0, а в
переменную k – ссылка на него.
Если a ≠ 0, b ≠ 0,k ≠ 0, то генерируются команды:
St
p
t1
Load
p
a
C
p
b
Load
0
«1»
J≥
0
M:
Load
0
«0»
M:
«следующая команда»
После генерации команд по ссылке k в магазин записывается t1, затем в магазин
записывается 0, а в переменную k – ссылка на него.
После выполнения всех сгенерированных команд в регистре сумматора будет
записано 1 или 0, что соответствует значению «истина» или «ложь» соответственно.
Если требуется сгенерировать команды, реализующие другие операции
сравнения, то в рассмотренных образцах требуется заменить команду J< или J≥
другой командой условного перехода.
Конец примера.
53
Перед генерацией команд, реализующих операции условного и безусловного
перехода ОПС, необходимо сформировать таблицу соответствия меток (адресов)
ОПС и адресов генерируемых команд. Эта таблица должна состоять из двух
столбцов: 1) метки ОПС; 2) адреса генерируемых команд. Вначале, после генерации
ОПС, ее необходимо просмотреть и записать в первый столбец все встретившиеся в
ОПС метки. Затем метки необходимо упорядочить по возрастанию и удалить
повторения.
Второй столбец таблицы формируется в процессе генерации команд
следующим образом. При просмотре очередного элемента ОПС его номер
сравнивается с очередной меткой в первом столбце таблицы, и если обнаружится
совпадение, то во второй столбец записывается номер очередной генерируемой
команды. Таким образом, вся таблица будет полностью заполнена после окончания
генерации команд.
При генерации команд операнды, являющиеся адресами команд, должны
временно заполняться метками ОПС, при этом номера таких команд должны
записываться в отдельный список. Наконец, на последней стадии необходим
просмотр этого списка, и в каждой команде из списка операнд (метка ОПС) должен
с помощью таблицы соответствия заменяться адресом соответствующей команды.
Пример 24. Рассмотрим правила генерации команд, реализующих операции
ОПС безусловного перехода и перехода по условию «ложь».
Операция безусловного перехода требует одного операнда – метки перехода,
которая при работе интерпретатора вначале попадает в магазин. При этом требуется
сгенерировать всего одну команду:
J
0
M:
где M: – метка перехода.
Операция перехода по условию «ложь» требует двух операндов – логического
значения и метки перехода. Если обозначить эти операнды в магазине как a и M:
соответственно, то правила генерации команд будут следующими. В переменной
k находится ссылка на элемент магазина, содержащий 0, t1 – дополнительная
временная переменная.
Если a = 0, то генерируются команды:
С
0
«1»
J≠
0
M:
Если a ≠ 0, k = 0, то генерируются команды:
Load
p
a
С
0
«1»
J≠
0
M:
Во всех случаях после генерации команд в магазин ничего не записывается,
k := 0.
Если a ≠ 0, k ≠ 0, то генерируются команды:
St
p
t1
Load
p
a
С
0
«1»
54
J≠
0
M:
После генерации команд по ссылке k в магазин записывается t1, k := 0.
Рассмотрим пример генерации команд. Пусть задана ОПС:
a b < M1 <jf> a b := M2 <j> b a :=
↑
↑
M1
M2
что соответствует оператору:
if a < b then a := b else b := a
Шаги работы генератора команд будут такими (пропущены действия по записи
в магазин операндов):
0 0
Операция < , k = 0,
в магазине:
a b
генерируемые команды:
Load 0
a
C
0
b
Load 0
«1»
J≥
0
M:
Load 0
«0»
M: «следующая команда»
k := 1
0
0
Операция <jf> , k = 1,
в магазине:
0 M1
генерируемые команды:
С
0
«1»
J≠
0
M1:
k := 0
0 0
Операция := , k = 0,
в магазине:
a b
генерируемые команды:
Load 0
b
St
0
a
k := 0
0
Операция <j> , k = 0,
в магазине:
M2
генерируемые команды:
J
0
M2:
k := 0
0 0
Операция := , k = 0,
в магазине:
b a
генерируемые команды: M1: Load 0
a
St
0
b
M2: «следующая команда»
k := 0, магазин пуст.
В результате получится такая программа:
Load
0
a
C
0
b
Load
0
«1»
55
J≥
0
M:
Load
0
«0»
M: С
0
«1»
J≠
0
M1:
Load
0
b
St
0
a
J
0
M2:
M1: Load
0
a
St
0
b
M2: «следующая команда»
Конец примера.
56
Download