ЛЕКЦИЯ 1
СУЩНОСТЬ ПРЕДМЕТА. СОДЕРЖАНИЕ КП. СРОКИ.
ОРГАНИЗАЦИЯ РАБОТ. МАТЕМАТИЧЕСКИЙ АППАРАТ.
СТРУКТУРНАЯ СХЕМА ТРАНСЛЯТОРА. ПРОХОДЫ ТРАНСЛЯТОРА.
СПИСОК ЛИТЕРАТУРЫ
Дорогие коллеги. В течении двух семестров мы будем зани-
маться интереснейшим разделом системного и теоретического прог-
раммирования - теорией проектирования трансляторов.
Представляюсь. Семикопенко Геннадий Петрович, к.т.н.
2 преподаватель - Дмитриенко Наталья Олеговна.
1 семестр: Будет прочитан курс основ проектирования трансля-
тора. Вы ознакомитесь с инструментальными средствами, которые ре-
комендуются для выполнения практической работы. В обязательном
порядке Вами будет разработано, согласовано и утверждено ТЗ на КП.
Завершается семестр зачетом. Его можно получить при выполне-
нии следующих условий:
- утвержденного ТЗ на КП;
- пояснительной записки к КП и ее защиты, либо традиционной
сдачи зачета по всему курсу.
Я рекомендую путь разработки ПЗ и ее защиты как наиболее для
нас выгодный. Текст ПЗ явится составной частью КП, а защищать то,
что Вы сами написали, намного легче. Сдача зачета все равно не
освобождает Вас от необходимости последующего составления ПЗ и ее
защиты во 2 семестре. Таким образом, разработка пояснительной
части КП в 1 семестре экономит время и студентам, и преподавате-
лям.
Цели проектирования:
- ознакомление с одним из существующих инструментов созда-
ния трансляторов - генераторов лексического и синтаксического
анализаторов;
- ознакомление с математическим аппаратом - формальными
грамматиками (G), используемыми для описания искуственных языков
(ИЯ);
- проектирование ИЯ (программирования, информационного, опи-
сательного и любых других);
- формальное описание ИЯ с использованием инструментальных
средств;
- отладка лексического (ЛА) и синтаксического (СА) анализа-
торов, входящих в состав проектируемого транслятора;
- разработка семантических программ транслятора;
- комплексная отладка транслятора на контрольных (тестовых)
примерах;
- и, наконец, завершающая подцель - защита КП. Содержание КП:
- введение, в котором Вы излагаете сведения о целях разра-
ботки КП, его связи с РИСКом, назначении проектируемого ИЯ;
- краткое описание используемого математического аппарата;
- описание инструментальных средств - генераторов лексичес-
ких и синтаксических анализаторов;
- неформальное описание разработанного ИЯ (назначение, об-
ласть применения, эффективность по сравнению с традиционными ЯП
для реализации конкретных процессов РИСК, примеры программ).
Если у конкретного студента не хватит воображения для разра-
ботки собственного ИЯ, он может использовать логически завершен-
ное подмножество существующего ИЯ (Фортран, Паскаль, ПЛ, языки
работы с БД и другие);
- формальное описание лексики и синтаксиса ИЯ;
- тексты тестовых программ на ИЯ;
- тексты тестовых программ на промежуточном языке - ожидае-
мый разработчиком результат трансляции. Как правило в качестве
промежуточного языка в КП используется язык Си;
- дерево вывода фрагмента тестовой программы;
- семантические программы (блоки, процедуры, функции), ис-
пользуемые для генерации текста на промежуточном языке и запоми-
нания результатов трансляции;
- протоколы результатов выполнения процессов трансляции;
- выводы;
- список литературы. В 1 семестре Вы можете мне не предъяв-
лять только протоколы. Все остальное должно обязательно присут-
ствовать в ПЗ КП.
ОРГАНИЗАЦИЯ ОТЛАДКИ. Все, кто проходит практику в подразде-
лениях предприятия, оснащенных ПЭВМ с операционной системой
МS-DOS, может их использовать для выполнения КП. Для этого пер-
вые 3 человека, согласовавшие со мной ТЗ на КП, должны предоста-
вить дискеты (по 1 шт.), на которые я скопирую инструментальное
ПО - генераторы программ лексического и синтаксического анализа-
торов - LEX и YACC соответственно. Есть и некоторая документация
к ПО.
Проектирование КП может вестись и на СМ-1420, находящихся в
распоряжении кафедры.
ПЗ КП лучше всего набрать на ПЭВМ и распечатать. В этом слу-
чае можно Вам подумать и о распределении отдельных составляющих
КП в зависимости от интересов конкретных студентов.
Те из Вас, кто готов сдать мне КП сейчас, завтра либо через
месяц - будет поощрен повышенной оценкой. При этом такие студен-
ты освобождаются от последующего посещения лекций и практических
занятий.
2 семестр: Отладка разработанного ПО и оформление протоко-
лов работы ЭВМ.
МАТЕМАТИЧЕСКИЙ АППАРАТ - теория формальных языков и грамма-
тик. Подробнее мы ознакомимся с ним на последующих лекциях.
ПРОЦЕСС ТРАНСЛЯЦИИ - выполняемое с сохранением смысла
преобразование входного сообщения с одного языка в выходное сооб-
щение на другом языке.
ТРАНСЛЯТОР - программа, выполняющая процесс трансляции. В
случае выполнения исходной программы без получения текста на вы-
ходном языке, говорят о режиме интерпретации входной программы.
Соответствующая программа, обеспечивающая непосредственную тран-
сляцию входного текста в последовательность команд ЭВМ, называет-
ся интерпретатором.
СТРУКТУРНАЯ СХЕМА ТРАНСЛЯТОРА:
- ЛА (сканер), используемый для разбора лексики исходного
языка (ИЯ). Сканер последовательно просматривает литеры исходной
(транслируемой) программы. Из литер по определенным правилам, за-
данным автоматной грамматикой, сканер строит символы программы
(иначе - лексемы). Состав лексем определяется разработчиком ИЯ и
транслятора с него. Как правило, это числа, идентификаторы, слу-
жебные слова, литералы (цепочки литер, имеющие в программе самос-
тоятельное значение);
- СА, используемый для определения синтаксиса транслируемо-
го текста и управления процессом трансляции. Результат СА - дере-
во вывода;
- семантический анализ, используемый для определения смысла
транслируемого сообщения (либо его фрагмента). Результат семанти-
ческого анализа - хранение информации о транслируемом сообщении в
специальных структурах (таблицах символов, идентификаторах, кон-
стант, и других);
- синтезатор (генератор) текста на промежуточном языке
(польская запись, триады, тетрады, графы, и другие). В Вашем КП в
качестве промежуточного языка я рекомендую использовать язык
программирования Си;
- генератор программы в объектном коде;
- редактирование связей, генерация программы в виде загру-
зочного модуля.
Принципы построения транслятора
Общие сведения
Компилятор должен выполнить анализ исходной программы, а
затем синтез обьектной программы. Исходная программа разлагается
на составные части, затем из них строятся части эквивалентной
обьектной программы. Для этого на этапе анализа компилятор строит
несколько таблиц, которые затем используются как при анализе,так
и при синтезе. Этот процесс представлен на рис.
Процесс преобразования языков программирования включает сле-
дующие этапы:
1) распознавание в исходном тексте программы различных сос-
тавляющих языка;
2) этап синтаксического анализа, на котором распознаются
структуры исходной программы и проверяется их правильность;
3) установление связи между используемыми идентификаторами и
их описанием.
Генерирование обьектного кода
Способ взаимодействия и метод обьединения этих этапов в еди-
ный процесс зависит от числа проходов компилятора.
Все 4-е последовательных процесса: сканирование, анализ,
подготовку к генерации и генерацию команд - можно выполнять пос-
ледовательно или параллельно определенной взаимной синхрониза-
цией. Одним из критериев, определяющих выбор в данном случае, яв-
ляется обьем доступной памяти. Часто выгодно или даже необходимо
иметь несколько проходов.
В однопроходных трансляторах целая последоватеьлность прос-
тых преобразований (этапов) применяется по очереди к каждой не-
большой части исходной программы таким образом, что рабочая прог-
рамма обращается в процессе единственного просмотра исходной
программы. В многопроходном трансляторе последовательности преоб-
разований применяются ко всей программе как к целому.
Имеется много факторов, определяющих выбор числа проходов
при проектировании трансляторов (время компиляции, время разра-
ботки, относительная независимость фаз трансляции, размеры ОП
ЭВМ).
Некоторые языки (Алгол-68, АДА) принципиально требуют для
своей реализации несколько проходов, в частности можно рассмот-
реть двухпроходную схему трансляции. Ее особенности:
1) она может использоваться для языков программирования, в
которых определяющая реализация идентификатора появляется (тек-
стуально) раньше любой прикладной реализации, что имеет место для
большинства языков;
2) при написании транслятора необходимо пректировать проме-
жуточный язык, на котором должен представляться исходный текст
между программными проходами.
Лексический анализ (сканер)
Сканер - самая простая часть компилятора, иногда также назы-
ваемая лексическим анализатором. Сканер просматривает литеры ис-
ходной программы слева направо и строит символы программы - це-
лые числа, идентификаторы, служебные слова, двухлитерные символы,
такие как ** и // и т.д. Символы передаются затем на обработку
фактическому анализатору. На этой стадии может быть исключен ком-
ментарий. Сканер также может заносить идентификаторы в таблицу
символов и выполнять другую простую работу, которая фактически
требует анализа исходной программы. Он может выполнить большую
часть работы по макрогенерации в тех случаях, когда требуется
только текстовая подготовка.
Синтаксический анализ
Анализаторы выполняют работы по расчленению исходной прог-
раммы на составные части, формированию ее внутреннего представле-
ния и занесению информации в таблицу символов и другие таблицы.
При этом также выполняется полный синтаксический и семантический
контроль программы.
Обычный анализатор представляет собой синтаксически управ-
ляющую программу. В действительности стремяться отделить синтак-
сис от семантики настолько, насколько это возможно. Когда синтак-
сический анализатор узнает конструкцию исходного языка, он вызы-
вает соответствующую семантическую программу, которая контроли-
рует данную конструкцию с точки зрения семантики и затем запоми-
нает информацию о ней в таблице символов или во внутреннем пред-
ставлении программы.
Семантический анализ
Семантическая программа осуществляет семантическую обработ-
ку, когда связанное с ней правило вызывает семантическую редукцию.
Мы предполагаем, что всякий раз, когда в сентенциальной фор-
ме основа x найдена и ее можно привести к нетерминалу U, синтак-
сический распознаватель вызывает программу, связанную с правилом
U ::= x. Генерируемая польская цепочка хранится в одномерном мас-
сиве Р. Целая переменная р содержит индекс, указывающий на пер-
вый свободный элемент массива. Каждый элемент массива может со-
держать один символ (идентификатор или оператор). Предположим
также, что вызванная семантическая программа имеет доступ к сим-
волам основы S(1) ... S(i), находящимся в синтаксическом стеке S,
который используется распознавателем.
Рассмотрим программу, связанную с правилом E1 ::= E2 + T.
Если она вызвана, то мы можем считать, что польская запись для Е2
и Т уже получена. При этом массив Р содержит
... <код для Е2> <код для Т>
поскольку Е2 раположено левее Т. Справа от в исходной прог-
рамме находятся только терминалы, т.к. разбор производится слева
направо. Следовательно, все, что требуется от программы, - это
занести знак "+" в польскую цепочку. При этом инфиксная запись Е2
+ Т переводится в суффиксную запись Е2 Т +. Следоватедьно, семан-
тическая программа имеет вид Р(р) :='+'; p := p+1.
Рассмотрим семантическую программу для F ::= I, где I обоз-
начает произвольный идентификатор. В соответствии с правилами
польской записи идентификаторы предшествуют своим операторам; бо-
лее того они встречаются в том же порядке, что и в инфиксной за-
писи. Все, что необходимо сделать, - это занести идентификатор в
массив Р. Поэтому программа имеет следующий вид;
Р(р) = S(i); p := p+1 где S(i) - верхний символ стека.
Т.к. для каждой продукции мы пишем одну семантическую прог-
рамму, то это помогает поделить обработку на мелкие независимые
части, каждую из которых можно запрограммировать отдельно, что
позволяет не думать обо всем сразу.
Небольшие изменения в синтаксисе или семантике требуют лишь
незначительных изменений в соответствующих правилах грамматики
или семантических программах. Различные части анализа отделены
друг от друга, поэтому внесение изменений не представляет особых
затруднений.
СПИСОК ЛИТЕРАТУРЫ
1. Грис Д. Конструирование компиляторов для цифровых вычис-
лительных машин. М., Мир, 1975 г.
2. Ахо А., Ульман Дж. Теория синтаксического анализа, пере-
вода и компиляции. М. Мир 1978 г.
3. Льюис Ф., Розенкранц Д., Стирнз Р. Теоретические основы
проектирования компиляторов. М., Мир, 1979 г.
4. Вирт, Вебер. Теория перевода, компиляции и редактирова-
ния. М., Мир, 1980 г.
5. Виленкин С.Я., Трахтенгерц Э.А. Математическое обеспече-
ние управляющих вычислительных машин. М., Энергия, 1972 г.
6. Фельдман Дж., Грис Д. Системы построения трансляторов.
Сб. Алгоритмы и алгоритмические языки, вып.5, ВЦ АН СССР, 1971 г.
ЛЕКЦИЯ 2
ОПРЕДЕЛЕНИЯ
Автомат с конечным числом состояний - это пятерка вида
(K,Vt,M,S,Z), где:
K - алфавит элементов, называемых состояниями;
Vt - входной алфавит (литеры, которые могут встретится в
цепочке или предложении);
M - отображение (или функция) множества К*Vt вo множество K
(если M(Q,T) = R, то это значит, что из состояния Q при входной
литере T происходит переключение в состояние R);
S C К - начальное состояние;
Z - непустое множество заключительных состояний, каждое из
которых принадлежит К.
Автомат - устpойство, пpедназначенное для выполнения целе-
напpавленных действий без непосpедственного участия человека.
Абстpактный автомат - математическая модель автомата, задан-
ная множествами (входных символов, состояний и выходных символов)
и двух двуместных функций (пеpеходов и выходов). Функция пеpехо-
дов отобpажает пеpвые два множества во втоpое, а функция выходов,
соответственно - в третье.
Конечный автомат - абстpактный автомат, все тpи опpеделяю-
щие множества котоpого конечны.
V+ - транзитивное замыкание множества V.
V* - рефлексивно-транзитивное замыкание множества V.
Фоpмальная гpамматика - фоpмальная система пpавил, описываю-
щих в опpеделенном аспекте некотоpый язык G=(Vt,Vn,P,S), где:
Vt - множество терминальных символов;
Vn - множество нетерминальных символов;
P - конечный набор правил подстановки;
S С Vn - начальный символ.
Символы в левой и правой частях правил в совокупности обра-
зуют словарь V, V = Vt U Vn.
Символы грамматики G, встречающиеся в левой части правил,
называются нетерминальными. Множество нетерминалов Vn С V являет-
ся подмножеством словаря, остальная часть множества V образует
множество терминальных симолов Vt С V.
В зависимости от ограничений, накладываемых на грамматичес-
каие правила (продукции), различают 4 основных класса грамматик
(по Хомскому). Их определение содержится ниже.
Правила автоматной гpамматики имеют вид:
U ::= N или U ::= NW, где N C Vt, а U, W C Vn.
Правила контекстно-свободной гpамматики имеют вид:
U ::= u, где U C Vn, u C V.
Правила контекстно-зависимой грамматики имеют вид:
xUy ::= xuy, где U C Vn, x, u, y C V.
Грамматика без ограничений на грамматичекие правила:
u ::= v, где u C V+ и v C V*.
Всякая конечная последовательность символов алфавита А назы-
вается цепочкой.
Непосредственный вывод. Пусть G - грамматика. Говорят, что
цепочка v непосредственно порождает цепочку w, т.е. v -> w, если
для некоторых цепочек x и y можно написать v = xUy, w = xuy, где
U ::= u - правило грамматики G. Также говорят, что w непосред-
ственно выводима из v.
Вывод. Пусть G - грамматика. Цепочка v порождает цепочку w,
т.е. v => w, если существует последовательность непосредственных
выводов v = x1 -> x2 -> x3 -> ... -> xn = w.
Формальный язык L(g) = { x | S=>x, x С Vt+ } Таким образом,
язык - это выводимое из S подмножество множества всех терми-
нальных цепочек, т.е. цепочек в Vt.
Сентенциальная форма. S => x - цепочка символов языка х, по-
рождаемых из аксиомы S.
Предложение: { x | S=>x, x C Vt* } - выводимая из аксиомы S
цепочка терминальных символов, принадлежащая рефлексивно-транзи-
тивному замыканию множества терминальных символов Vt*.
Транслятор - это программа, которая преобразовывает сообще-
ние, написанное на языке L1, в сообщение, написанное на языке L2,
с сохранением смысла.
Формальный язык характеризуется алфавитом, лексикой, семан-
тикой и синтаксисом.
┌──────────── ПРЕДЛОЖЕНИЕ ───────────┐
│ │ │ │
│ │ │ │
ОПРЕДЕЛЕНИЕ ПОДЛЕЖАЩЕЕ СКАЗУЕМОЕ ДОПОЛНЕНИЕ
│ │ │ │
голодный верблюд съел колючку
В самом общем виде в состав транслятора должны входить сле-
дующие блоки:
- Лексический анализ;
- Синтаксический анализ;
- Семантический анализ;
- Синтез программы на промежуточном языке;
- Оптимизация программы;
- Синтез объектной программы.
Лексический анализ реализуется с помощью лексического анали-
затора (сканера). ЛА выделяет лексемы из транслируемого сообще-
ния и заменяет их на символы языка. В процессе анализа могут воз-
никать ошибки.
Лексемы могут быть следующих классов:
- разделители;
- арифметические операции: + - / *;
- ключевые слова: for, begin, end, do, to, step;
- идентификаторы.
Синтаксический анализатор распознает синтаксис языка (струк-
туру).
Семантический разбор - это программа или группа программ,
занимающаяся распознаванием смысла сообщения.
Синтез программы - программа, которая занимается генерацией
программы на промежуточном языке.
Оптимизация программы - синтез программы в виде объектного
кода.
ФОРМАЛЬНОЕ ОПРЕДЕЛЕНИЯ ГРАММАТИКИ:
Грамматика - упорядоченная четверка G = (Vт, Vn, P, S), S C
Vn, множества терминальных Vt и нетерминальных Vn символов, грам-
матических правил P, начальный нетерминальный символ S или аксио-
ма.
Правила P непосредственно определяют процесс вывода. Хом-
ский ввел 4 класса грамматик:
1. Автоматная грамматика: символы, которые встречаются в ле-
вой части правил называются нетерминалами, они образуют множес-
тво нетерминальных символов Vn; символы, которые входят в множес-
тво Vт, называются терминалами. Нетерминалы и терминалы вместе
образуют словарь V.
V = Vn U Vт
U ::= N, U ::= WN
N C Vт, U,W C Vn
На базе автоматной грамматики строятся конечные или беско-
нечные автоматы, использующиеся для сканирования или синтаксичес-
кого анализа.
2. Контекстно-свободная грамматика:
U ::= u ; U C Vn , u C V
│ │
цепочка строка состоит из
исходного термин. и нетерм.
текста символов
(нетерм.)
Строка u сворачивается в U вне зависимости от контекста.
3. Контекстно-зависимые грамматики:
x U y ::= xuy
U C Vn; x,y,u C V
4. Грамматика без ограничения на правила вывода:
V ::= u u C V*, V C V*
Грамматика, которая позволяет разбирать арифметические выра-
жения:
<выражение>::= <терм>│
<выражение>+<терм>│
<выражение>-<терм>
<терм> ::= <множество>│
<терм>*<множество>│
<терм>/<множество>
<множество> ::= (<выражение>)│ i
i - идентификатор
Алфавит языка - это некоторое непустое конечное множество
элементов, называемых символами.
Всякая конечная последовательность символов языка называет-
ся цепочкой.
Пустая цепочка - цепочка, не содержащая ни одного символа.
Ее длина равна 0.
Множества цепочек в алфавите обычно обозначаются заглавными
буквами.
Х = mABCn
│ │
голова хвост
xy - конкатенация цепочек x, y. х - голова, у - хвост цепоч-
ки z=xy.
Произведение АВ двух множеств цепочек А и В:
AB = { xy │ x C A, y C B }
Степень цепочки: x^0 - "", x^N = x^(N-1)*x
V* - рефлексивно-транзитивное замыкание (итерация).
V+ - транзитивное замыкание (усеченная итерация).
Бесконечные множества:
V* = V^0 U V^1 U ... U V^N U ...
Формальное описание строки:
V*={z │ z = "",z = xU}, z,x C V*, U C V - любой символ из V.
Строка x непосредственно порождает y относительно P (x->y),
когда существуют строки u, w, (возможно пустые) такие, что x=uVw,
y=uzw, V ::= z C P.
Строка x порождает y относительно P (x=>y), когда сущес-
твует последовательность строк x0, x1, x2, ... xN, такая, что
x=x0, y=xN, xI-1 -> xI (I=1,2,...,N).
Язык - некоторое множество предложений:
Lg = { x │ x C Vt*, S => x }.
Порождение (либо свертывание) строк Lg можно представить в
виде дерева. Терминальные символы не порождают новых символов,
нетерминальные - порождают. Иначе терминальные символы - это те,
на которых образуются конструкции Lg.
Cентенциальная форма: S => x, х C V+.
Предложение - цепочка терминальных символов, выведенных из
аксиомы: { x │ S => x, x C Vt* }
Пусть w=xuy - сентенциальная форма. Тогда u - фраза для U C
Vn, если S => xUy и U => u. Простая фраза - если U -> u.
Основа - самая левая простая фраза. Существуют леворекурсив-
ные и праворекурсивные грамматики. Различные грамматики могут по-
рождать один и тот же L. Мы можем генерировать синтаксически пра-
вильные сообщения.
<предложение>::=<определение><подлежащее><сказуемое><дополнение>
<определение>::=<голодный>│<большой>
<подлежащее>::=<верблюд>│<слон>│ ...
<сказуемое>::=<съел>│<взял>│ ...
<дополнение>::=<колючку>│<ветку>│ ...
Используя функции порождения строк относительно синтаксиса
этого языка, можно генерировать строки, формально принадлежащие
этому языку, правильные синтаксически, но неверные семантически (
Пример - Глоклая куздра бодланула куздренка).
G1 и G2 эквивалентны, если порождаемые ими языки Lg1 и Lg2
равны. Эквивалентные преобразования грамматик могут быть ис-
пользованы для удобства выбора семантических программ.
Однозначная грамматика - если существует только одно дерево
вывода для каждого предложения Lg.
Разбор или синтаксический анализ сентенциальной формы - это
построение вывода и, возможно, синтаксического дерева для нее.
Существуют методы как нисходящего, так и восходящего разбо-
ра (относительно движения по синтаксическому дереву).
Непосредственный вывод xUy -> xuy называют каноническим, ес-
ли u C Vt*. Вывод w=>v канонический, если все непосредственные
выводы в нем канонические.
Сентенциальная форма, имеющая канонический вывод - канони-
ческая сентенциальная форма.
Свертывание будем называть проходом или анализом. В дальней-
шем будем считать, что в процессе анализа считывание входного
текста происходит слева направо, а свертывание начинается с той
самой левой части строки, которая может быть свернута без допол-
нительного анализа последующего текста. Такое свертывание будем
называть каноническим.
Отношения
->, => - символы отношений между цепочками.
Пара цепочек (c,d) принадлежит отношению R, если выполняет-
ся cRd.
Отношение P включает R, если из (c,d) C R следует (c,d) C Р.
Отношение, обратное R - R^(-1).
Рефлексивное отношение - при справедливости сRc.
Например: i <= i - рефлексивное, а i < i - не рефлексивное
отношение.
Транзитивное отношение R - если выполняется aRb, bRc => aRc.
Произведение R,P: cRPd - если существует е, такое что cRe и ePd.
Итерация R - (обозначается R*): aR*b - когда a=b или aR+b.
Ограничения, накладываемые на грамматику G:
- нет правил вида U ::= U;
- S=>xUy, U=>t, t C Vt* - приведенная грамматика;
Пример. G - язык арифметических выражений.
S ::= E
E ::= T
E ::= E+T
T ::= P
T ::= T*P
P ::= (E)
P ::= I
ЛЕКЦИЯ 3
АНАЛИЗ КОНТЕКСТНО-СВОБОДНЫХ ЯЗЫКОВ
С ПОМОЩЬЮ МАТРИЦ ПРЕДШЕСТВОВАНИЯ
Будем рассматривать каноническое свертывание контекстно-сво-
бодных (КС) языков. Нахождение эффективных методов свертывания -
одна из важнейших задач в теории построения трансляторов. В рас-
сматриваемых алгоритмах анализа входного текста, написанного на
КС-языке, используются отношения предшествования между каждой па-
рой соседних символов строки.
Рассмотрим подробнее отношения предшествования: пусть Si и
Sj - символы языка L (Si,Sj С V). Если в некоторой строке языка
они могут быть записаны рядом (...SiSj...), то между ними могут
существовать только три отношения.
1. В строке ...SiSj... свертываемая часть строки начинается
└──┘
с символа Sj, то есть Sj - самый левый символ свертываемой под-
строки. Если при этом Si не является последним символом другой
строки свертываемой подстроки, то будем говорить, что Si предшес-
твует Sj. Запишем это условие в виде Si < Sj.
2. В строке ...SiSj... символы Si и Sj входят в одну и ту
└────┘
же свертываемую часть строки.Этот факт запишем в виде Si = Sj и
будем говорить, что Si и Sj входят в одно и то же свертывание.
3. В строке ...SiSj... свертываемая часть строки кончается
└──┘
символом Si, то есть Si - самый правый символ свертываемой части
строки. Это условие запишем в виде Si > Sj и будем говорить, что
Sj следует за Si.
Отношения <, =, > назовем отношениями предшествования. Отно-
шения предшествования обычно записываются в виде матрицы, стол-
бцы и строки которой соответствуют символам словаря V. На пересе-
чении i-ой строки и j-го столбца записывается отношение предшес-
твования между символами Si и Sj. Элементами матрицы являются
знаки <, =, > или пусто. Последний случай означает, что символы
Si и Sj ни в одной строке языка не могут стоять рядом.
В дальнейшем будет введено формальное определение отношений
предшествования и дан алгоритм их определения.
Сейчас же мы рассмотрим алгоритм анализа программы, разрабо-
танный Виртом и Вебером.
Достоинство этого алгоритма заключается в том, что он сни-
мает дополнительное ограничение на грамматику языка, и допускает,
чтобы в правилах грамматики два нетерминальных символа стояли ря-
дом. Но и в этом алгоритме требуется, чтобы между каждой парой
символов языка существовало не более одного отношения предшество-
вания и в множестве синтаксических правил не было двух одинако-
вых правых частей, т.е. правил, у которых правые части одинаковы,
а левые - разные.
Алгоритм основан на каноническом свертывании, т.е. на выде-
лении самой левой свертываемой части строки. Блок-схема алгорит-
ма показана на рис. 1 (@ - символ начального (конечного) ограни-
чителя входного анализируемого текста; не входит в алфавит языка).
┌────────────────────────────────┐
│ ^
┌─────┴───┬─┐ │
┌───────┬─┐ │ i:=i+1 │2│ │
│ i:=1 │1│ │ └─┤ │
│ └─┤ │ j:=i │ │
│ k:=2 │ │ │ │
│ │ │ R :=L │ │
│ R :=@ │ │ i k │ │
│ i │ │ │ │
└────┬────┘ │ k:=k+1 │ │
│ └────┬──────┘ │
│ │ │
│ ___│___ │
│ / │3\ ┌─────────┬──┐ │
│ / └──\ Да │ Конец │10│ │
│ / R равно @ \─────>┤ работы └──┤ │
│ \ i / │ алгорита │ │
│ \ / └────────────┘ │
│ \_______/ │
│ │ │
│ │ │
└───────────────────>─┤ │
┌───────────────────>─┤ Нет │
│ ___│____ │
│ / │4\ │
│ / └──\ Нет │
│ / R > L \__________________________│
│ \ i k /
│ \ ? /
│ \________/
│ Да│
│ ├<────────────────────┐
│ ____│______ │
│ / │5\ │
│ / └──\ ┌─────┴───┬─┐
│ / R = R \______│ │6│
│ \ j-1 j / Да │ j:=j-1 └─┤
│ \ / │ │
│ \___________/ └───────────┘
│ │Нет
│ ┌────────┴─────┬─┐
│ │ │7│
│ │ U < R ... R└─┤
│ │ j i │
│ └────────┬───────┘
│ │
│ │
│ ┌───────┴─────┬─┐
│ │ │8│
│ │ Переход на └─┤
│ │ семантические │
│ │ подпрограммы │
│ │ │
│ └───────┬───────┘
│ │
│ ┌───────┴────┬─┐
│ │ i:=j │9│
└───────────────┤ R :=u └─┤
│ i │
└──────────────┘
Рис. 1. Блок-схема алгоритма свертывания
В начале анализа строки в верхнюю ячейку магазина записы-
вается первый символ программы, т.е. символ "@" (блок 1).
Затем находится самый правый символ самой левой свертывае-
мой части строки (блок 4). Для этого по матрице предшествования,
которая составляется заранее, проверяют отношения предшествова-
ния между символом, находящимся в верхней ячейке магазина Ri, и
очередным входным символом строки Lk. Если условие Ri > Lk не вы-
полняется, то очередной входной символ записывается в верхушку
магазина (блок 2) и процесс продолжается до тех пор, пока не бу-
дет выполнено условие Ri > Lk, т. е. не будет найден самый пра-
вый символ самой левой свертываемой части строки.
Затем находится самый левый символ этой свертываемой части
строки. Для этого проверяется отношение предшествования между
каждой парой символов Rj-1 = Rj, записанных в магазине (блоки 5 и
6). Нарушение условия Ri = Rj означает, что свертываемая часть
строки кончилась и последовательность символов Rj...Ri есть са-
мая левая свертываемая часть строки.
У каждого нетерминального символа может быть несколько са-
мых левых и самых правых символов.
По таблице, имеющейся в трансляторе, в которой записаны
правила свертывания, производится свертывание (блок 7) и управ-
ление передается на соответствующую семантическую подпрограмму.
Семантическая подпрограмма генерирует программу на выходном язы-
ке, соответствующую данному синтаксическому правилу (блок 8).
После того, как генерирование соответствующего куска выход-
ной программы закончено, символы Rj...Ri "выталкиваются" из мага-
зина и в его верхнюю ячейку записывается символ u (блок 9).
Процесс продолжается до тех пор, пока в верхней ячейке мага-
зина не будет обнаружен символ "@" (блок 3), определяющий конец
программы.
Для анализа ошибок в алгоритм включены следующие блоки:
Блок 11 проверяет, могут ли символы Rj и Lk стоять в строке
рядом. Если в матрице предшествования (i,j)-ый элемент пуст (знак
_), то символы Si и Sj стоять рядом не могут и осуществляется пе-
реход к блоку 15. Иначе управление передается в блок 4.
Блок 12 проверяет значение величины j: если j=1, то должно
производиться сравнение с символом "@", что по алгоритму анализа
вообще быть не может. В этом случае переход к блоку 15. Если j не
равно 1, то переход к блоку 5.
Блок 13 проверяет, есть ли правило с правой частью Rj...Ri в
грамматике языка. Если да,переход к блоку 7, иначе - к блоку 15.
Блок 14 проверяет, есть ли правило Rj...Ri. Если да, то ал-
горитм анализа и свертывания входного текста закончен (переход к
блоку 10); иначе в тексте есть ошибка, из-за которой он не может
быть свернут (переход к блоку 15).
Блок 15 выводит сообщение об ошибке и переходит к анализу
следующего оператора.
Таким образом, сложный анализ входного текста, написанного
на входном языке, реализуется простым алгоритмом.
Требование единственности отношений предшествования вызы-
вает необходимость перестройки грамматики языка. Устранение этой
неоднозначности иногда становится достаточно трудоемкой задачей.
Мак-Киман разработал алгоритм анализа входного текста, который не
предъявляет к грамматике языка требования однозначности отноше-
ний предшествования.
Для определения границ сворачиваемой строки он использует не
одну, а две матрицы: первую - для нахождения самого правого сим-
вола строки - назовем М1, и вторую - для нахождения самого лево-
го символа строки - М2. В М1 заносятся следующие отношения пред-
шествования: <= (< или =), > и >=< (последнее означает > и либо
=, либо <). B M2 заносятся отношения предшествования => (> или =),
< и <=> (последнее означает < и либо =, либо >).
При поиске самого правого символа безразлино, какое от ноше-
ние предшествование <, = или <= находиться между двумя анализи-
руемыми символами. Аналогично при поиске самого левого символа
безразлично, какое отношение предшествование >, = или => находит-
ся между двумя анализируемыми символами. Поэтому эти отношения в
соответствующих матрицах не различаются. В тех случаях, когда
между парой символов существуеют отношения >=<, при поиске гра-
ниц сворачиваемой строки необходима дополнительная информация.
Эта информация задается в виде логических функций трех аргументов.
При поиске самого правого символа сворачиваемой строки с
помощью матрицы М1 используется функция
| истина, если Si > Lk;
Р1(Si-1, Si, Lk) = <
| ложь в противном случае.
Функция Р1 истинна, если в контексте Si-1SiLk символ Si яв-
ляется самым правым символом сворачиваемой строки ...Si-1Si.
Здесь Si - символ, хранящийся в верхней ячейке стека, Si-1 -сим-
вол, хранящийся в следующейся ячейке стека и Lk - очередной сим-
вол анализируемого текста.
Таким образом, каждой паре символов, у которых в М1 записа-
но отношение >=<, ставится в соответствие несколько функций Р1,
позволяющих анализировать уже не пару, а тройку символов для оп-
ределение правил границы сворачиваемой строки. Но эта тройка уже
должна быть определена так, чтобы ответ был однозначный.
Аналогично при поиске самого левого символа сворачиваемой
строки с помощью матрицы М2 используется функция
| истина, если Sj-1 < Sj;
Р2(Sj-1, Sj, Sj+1) = <
| ложь в противном случае.
B контексте Sj-1SiSj+1 символ Sj является самым левым симво-
лом сворачиваемой строки SjSj+1... . Здесь Sj-1, Sj, Sj+1 - сим-
волы, хранящийся в j-1, j и j+1 ячейках стека.
Каждой паре символов, у которых в М2 записано отношение <=>,
ставится в соответствие несколько функций Р2, позволяющих, как и
функции Р1, анализировать не пару, а тройку символов. Но эта
тройка уже должна быть определена так, чтобы ответ был однознач-
ный.
Блок-схема алгоритма анализа входного текста с помощью мат-
риц предшествования и функций Р1 и Р2 приведена на рис. 2. Буквы
i и j обозначают номера ячеек магазина, k - номер очередного сим-
вола входного текта, Ri и Rj- символы, находящиеся в i-х и j-х
ячейках магазина, Lk - k-й символ входного текста. В начале ана-
лиза строки в верхнюю ячейку магазина записывается первый символ
программы, т.е. символ "@" (блок 1). Затем производится проверка
на конец программы (блок 3). Если Lk = @, то выполнение програм-
мы окончено и осуществляется переход к блоку 14. Если Lk не сов-
падает @, осуществляется переход к блоку 4.
Производится поиск самого правого символа самой левой свер-
тываемой строки. Для этого по М1 проверяется отношение предшес-
твования между символами Lk и Ri (блок 4). Если это отношение
равно <=, т.е. Ri не является самым правым символом сворачивае-
мой строки, то производится запись очередного символа в магазин
(блок 2).
Если между символами Ri и Lk существует отношение <=>, то
блоком 5 проверяется функция Р1 для двух верхних символов магази-
на и очередного символа входного текста. При значении функции Р1
- ложь (на рис. 2), это значение обозначено Л, т.е. если Ri не
является самым правым символом, осуществляется переход к блоку 2.
Если значение Р1 - истина (И), т.е. Ri - самый правый символ
строки, то осуществляется переход к блоку 6.
Блоком 6 начинается поиск самого левого символа свертывае-
мой строки; j присваивается значение i. По М2 определяется отно-
шение между символами Rj-1 и Rj (блок 7). Если отношение есть =>,
т.е. Rj не является самым левым символом свертываемой строки, то
j := j-1 (блок 8) и определяется отношение между следующей парой
символов. Если отношение есть <, т.е. Rj-1 является самым левым
символом, осуществляется переход к блоку 10. Блок 10 введен для
того, чтобы в блоке 11 свертывание всегда происходило от j-го до
i-го элемента.
Если между символами Rj-1 и Rj отношение >=<, то проверяет-
ся функция Р2 для трех символов, находящихся в j-1, j и j+1-й
ячейках магазина. Если значение Р2 - Л (Rj не является самым ле-
вым символом), осуществляется переход к блоку 8; если значение Р2
- И (Rj является самым левым символом), то осуществляется пере-
ход к блоку 11.
Блоки 11,12 и 13 производят свертывание, переход на генери-
рующую подпрограмму и запись вновь полученного нетерминального
символа U в верхнюю ячейку магазина. Они в точности соответ-
ствуют блокам 7, 8, 9 на рис. 1. На рис. 1. не показаны блоки
анализа синтаксических ошибок. Они аналогичны соответствующим
блокам на рис. 2.
Метод Мак-Кимана освобождает от ограничения однозначности
отношений предшествования, накладываемого методом Вирта, но он
требует, естественно, больших объeмов памяти для записи матриц и
функции. В трансляторе с одной из версий языка PL/1 для машин се-
рии IBM-360 М1 составила 90x45 элементов, М2-90x90. Каждый эле-
мент занимает 2 бита. Число функций Р1 и Р2 приближалось к 450.
ЛЕКЦИЯ 4
ПОСТРОЕНИЕ МАТРИЦ ПРЕДШЕСТВОВАНИЯ
И ПРЕОБРАЗОВАНИЕ ГРАММАТИК
Рассмотрим алгоритм составления матрицы предшествования. Для
этого дадим формальные определения отношений предшествования и
множество самых левых и самых правых символов.
1. Для любой упорядоченной пары символов (Si, Sj) Si = Sj
тогда и только тогда, когда существует синтаксическое правило ти-
па U::=xSiSjy для некоторого символа U и некоторых строк x и y.
2. Для любой упорядоченной пары символов (Si,Sj) Si < Sj
тогда и только тогда, когда существует синтаксическое правило ти-
па U::= xSiUly для некоторых U, Ul, x, y и порождение Ul -> Sjz
для некоторой строки z.
3. Для любой упорядоченной пары символов (Si, Sj) Si > Sj
тогда и только тогда, когда:
a) существует синтаксическое правило типа U ::= xUkSjy для
некоторых U, Uk, x, y и порождение Uk -> zSi для некоторой стро-
ки z или
б) существует синтаксическое правило типа U ::= xUkUly для
некоторых U, Uk, Ul, x, y и порождения Uk -> zSi, Ul -> Sjw для
некоторых строк z и w.
Строки x,y,z,w могут быть пустыми.
4. В множество L(U) самых левых символов нетерминального
символа U входят все символы S такие, что существует порождение:
U -> Sz,
где z - некоторая строка (возможно, пустая).
Это определения может быть записано в виде:
Л(U) = { S | существует строка z, такая, что (U -> Sz) }.
5. В множество R(U) самых правых символов нетерминального
символа U входят все символы S такие, что существует порождение:
U -> zS,
где z - некоторая строка (возможно, пустая).
Это определения может быть записано в виде:
R(U) = { S | существует строка z, такая, что (U -> zS) }.
Данные выше формальные определения отношений предшествова-
ния и множеств самых левых и самых правых символов хорошо отра-
жают содержательную сторону определяемых понятий, но для их на-
хождения с помощью ЭВМ целесообразно использовать следующие ре-
курсивные определения (символ ] означает существование, символ /\
- "И", символ \/ - "ИЛИ", символ ф - правило):
1а. Si = Sj ::= ] ф (ф: U ::= xSiSjy).
2а. Si < Sj ::= ] ф ((ф: U ::= xSiUly) /\ Sj С L(Ul)).
3а. Si > Sj ::= ] ф ((ф: U ::= xUkSjy) /\ Si С R(Uk)) \/
(] ф ((ф: U ::= xUkUly) /\ Si С R(Uk)) /\
Sj С L(Ul)).
4a. L(U)= S | ]z (U ::= Sz) \/ ]z, U1 (U ::= U1z /\ S С L(U1)).
5a. R(U)= S | ]z (U ::= zS) \/ ]z, U1 (U ::= zU1 /\ S С R(U1)).
Всюду ф C P.
Определения 1а-5а дают эффективный алгоритм нахождения мно-
жеств L(U) и R(U) и отношений предшествования.
Рассмотрим алгоритм нахождение множества самых левых симво-
лов для символов, принадлежащих Vn.
Шаг 1: проверяем, является ли i-ый символ самым левым в
l-том синтаксическом правиле. Если да, то записываем Si в табли-
цу самых левых символов нетерминального символа Ul (при условии,
что он туда еще не записан);
Шаг 2: проверяем, не является ли символ Ul, для которого Si
- самый левый, самым левым символом Uk. Если да, то записываем
символы Ul и Si в таблицу самых левых символов нетерминального
символа Uk (при условии, что они туда еще не записаны).
Осуществляем просмотр всех синтаксических правил, изменяя
индекс l и k (два индекса для того, чтобы различать нетерми-
нальные символы в левой (k) и правой (l) частях правила); индекс
i указывает номер символа в синтаксическом правиле. Блок-схема
алгоритма показана на рис. 2.
┌─────┐
│ 1 │
└──┬──┘
│
┌──┴──┐
│ 2 ├──────────┐
└──┬──┘ │
│ │
нет / \ │
┌─────< 3 > │
│ \ / │
│ │да │
│ ┌──┴──┐ │
│ │ 4 │ │
│ └──┬──┘ │
└───────┤ │<
┌──┴──┐ / \ > ┌─────┐
│ 5 │ <13 >────┤ВЫХОД│
└──┬──┘ \ / └─────┘
┌────────────┤ │
│ / \ нет ┌──┴──┐
│ < 6 >────┐ │ 12 │
│ \ / │ └──┬──┘
│ да│ │ │да
┌──┴──┐ ┌──┴──┐ │нет / \
│ 9 │ │ 7 │ ├────<11 >
└──┬──┘ └──┬──┘ │ \ /
│ ├──────┘ │
│ < / \ > ┌──┴──┐
└──────────< 8 >────────┤ 10 │
\ / └─────┘
Рис. 2. Блок-схема алгоритма нахождения самых левых символов
Обозначения к алгоритму:
1. l = 1.
2. i = 1 - номер символа в синтаксическом правиле.
3. ] P: Ul ::= Si z - является ли i-ый символ самым левым
в синтаксическом правиле l.
4. Si записать в L(Ul): (L(Ul) = L (Ul) U Si).
5. k = 1.
6. ] P: Uk ::= Ul z - является ли Ul самым левым символом Uk
при условии, что Si C L(Ul).
7. L (Uk) = L (Uk) U Ul U Si - дополнить символами Ul и Si
мн-во L(Ul)
8. k < l (k, l - номера синтаксических правил),
проверка окончания цепочки.
9. k = k + 1.
10. i = i + 1.
11. Si = ; - завершилось ли синтаксическое правило.
12. l = l + 1.
13. l <= (L - общее число грамматических правил в грамматике).
Допущения к алгоритму:
- синтаксические правила отделены друг от друга ";"
- левая часть не отделяется от правой, и левая часть (т.е.
контекстно-свободная грамматика) всегда состоит из одного символа.
Аналогично можно построить множество самых правых символов.
Теперь перейдем к нахождению матрицы предшествования.
Блок-схема алгоритма построения матрицы предшествования, ис-
пользующая определения 1а-3а, представлена на рис. 3. Используют-
ся следующие условные обозначения:
i, j - индексы определяют номер символа в списке символов
языка.
M[i,j] - элемент матрицы предшествования;
l, k - номера синтаксических правил языка.
┌─────┐
│ 1 │
└──┬──┘
│
┌──┴──┐
│ 2 ├─────────────────────────┐
└──┬──┘ │
│ │
┌─────┐ / \ │
│ 4 │────< 3 >─────────────────┐ │
└─────┘ \ / │ │
│ │ │
┌──┴──┐ │ │
│ 5 │ │ │
└──┬──┘ │ │
├────────┐ │ │
/ \ да / \ │ │ │
┌──────────< 7 >────< 6 > │ │ │
│ \ / \ / │ │ │
│ │нет │нет │ │ │
│┌─────┐ │ / \ ┌──┴──┐ │ │
└┤ 8 ├─────┴──────< 9 >───┤ 10 │ │ │
└─────┘ \ / < └─────┘ │ │
│> │ │
┌──┴──┐ │ │
│ 11 │ │ │
└──┬──┘ │ │
├────────┐ │ │
┌─────┐ да / \ да / \ │ │ │
│ 14 ├───<13 >────<12 > │ │ │
└──┬──┘ \ / \ / │ │ │
└────────┴────────┤ │ │ │
/ \ ┌──┴──┐ │ │
<15 >───┤ 16 │ │ │
\ / < └─────┘ │ │
│ │ ┌──┴──┐
┌──┴──┐ │ │ 29 │
│ 17 │ │ └──┬──┘
└──┬──┘ ┌──┴──┐ │
├─────────────┐ │ 30 │ / \ > ┌─────┐
┌──┴──┐ │ └──┬──┘ <28 >───┤ВЫХОД│
│ 18 │ │ │ \ / └─────┘
└──┬──┘ │ │ ┌───┘
│ │ │ │>
нет / \ ┌──┴──┐ │ / \
┌──────────────────< 19 > │ 26 │ └──<27 >
│ \ / └──┬──┘ < \ /
│ │ │< │
│┌─────┐да / \ да / \ / \ │
││ 22 ├───<21 >────<20 > <25 >────────┘
│└──┬──┘ \ / \ / \ / >
│ │ │нет │ │
│ │ │ / \ < ┌─────┐ │
└───┴────────┴──────<23 >───┤ 24 │ │
\ / └─────┘ │
│> │
└─────────────┘
Рис. 3. Блок-схема алгоритма построения матрицы предшествования
для контекстно-свободных грамматик
Обозначения к алгоритму 1:
1. i = 1 (номер символа в алфавите языка).
2. j = 1.
3. ] P: U ::= x Si Sj - проверка наличия отношения = и
нахождение элемента матрицы с этим
отношением.
4. М [i,j]= =.
5. k = 1 (k,l - номера синтаксических правил).
6. Si C L (Uk) - находят элементы матрицы.
7. ] P: U ::= x Si Ux y имеющие отношение <.
8. М [i,j] = <.
9. k < j.
10. k = k + 1.
11. k = 1.
12. Si C L(Uk) находят элементы матрицы.
13. ] P: U ::= x Uk Sj y соответствующие отношению >
14. М [i,j] = >.
15. k < j.
16. k = k + 1.
17. k = 1.
18. l = 1.
19. Si C L(Uk) находят
20. Si C L(Ul) элементы матрицы,
21. ] P: U ::= x Uk Ul y соответствующие отношению >
22. М [i,j] = >.
23. l > j.
24. l = l + 1.
25. k < j.
26. k = k + 1.
27. j < I (I - мощность словаря языка).
28. i < I.
29. i = i + 1.
30. j = j + 1.
Блок 3 проверяет условие 1а и находит элементы матрицы, рав-
ные =, блок 4 записывает значение элемента в матрицу.
Блоки 6 и 7 проверяют условие 2а и находят элементы матрицы,
равные <.
Блок 8 записывает значение элемента в матрицу.
Блоки 12, 13, 19, 20 и 21 проверяют условия 3а и находят
элементы, равные >, блоки 14 и 22 записывают значения этих эле-
ментов в матрицу.
Остальные этапы выполнения алгоритма видны из блок-схемы.
Данный алгоритм не ограничивается нахождением только одного
отношения предшествования между любыми двумя символами Si и Sj, а
записывает в матрицу все отношения предшествования, которые меж-
ду ними существуют. Такая матрица не может использоваться в алго-
ритме анализа программы, так как не ясно, какое из отношений
предшествования между двумя символами брать для нахождения грани-
цы в каждом отдельном случае.
Но введением дополнительных нетерминальных символов и изме-
нением синтаксических правил, которые не меняют правил написания
программ на языке программирования, т. е. эквивалентным преобра-
зованием грамматик, можно добиться того, чтобы между каждой па-
рой символов существовало не более одного отношения предшествова-
ния.
Единого алгоритма, преобразующего любую КС-грамматику в
грамматику типа 2 по классификации Хомского, отвечающую двум до-
полнительным ограничениям:
- однозначности отношений предшествования;
- отсутствие двух грамматических правил с одинаковыми правы-
ми частями
НЕ СУЩЕСТВУЕТ.
Но, построив матрицу предшествований и выяснив неоднознач-
ность отношений между некоторыми символами, эту неоднозначность
можно устранить введением дополнительных нетерминальных символов
и некоторым изменением синтаксических правил.
Рассмотрим несколько алгоритмов, позволяющих преобразовать
КС-грамматику в грамматику типа 2 с учетом указанных ограничений:
1. Пусть между некоторыми двумя символами Si и Sj сущес-
твуют два отношения предшествования Si = Sj и Si < Sj.
Значит, существуют правила
Un ::= XnSiSjYn (n = 1,2,...,N); (1)
Um ::= ZmSiFkDm (m = 1,2,...,M; k = 1,2,...,K);
Fk -> SjQk; (2)
Введем новые нетерминальные символы Pn и синтаксические
правила Pn ::= SjYn (n = 1,2,...,N), заменяя все правила (1)
правилами Un ::= XnSiPn, при этом если исходная грамматика со-
держит правила вида Qn ::= SjYn, то заменяем их правилами Qn ::=
Pn.
1а. Если применение правила 1 не устраняет неоднозначности,
то вводятся нетерминальные символы Pn и Lm и синтаксические пра-
вила Pn ::= XnSi и Lm ::= ZmSi и все правила (1) заменяются на
Un ::= PnSjYn, а все правила (2) на правила Um ::= LmFkDm.
Если исходная грамматика содержит правила вида Qn ::= XnSi и
Tm ::= ZmSi, то заменяя их на правила Qn::=Pn и Tm::=Lm соответ-
ственно. Правила Pn и Lm надо выбирать так, чтобы Pn не совпада-
ло с Lm, т.е. чтобы ни для каких n и m Xn не совпадало с Zm.
Алгоритмы 1 и 1а устраняют отношения предшествования =. Это
видно из определений 1а-3а.
2. Пусть между некоторыми двумя символами Si и Sj сущес-
твуют два отношения предшествования Si = Sj и Si > Sj.
Значит, существуют правила
Un ::= XnSiSjYn (n = 1,2,...,N); (3)
Um ::= ZmFkSjDm или Um ::= ZmFkRlDm (4)
(m = 1,2,...,M; k = 1,2,...,K; l = 1,2,...,L);
Fk -> QkSi Rl -> SjTl.
Введем новые нетерминальные символы Pn и синтаксические пра-
вила Pn ::= XnSi (n = 1,2,...,N). Заменяем правила (3) правилами
Un ::= PnSjYn, устраняя тем самым отношения предшествования =.
Если исходная грамматика содержит правила вида Qn ::= XnSi, то
заменяем их правилами Qn ::= Pn.
3. Пусть между некоторыми двумя символами Si и Sj сущес-
твуют два отношения предшествования Si < Sj и Si > Sj, т. е. су-
ществуют правила
Un ::= XnSiFkYn (n = 1,2,...,N); (5)
Um ::= ZmRlSjDm или Um ::= ZmRlTpDm (6)
(m = 1,2,...,M; k = 1,2,...,K; l = 1,2,...,L;
p = 1,2,...,P);
Fk -> SjQk Rl -> HlSi, Tp -> SjWp.
Введем новые нетерминальные символы Pn и синтаксические пра-
вила Pn ::= XnSi (n = 1,2,...,N). Заменяем правила (5) правилами
Un ::= PnFkYn, сведя их к правилам (6) и устранив тем самым отно-
шения предшествования типа <. Если исходная грамматика содержит
правила вида Qn ::= XnSi, то заменяем их правилами Qn ::= Pn.
Справедлива теорема, согласно которой алгоритмы 1-3 измене-
ния синтаксиса языка программирования не изменяют правила на-
писания и семантику программ на данном языке.
Для тех случаев, когда КС-грамматику надо преобразовывать в
грамматику типа 2 только с одним дополнительным ограничением -
однозначность отношений предшествования (одинаковые правые части
допускаются), Ленаром и Лимом предложен следующий универсальный
алгоритм.
1. Составляются таблицы L(N) самых левых и R(N) - самых пра-
вых сиволов нетерминального символа N. Символ N C L(N) назовем
леворекурсивным, ссимвол N C R(N) - праворекурсивным.
2. Составляется матрица предшествования и определяются все
нарушения единственности отношений предшествования, и если их
нет, то производится остановка. Если они есть, то осуществляется
переход к 3.
3. К каждому праворекурсивному символу применяется процеду-
ра ограниченного правого расширения, которая заключается в том,
что каждый праворекурсивный символ N заменяется символом N1 и
вводится новое грамматическое правило N1::=N. Символ N не заме-
няется на символ N1, если в данном грамматическом правиле он яв-
ляется самым правым символом.
4. К каждому леворекурсивному символу применяется процедура
ограниченного левого расширения, которая заключается в том, что
каждый леворекурсивный символ N заменяется символом N2 и вводит-
ся новое грамматическое правило N2::=N. Символ N не заменяется
символом N2, если в данном грамматическом правиле он является са-
мым левым символом.
5. Если выполнен п. 3 или 4, то осуществляется переход к
п.1. Если нет, то осуществляется переход к п.6.
6. По матрице предшествования находятся пары символов X,Y и
P,Q такие, что X = Y и P = Q.
а. Если X C R(P) и выполняется одно из следующих условий: Q
и Y являются одними и теми же символами или Q C L(Y) или Y C L(Q)
или существует символ D такой, что D C (L(Q) /\ L(Y)), то к сим-
волу X применяется процедура ограниченного правого расширения.
б. Если Y C L(Q) а P и X являются одними и теми же символа-
ми, то к символу Y применяется процедура ограниченного левого
расширения и осуществляется переход к п.1.
ЛЕКЦИЯ 5
АНАЛИЗ КОНТЕКСТНО-ЗАВИСИМЫХ ЯЗЫКОВ
С ПОМОЩЬЮ МАТРИЦ ПРЕДШЕСТВОВАНИЯ
Наиболее распространенные грамматики, описывающие языки
программирования, - это КС-грамматики (грамматики типа 2 в клас-
сификации Хомского). Однако описание языков программирования
грамматиками типа 1, т.е. контекстно-зависимыми (КЗ) грамматика-
ми во многих случаях может облегчить как сам процесс описанияопи-
сания языка, так и построение транслятора.
Рассмотрим алгоритм анализа программы, если алгоритмический
язык описан неукорачивающей грамматикой. Класс неукорачивающих
грамматик совпадает с классом КЗ-грамматик (грамматики типа 1 по
Хомскому).
Грамматика G является неукорачивающей, если для каждого
правила x ::= y С Ф выполняется условие │ x │<=│ y │, где │ x │
i i i i i
и │ y │ - число символов, входящих в строки x и y соответ-
i i i
ственно.
Множества самых левых Л(В) и самых правых П(В) символов сим-
вола В:
L(В) = { U/ ] (Be ::= Ux) С Ф \/ ] (Be ::= B1x) С Ф /\
/\ U С L(В1) };
R(В) = { U/ ] (eB ::= xU) С Ф \/ ] (eB ::= xB1) С Ф /\
/\ U С R(В1) };
где e, x - строки, возможно, пустые; В, В1, U С Vt U Vn.
Из этих определений следует, что терминальные символы также
могут обладать непустым множеством самых левых (правых) символов
при условии, что они использовались как начальные (конечные) сим-
волы в строках грамматических правил (x i ::= y i) С Ф.
Из определения L(В) непосредственно следует, что если a ->
b, т. е. a = f1 -> f2 -> ... -> fk = b (k>1) и a = Bx, то на-
чальные символы строк f i = Ux i (1<i<=k), в которых начальный
символ U участвовал в строках y правил вывода, принадлежат мно-
жеству L(В).
Аналогично из определения R(В) непосредственно следует, что
если a -> b, a = xB и, кроме того, a = f1 -> f2 -> ... -> fk =
= b (k>1), то конечные символы строк f i = x i U (1<i<=k), в кото-
рых конечный символ U участвовал в правилах вывода, принадлежат
множеству R(В).
Определим отношения предшествования для неукорачивающих
грамматик:
В = В <--> ] ф (ф: g ::= xB B y) С Ф;
i j i j
B < B <--> ] ф (ф: g ::= xB Ny) С Ф /\ B С Л(N);
i j i j
B > B <--> ] ф (ф: g ::= xNB y) С Ф /\ B С П(N) \/
i j j i
\/ ] ф (ф: g ::= xNN y) С Ф /\ B С П(N) /\ B С Л(N );
1 i i 1
где x и y - строки, возможно, пустые, N, N , С V U V .
1 t n
Описание языка программирования неукорачивающими грамматика-
ми позволяет вводить символы языка типа IF, BEGIN, ELSE, FOR и
идентификаторы стандартных функций типа SIN, COS, EXP и т. п. в
виде нетерминальных символов в грамматические правила, что облег-
чает анализ этих символов и ускоряет процесс трансляции.
Теперь рассмотрим алгоритм анализа входного текста, написан-
ного на языке, порожденном неукорачивающей грамматикой предшест-
вования типа 1 по классификации Хомского. Блок-схема алгоритма
показана на рис. 4.
┌─────┐
│ 1 │
└──┬──┘
┌──────────────────────┼───────────────────┐
│ │ │ нет
│ ┌────┐ пусто / \ = ┌─────┐ / \
│ │ 15 ├───┬───────< 2 >────┤ 3 ├────< 4 >
│ └─┬─┬┘ │ \ / < └─────┘ \ /
│ │ │ │ ├─────────┐ │ да
│ │ │ │ │ │ │
│ │ │ │ пусто / \ = ┌──┴──┐ / \ ┌────┐
│ │ │ └───────< 5 >────┤ 6 │ < 13>─1──┤ 14 │
│ │ │ \ / └─────┘ \ / да └────┘
│ │ │ │< нет│
│ │ └──────────────┼───────────────────┘
│ │ / \
│ └──────────────< 7 >
│ нет \ /
│ │ да
│ ┌──┴──┐
│ │ 8 │
│ └──┬──┘
│ ┌──┴──┐
│ │ 9 │
│ └──┬──┘
│ ├────────┐
│ ┌────┐ / \ ┌─┴───┐
└─┤ 11 ├─────────────<10 >────┤ 12 │
└────┘ = \ / =/= └─────┘
Рис.2
1. k,i=1 8. q := │Xn│
Мi=1 9. ГП
2. Ri ? Lk2 10. q=1
3. i=i+1 Ri=Lk3 11. i::=j
k=k+1 12. k=k-1
j=i Lk=n(q)
4. Ri=4 q=q-1
5. Rj=Ri 13. Y=R1...Ri
6. j=j-1 14. выход
7. Сущ. правило Y =Rj...Ri 15. обработка ошибки
При анализе входного текста используется стек R, работающий
по правилу FILO (first in, last out), чтение исходного текста
производится слева направо, а дерево сворачивания строится снизу
вверх.
Алгоритм в каждом текущем предложении Si выделяет самую ле-
вую строку, заключенную между отношениями > и <, и заменяет ее по
соответствующему правилу грамматики. Если грамматика предшество-
вания не имеет двух правил с одинаковыми строками y i, то данный
алгоритм для каждого предложения S L(G) всегда порождает одну и
ту же последовательность правил сворачивания. Для того, чтобы
строки сворачивания всегда были заключены между отношениями > и
<, в грамматиках анализируемых языков, как и в случае КС-грамма-
тик, вводятся ограничители начала и конца текста @.
В начале анализа строки в верхнюю ячейку стека R записыва-
ется первый символ программы, т. е. символ @. Индексам i (номер
символов стека R) и k (номер смвола входного текста) присваива-
ются начальные значения 1.
Блок 2. Проверяется отношение предшествования между верхним
символом стека Ri и очередным символом входного текста Lk . Если
между ними отношение вида _ (пусто), значит во входном тексте до-
пущена ошибка.
Блок 3 записывает в стек R очередной символ входного текста.
Блок 4 выделяет признак окончания текста Ri = @.
Блоки 5 и 6 определяют левую границу сворачиваемой строки.
Для этого проверяется отношение предшествования между каждой па-
рой символов R и R до выполнения условия R < R . В блоке
j-1 j j-1 j
5 не предусмотрен выход при выполнении условия R > R , так как
j-1 j
такое условие не может иметь место; вообще в процессе работы ал-
горитма в стеке R не может появиться пара символов с отношением
>, так как это исключает сам алгоритм.
Блок 7 производит поиск правила y=Rj,...,Ri по таблице грам-
матических правил и запоминает номер правила n, если оно найдено.
Блок 8 записывает в счетчик q число символов строки
Xn (q:=│Xn│).
Блок 9 производит переход на генерирующую подпрограмму.
Блок 10 проверяет длину строки Xn.
Блок 11 "выталкивает" символы Rj,..., Ri из стека R и запи-
сывает в него первый символ строки Xn, обозначенный на блок-схе-
ме Xn(1).
Блок 12 записывает все символы строки Xn, кроме первого, пе-
ред первым непрочитанным символом входного текста. Это не вы-
зовет переполнения массива L, так как по условию │Xn│ <= │Yn│.
Следовательно, число символов строки x не может быть больше чис-
ла символов строки y.
Блок 13 проверяет, выполняется ли правило R1,...,Ri. Если
такое правило выполняется, то алгоритм анализа и свертывания
входного текста закончен. Если такое правило не выполняется, зна-
чит данный текст из-за допущенной ошибки не может быть свернут.
Описания языков программирования КС- и даже неукорачивающи-
ми грамматиками вынуждает переносить определение части формаль-
ных условий из синтаксиса в семантику. Например, в ФОРТРАНе при
анализе каждого идентификатора необходимо определить, является
ли его описание явным или неявным. Если описание явное, то опре-
деляется тип идентификатора.
Пример:
Имеется грамматика:
Vт = { А, B }, Vn = { S, D, H, B`, A` }
1: S -> A`SD
2: S -> A`B`A
3: AD -> HA
4: H -> D
5: B` -> B
6: BD -> BB`A
7: A` -> A
Эта грамматика порождает язык следующего вида:
(А**n)(B**n)(А**n) ; n - степень
L(S) =A`,A,H,D R(S)=A, D
L(B`)=B R(B`)=B
L(A`)=A,H,D R(A`)=A
L(H) =D R(H)=D, A
L(D) =" R(D)=A
L(B) =B R(B)=" (неопределено)
L(A) =H,D R(A)="
Матрица предшествования:
│
S │ = =
B`│ < < =
A`│ = = < < < <
H │ < < =
D │ > > > >
A │ > > > > > > >
B │ = > > > <
@ │ = < < < <
───┼────────────────────────
│ S B` A` H D A B @
Дерево вывода на основе матрицы и правил:
┌──────────────────────────┐
S+──┐ ┌─────────────А+ +D
│ │ │ \ /
│ S+─── +─────+B` H +────+3
│ │ │ │ │
│ │ +5 4+ │
│ │ │ │ │
│ │ +B D+ │
│ │ │ │ │
+А` +А` └──+──┘ │
│ │ ┌──/│\──┐ │
│ │ │ │ │ │
+1 +1 │ +B` │ │
│ │ │ │ │ │
│ │ │ +5 │ │
│ │ │ │ │ │
А А B В А А
Ф : BA ─> BDA B. .A
\./
./│\.
B D A
ЭКВИВАЛЕНТНЫЕ ПРЕОБРАЗОВАНИЯ ПОРОЖДАЮЩИХ ГРАММАТИК
В ГРАММАТИКИ ПРЕДШЕСТВОВАНИЯ
Грамматики называются эквивалентными, если порождающие их
языки совпадают.
Теорема 1. Пусть в порожденной грамматике имеется одно или
несколько вхождений строки y в правые части порождающих правил.
Преобразование грамматики путем введения нового нетерминала Ф,
нового правила вывода Ф::=y и замена всех или части вхождений
строк y на вхождения нового символа порождает новую грамматику,
эквивалентную исходной.
G - грамматика.
Строка АВ, которая входит хотя бы в одну часть грамматичес-
кого правила, называется парой. Если А С R(А), В С L(В), то пара
рекурсивно-неоднозначна.
Грамматика, не содержащая рекурсивно-неоднозначных пар сим-
волов, называется рекурсивно-приведенной. Любая приведенная грам-
матика - рекурсивно-приведенная.
АЛГОРИТМ ПРЕОБРАЗОВАНИЯ РЕКУРСИВНО-НЕОДНОЗНАЧНОЙ ГРАММАТИКИ
В ГРАММАТИКУ ПРЕДШЕСТВОВАНИЯ
1. Находим множество правых символов. Выбираем все рекурсив-
ные символы грамматики (А R(А)). Грамматические правила имеют
вид: Ф::=(psi). Выделяем все вхождения этих символов в строки
psi, при условии, что они являются левыми частями пар. Для каждо-
го выбранного символа, имеющего такое вхождение, введем новый не-
терминальный символ А, новое правило вывода и заменим все выде-
ленные вхождения А на вхождения А`.
2. Для каждого нового правила А`::= А ищем другое правило
вида Ф ::= А, и если R(А) не содержит последнего символа строки,
то заменяем правило Ф ::= А на Ф ::= А`.
3. Находим множество левых символов. Выбираем все леворекур-
сивные символы В L(B), при условии, что В - правый член некото-
рой пары. Выделим все вхождения этих символов в строку (psi), при
условии, что эти вхождения являются правыми членами пар. Для каж-
дого В, имеющего такое вхождение, вводим В`, В`::= В и заменим
все вхождения В на вхождения В`.
4. Для каждого нового правила В` ::= В ищем в G другие пра-
вила вида Ф ::= В, при условии, что правые части этих правил сов-
падают. Если L(B) не содержит первого символа строки Ф, то заме-
няем правило Ф ::= В на Ф ::= В`.
5. Находим множество левых и правых символов для полученной
грамматики, находим матрицу отношений предшествования, если мат-
рица однозначна выход.
6. Перебирая все вхождения пар во всех строках (psi), отли-
чаем вхождения таких пар АiDi, где неоднозначность типа >< или >=
имеется либо между Аi и В L(Di). Для каждого отличного вхождения
пары АiDi в строку (psi)m выделяем подстроку хАi, для нее вводим
нетерминал Nj и новое грамматическое правило вида Nj ::= xАi. В
правых строках грамматических правил выделенные вхождения цепоч-
ки xАi заменяем новым символом Nj. Если в одной строке в правой
части выделено несколько вхождений таких цепочек, то надо после-
довательно заменять сначала более длинные, а потом короткие. Если
в правых частях грамматического правила выделено несколько строк,
то замена выполняется сначала для самых длинных строк, потом для
самых коротких.
7. Потом повторяется п. 5.
8. Перебирая все вхождения пар в правых частях грамматичес-
ких правил, выбираем (отмечаем) пары АiDi, где имеется неодноз-
начность. Для каждого отмеченного вхождения вычисляем строку Di y.
Для каждой выделенной подстроки, отличной от других, вводит-
ся новый нетерминал вида Nj ::= Di y. Для всех выделенных под-
строк грамматика будет однозначной.
Грамматики предшествования.
L(U)={S/Эz(U=>Sz)}
R(U)={S/Эz(U=>zS)}
Si = Sj ::= Э F (F: U::=xSiSjy)
Si < Sj ::= Э F ((F: U::=xSiUly) & Si{-L(Ul))
Si > Sj ::= Э F ((F: U::=xUkSjy) & Si{-R(Uk)) v
Э F ((F: U::=xUkUly) & Si{-R(Uk) & Sj {- L(Ul))
Алгоритм.
Обозначения:
L - строка анализируемого текста;
L(k) - к-й символ L;
S - стек реализации процесса свертывания;
S(i) - i-й элемент стека S
u(l),w(l),fi(l),psi(l) - соответственно цепочки u,w,fi,psi
правила P(l);
│u(l)│,│w(l)│,│fi(l)│,│psi(l)│ - длины цепочек u,w,fi,psi;
n - индекс самого нижнего символа S(n), такого что
S(n).x.S(n+1);
m - указатель существования в S пропущенной основы;
│p│ - число правил в грамматике.
Блок 1. Инициирует работу алгоритма.
i=k=1; n=max; m=0; Si=1;
Блок 2. Обработка ошибок.
Блоки 3,4. Нахождение правой границы основы свертывания.
3: S(i) ? L(k) =< - 4, >,>< - 6
4: j=i=i+1;
S(i)=L(k);
k=k+1;
Блоки 5,6. Запоминают n.
5: n=i;
6: S(i).><. L(k) & n>i да - 5, нет - 8.
Блоки 8,9. Нахождение левой границы основы свертывания.
8: S(j-1) >< S(j), < - 7, = - 9
9: j=j-1;
Блок 7. Начальное значение номеру грамматического правила Р
l=0
Блок 10. Анализ завершения просмотра всех правил.
l=│p│ да - 12, нет - 11.
Блок 11. Переход на просмотр следующего правила.
l=l+1;
Блок 12 проверяет возможность анализа при отсутствии правил
вида (u fi, u psi) для свертывания выделенной основы.
m=0;
Блок 14 проверяет возможность запоминания выделенной основы
в S.
n=i;
Блок 16 - возврат на первую из пропущенных основ.
L(k-n+j)...L(k+1)=S(n)...S(i)
i=j=n;
m=0;
k=k-n+1;
Блоки 13,15,17 проверяют соответствие строк u(l), w(l), fi
(l) выделенной основе и контекстным строкам, ее окружающим.
ЛЕКЦИЯ 6
LR(k)-ГРАММАТИКИ И СООТВЕТСТВУЮЩИЙ АНАЛИЗАТОР
Анализ для LR(k) - грамматики называется методом Кнута.
LR(k)-анализатор - устройство из неограниченных вправо вход-
ной ленты, верхнего и нижнего магазинов.
На входной ленте помещается еще не обработанная правая под-
цепочка анализируемой цепочки. К анализируемой цепочке справа
приписано k маркеров.
В верхнем магазине - цепочки-символы состояний анализатора.
Состояния подбираются так, чтобы они соответствовали возможным
вариантам дерева вывода на различных тактах анализа.
В нижнем магазине - частично свернутые левые подцепочки
входной цепочки (обработанные анализатором).
На каждом такте работы анализатора может выполняться одно из
действий: сдвиг или свертка. После выполнения определенного коли-
чества тактов анализатор допускает или отвергает анализируемую
цепочку.
Выполнение каждого такта можно разбить на 2 этапа. На 1 -
преобразование информации нижнего (и, возможно, верхнего) магази-
нов. Информация для выполнения 1 этапа - правое состояние цепоч-
ки состояний (верхний магазин) и к левых символов не обработан-
ной части входной цепочки. Если действие - сдвиг, в нижний мага-
зин записывается терминал из левого символа входной цепочки. Вер-
хний магазин остается без изменений. Если действие - свертка, то
из нижнего и верхнего магазинов исключается по одинаковому числу
символов, в нижний магазин записывается нетерминал (правая часть
гр.правила). Входная цепочка - без изменений. Информация для
свертки - правые состояние и символ из верхнего и нижнего магази-
нов соответственно (после выполнения 1 этапа). Запись в верхний
магазин "переходного состояния".
Свертка соответствует случаю использования некоторого прави-
ла вывода порождающей грамматики. Символы, исключаемые из нижне-
го магазина, соответствуют правой, а символ, записываемый в мага-
зин - левой части грамматического правила.
ОПРЕДЕЛЕНИЕ: LR(k)-анализатор, соответствующий G=(Vt,Vn,P,S)
- LR(k)=(U,X,H,T,b1,b2,S0,Z0,Sr), где:
U - конечное множество состояний анализатора;
X - конечное множество входных символов, Х=Vt U # -маркер;
H - конечное множество H=(Vt U Vn U Z0),
Х=Vt U # - маркер;
T - {Q U (p,A)}, A C Vn,
p - положительное целое,
Q - сдвиг,
пара (p,A) - cвертка, с исключением p символов из
магазинов и записью в нижний магазин
символа А
k k
b1 - (UxX ) -> T, X - цепочки длины k над алфавитом X;
b1 - частично определенная функция, задающая первые этапы
тактов анализа b2 - (UxH) -> U;
b2 - частично определенная функция, задающая вторые этапы
тактов анализа;
S0 - S0 C U - начальное состояние;
Z0 - граничный маркер;
Sr - Sr C U, заключительное состояние.
Для любой LR(k) - грамматики можно построить LR(1) - грамма-
тику, допускающую тот же язык.
ОПРЕДЕЛЕНИЕ: Автомат с магазинной памятью (сокращенно МП-ав-
томат) - это семерка
P = (Q, X, Г, b, q0, Z0, F),
где:
Q - конечное множество символов состояний, представляющих
всевозможные состояния управляющего устройства;
Х - конечный входной алфавит;
Г - конечный алфавит магазинных символов;
b - отображение множества Q * (X U {e}) * Г в множество ко-
нечных модмножеств множества Q * Г";
q0 C Q - начальное состояние управляющего устройства;
Z0 С Г - символ, находящийся в магазине в начальный момент
(начальный символ);
F C Q - множество заключительных состояний.
ОПРЕДЕЛЕНИЕ: МП-автомат P=(Q,X,Г,b,q0,Z0,F) называется де-
терминированным (сокращенно ДМП-автоматом), если для каждых q C Q
и Z C Г либо
1) b (q,a,Z) содержит не более одного элемента для каждого
а С Х и b (q,e,Z) = 0, либо
2) b (q,a,Z) = 0 для всех а С Х и b (q,e,Z) cодержит не бо-
лее одного элемента.
Утверждение. Любой LR(k) - анализатор может быть преобразо-
ван в детерминированный МП-автомат.
При доказательстве этого утверждения используют свойство
анализатора записывать по одному символу в верхний и нижний мага-
зины. Исключаться из магазина эти символы могут только одновре-
менно и только на этапе свертки, следовательно верхний магазин
может быть исключен, если каждый символ нижнего магазина снаб-
дить индексом. Индекс соответствует тому состоянию, которое запи-
сывается в нижний магазин. Каждый символ нижнего магазина должен
иметь N модификаций, где N - число состояний анализатора, соот-
ветствующих этому символу.
Для любого языка, распознаваемого LR(k)-анализатором, сущес-
твует распознающий этот язык LR(1)-анализатор (класс языков,
распознаваемых LR(k)-анализатором, совпадает с классом языков
LR(1)-анализатора. Входит в класс несущественно неоднозначных
УКС-языков.
Функции b1 и b2 обычно задаются в виде общей таблицы, сос-
тоящей из конечного числа "рядов". Каждый ряд соответствуют неко-
торому состоянию и имеет следующую структуру:
- состояние;
- наблюдаемая цепочка;
- функция действия (b1);
- символ нижнего м-на;
- функция b2 (переходное состояние). Для заключительного
состояния Sr имеется сл. строка:
- состояние Sr;
- наблюдаемая цепочка - ##### - k раз - маркеры Z0;
- функция действия (b1) - допуск;
- символ нижнего м-на;
- функция b2 (переходное состояние). Таблица наз. анализи-
рующей таблицей LR(k)-анализатора.
┌──────────┬───────────────────┬─────┬───────────────┬──────┐
│ │ k │ │ │ │
│ U │ X │ b1 │ H │ b2 │
│ Состояние│ Наблюдаемая строка│ │ Символ нижнего│ │
│ │ │ │ магазина │ │
├──────────┼───────────────────┼─────┼───────────────┼──────┤
│ │ Xi1 │(p,A)│ Zi1 │ Si1 │
│ ├───────────────────┼─────┼───────────────┼──────┤
│ │ Xi2 │ Q │ Zi2 │ Si2 │
│ ├───────────────────┼─────┼───────────────┼──────┤
│ │ ... │(p,B)│ ... │ ... │
│ ├───────────────────┼─────┼───────────────┼──────┤
│ │ Xij │ Q │ Zij │ Sij │
│ ├───────────────────┼─────┼───────────────┼──────┤
│ │ ... │ ... │ ... │ ... │
└──────────┴───────────────────┴─────┴───────────────┴──────┘
LR(k)-грамматики образут широкий класс грамматик, анализи-
руемых естественным образом снизу вверх с помощью ДМП-автомата.
Пусть ах - правовыводимая цепочка какой-то грамматики при-
чем а - либо пустая цепочка либо оканчивается нетерминалом. Тог-
да а называется открытой частью цепочки ах , х - замкнутой. Гра-
ницу между а и х назовем рубежом.
Алгоритм разбора типа "перенос-свертка" можно рассматривать
как программу расширенного ДМП-преобразователя который проводит
разбор "снизу-вверх". Для данной входной цепочки W ДМП-преобразо-
ватель смоделирует в обратном порядке ее правый вывод.
S=a(0)=>a(1)=>...=>a(m)=W
Это правый вывод цепочки W. Каждая правовыводимая цепочка
а(i) распределяется в памяти ДМП так, что ее открытая часть хра-
нится в магазине а замкнутая - на входной ленте справа от голов-
ки. Затем ДМП должен локализовать правый конец основы и сделать
свертку. Число принимаемых ДМП решений - два: решения о переносе
и о свертке (по конкретному правилу).
Грамматика будет LR(K) грамматикой, если для произвольного
правого вывода
S=a(0)=>a(1)=>...=>a(m)=Z
в каждой правовыводимой цепочки а(i), читая ее слева напра-
во, можно выделить основу и определить, каким нетерминалом надо
ее заменить, дойдя при этом не более чем до k-го символа, распо-
ложенного справа от правого конца этой основы.
ОПРЕДЕЛЕНИЕ:
Пусть G=(N,E,P,S) - КС грамматика.
Пополненной грамматикой, полученной из G, будем называть
G'=(N+S',E,P+{S'->S},S').
S' - новый начальный символ, не принадлежащий N.
S' -> S - это нулевое правило грамматики G', добавляемое для
того, чтобы свертку , в которой используется нулевое правило,
можно было интерпретировать как сигнал о том, что цепочка допус-
кается.
ОПРЕДЕЛЕНИЕ: пусть G - КС грамматика, а G' - полученная из
нее пополненная. Будем называть G LR(k) грамматикой для k >= 0,
если из условий:
1) S' -> aAw -> abw;
2) S' -> gBx -> aby;
3) FIRST(k;w) = FIRST(k;y) где k соответствует грамматике.
Из условий следует, что
aAy = gBx (т.е. a=g, A=B, x=y)
Интуитивно это определение говорит о том, что если abw и aby
-правовыводимые цепочки пополненной грамматики, у которых
FIRST(w)=FIRST(y), и A->b -последнее правило, использованное в
правом выводе цепочки abw, то правило A->b должно использоваться
также в правом разборе при свертке aby к aAy.
Так как A дает b независимо от w, то LR(k) условие говорит о
том , что в FIRST(w) содержится информация, достаточная для того,
что ab за один шаг выводится из aA. Поэтому никогда не может воз-
никнуть сомнений относительно того, как свернуть очередную право-
выводимую цепочку пополненной грамматики.
Кроме того, работая с LR(k)-грамматикой, мы всегда знаем,
допустить ли данную входную цепочку или продолжать разбор.
Если начальный символ S не встречается в правых частях пра-
вил, то в определении LR(k) грамматики вместо S' можно взять S, а
именно G будет LR-грамматикой, если из трех условий:
1: S=>aAw=>abw;
2: S=>yBx=>aby;
3: FIRST(w)=FIRST(y)
следует, что aAy=yBX.
В данном разделе мы кратко рассмотрим, как для каждой
LR-грамматики G можно построить детерминированный правый анализа-
тор, который ведет себя следующим образом.
Прежде всего, этот анализатор строится по пополненной грам-
матике G'. Ведет он себя также, как анализатор типа "пере-
нос-свертка", за исключением того, что после каждого символа
грамматики в магазин будет записываться специальный информацион-
ный символ, называемый LR(k)-таблицей, которые могут определить,
что нужно делать на очередном шаге-свертку или перенос, и в слу-
чае свертки - номер правила.
┌──────────┬─────────────────────┬──────────────────────┐
│состояния │ действие │ переход │
├──────────┼─────────────────────┼──────────────────────┤
│ │ a b e │ S a b │
│ │ │ │
│ │ │ │
│ T0 │ 2 X 2 │ T1 X X │
│ │ │ │
│ Т1 │ S X A │ X T2 X │
│ │ │ │
│ T2 │ 2 2 X │ T3 X X │
│ │ │ │
│ T3 │ S S X │ X T4 T5│
│ │ │ │
│ T4 │ 2 2 X │ T6 X X │
│ │ │ │
│ T5 │ 1 X 1 │ X X X │
│ │ │ │
│ T6 │ S S X │ X T4 T7│
│ │ │ │
│ T7 │ 1 1 X │ X X X │
└──────────┴─────────────────────┴──────────────────────┘
Рис. LR(1) анализатор для грамматики G (i-свертка,при кото-
рой применено i-е правило, S-перенос, A-допуск, X-ошибка.
Возьмем для примера грамматику G. Ee правила:
1:S->SaSb
2:S->e
и правый вывод S->SaSb->SaSaSbb->SaSabb->Saabb->aabb.
Это LR(1)-грамматика.
Пополненная грамматика состоит G' правил:
0:S'->S
1:S ->SaSb
2:S ->e
LR(1)-анализатор для грамматики G приведен на Рис.
LR(k)-анализатор для КС-грамматики G - это множество строк
большой таблицы, каждая строка которой называется LR(k)-таблицей.
Т0 выделяется в качестве начальной LR(k)-таблицы. Каждая из
таблиц состоит из двух функций - функции действия f и функции пе-
реходов g:
(1) Аргументом функции действия f служит аванцепочка, а
соответствующее значение функции f - один из символов "действий":
перенос, свертка i, ошибка или допуск;
(2) Аргументом функции переходов g служит символ X, принад-
лежащий N+E, а соответствующее значение g(X)-либо имя некоторой
LR(k)-таблицы, либо ошибка.
LR-анализатор ведет себя также, как алгоритм типа "пере-
нос-свертка", используя в процессе работы магазин, входную и вы-
ходную ленты. Вначале магазин содержит начальную таблицу Т0 и ни-
чего больше. На входной ленте находится анализируемая цепочка, а
выходная лента вначале пустая. Если предположить, что надо разоб-
рать входную цепочку aabb ,то начальной конфигурацией анализато-
ра будет (T0,aabb,e). Далее разбор осуществляется по следующему
алгоритму.
LR(k)-алгоритм разбора
Вход. Множество LR(k) таблиц для грамматики G с начальной
таблицей Т0 и входная цепочка z , которую надо разобрать.
Выход. Если z+ L(G), то правый разбор цепочки z в граммати-
ке, в противном случае сигнал об ошибке.
Метод. Выполнять шаги (1) и (2) до тех пор, пока не будет
допущена входная цепочка или не встретится сигнал об ошибке. В
случае допуска цепочка на выходной ленте представляет собой пра-
вый разбор цепочки z.
(1) Определяется аванцепочка u ,состоящая из k очередных
входных символов (или менее чем k символов ,если обрабатывается
конец входной цепочки)
(2) Функция действия f таблицы ,расположенной наверху мага-
зина, применяется к аванцепочке u.
(а) Если f(u) =перенос, то следующий входной символ, скажем
a ,переносится со входа в магазин. К a применяется функция пере-
ходов g верхней таблицы магазина и определяется новая таблица,ко-
торую надо поместиь наверху магазина. После этого вернуться к ша-
гу (1). Если следующего входного символа нет или значение g(a) не
определено, остановиться и выдать сигнал об ошибке.
(б) Если f(u) свертка i и A->a-правило с номером i , то из
верхней части магазина устраняется 2|a| символов и на выходной
ленте записывается номер правила i. Наверху магазина оказывается
тргда новая таблица T', и ее функция переходов применяется к А
для определения следующей таблицы, которую надо поместить навер-
ху магазина. Помещаем А и эту новую таблицу наверху магазина и
переходим к шагу (1).
(в) Если f(u)= ошибка , разбор прекращается (на практике на-
до перейти к процедуре исправления ошибок).
(г) Если f(u) =допуск, остановиться и обьявить цепочку, за-
писанную на выходной ленте, правым разбором первоначальной вход-
ной цепочки.
Конец работы алгоритма.
G является LR -грамматикой тогда и только тогда , когда для
нее можно построить LR(k)-анализатор. Она также будет LR-грамма-
тикой, если просмотрев только часть кроны дерева вывода в этой
грамматике, расположенную слева от данной внутренней вершины, и
часть кроны , выведенную из нее, а также следующие k терми-
нальных символов, можно установить, какое правило было применено
к этой вершине.
Определение. Допустим, что S->aAw->abw- правый вывод в грам-
матике. Назовем цепочку g АКТИВНЫМ ПРЕФИКСОМ грамматики, если
gпрефикс цепочки ab, т.е g- префикс некоторой правовыводимой це-
почки, не выходящие за правый конец ее основы.
Ядро анализатора составляют таблицы. Для LR(k)-грамматики
каждая таблица соответствует некоторому активному префиксу. Таб-
лица, соответствующая активному префиксу g, для данной аванцепоч-
ки. состоящей из k символов, сообщает о том достигнут ли правый-
конец основы. Если да, то она сообщает также какова эта основа и
какое правило надо применить для ее свертки.
LR(k)-условие говорит о том, что основу правовыводимой це-
почки можо определить неоднозначно, если известен весь отрезок
этой цепочки слева от основы, а также k очередных входных симво-
лов. Поэтому не очевидно, что основу всегда можно определить,
располагая только фиксированным количеством информации о цепочке,
предшествующей основе. Поэтому таблицы должны содержать достаточ-
но информации, чтобы по таблице, соответствующей ab, можно было
вычислить таблицу для aA, если aAw->abw.
Определение. Пусть G - КС-грамматика. Будем называть
[A->b1*b2,u] LR-ситуацией, если A->b1b2-правило из P и u принад-
лежит входной цепочке.
Определение. Пусть G-КС-грамматика. g-ее активный префикс.
Тогда V(g) -множество LR(k)-ситуаций, допустимых для g.
Чтобы помочь анализатору принять правильное решение, в нуж-
ных ячейках магазина будут находиться LR-таблицы, содержащие
необходимую информацию, извлеченную из соответствующего множес-
тва ситуаций. Следовательно, построение правого анализатора сос-
тоит в нахождении LR-таблиц, соответствующих этим ситуациям.
На первый взгляд кажется, что при реализации анализаторов
придется помещать в магазин большие таблицы. Этого можно избе-
жать следующим образом:
(1) Поместить в память по одному экземпляру каждой таблицы,
а в магазине заменить сами таблицы указателями на их место в па-
мяти;
(2) Так как в таблицах есть ссылки на другие таблицы, вмес-
то имен таблиц можно использовать указатели.
Наличие в магазине символов грамматики излишне и на практи-
ке их можно туда не записывать.
ЛЕКЦИЯ 7
МП-АВТОМАТЫ
Изучая конечные автоматы, мы изучили теоpию, охватывающую
пpоблемы pаспознования. При использовании конечных автоматов в
пpактических задачах такие аспекты обpаботки цепочек как выходы
из цепочек и обpаботка значений pешались с помощью пеpеходных
пpоцедуp, задаваемых в зависимости от конкpетного случая. Так как
почти всегда пpоцедуpы могли быть описаны коpотко и пpосто, то мы
сделали вывод: теоpия конечных pаспознований является адекватной
теоpетической базой для pазpаботки конечных пpоцессоpов.
В этом пункте мы pассмотpим pаспознование входных цепочек с
помощью МП-автоматов. В отличие от конечного pаспознавателя для
МП-pаспознавателя стpоить соответствующие pасшиpения достаточно
тpудно, поэтому теоpия pаспознования КС-гpамматик сама по себе не
стpоит адекватной теоpии для постpоения компилятоpов.
Все методы тpансляции, котоpые будут pассмотpены ниже, осно-
вываются на технике, в котоpой пpоцесс обpаботки КС-языка опpеде-
ляется в теpминах обpаботки каждоого отдельного пpавила соответ-
ствующей гpамматике. Для описания пpоцесса обpаботки , основанно-
го на этой технике , обычно используется пpилагательное "синтак-
сически тpанслиpуемый". Синтаксически упpавляемые методы в дан-
ном КП основываются на математическом понятии "тpанслиpующей
гpамматики" и понятия "атpибутной гpамматики".
Тpанслиpующей гpамматикой или гpамматикой пеpевода называет-
ся КС-гpамматика, множество теpминальных символов котоpого pазби-
то на множество входных символов и множество символов действия.
Цепочки языка, опpеделяемого тpанслиpующей гpамматикой, называют-
ся последовательностью актов.
Атpибутная тpанслиpующая гpамматика - это тpанслиpующая
гpамматика, к котоpой добавляются следующие опpеделения.
1) Каждый входной символ, символ действия или нетеpминал
имеет конечное множество атpибутов, и каждый атpибут имеет (воз-
можно бесконечное) множество допустимых значений;
2) Все атpибуты нетеpминальных символов и символов действия
делятся на наследуемые и синтезиpуемые;
3) Пpавила вычисления наследуемых атpибутов опpеделяются
следующим обpазом:
а) каждому вхождению наследуемого атpибута в пpавую часть
данной пpодукции ставится пpавило вычисления значения этого атpи-
бута как функции некотоpых атpибутов символов, входящих в пpавую
или левую часть пpодукции;
б) задается начальное значение каждого наследуемого атpибу-
та начального символа;
4) Пpавила вычисления синтезиpуемых атpибутов:
а) каждому вхождению синтезиpуемого нетеpминального атpибу-
та в левую часть пpодукции сопоставляется пpавило вычисления зна-
чения этого атpибута как функции некотоpых дpугих атpибутов сим-
волов, входящих в левую или пpавую часть этой пpодукции;
б) каждому синтезиpуемому атpибуту символа действия сопоста-
ляется пpавило вычисления значения этого атpибута как функции не-
котоpых дpугих атpибутов этого символа действия.
Атpибутные гpамматики исользуются для опpеделения атpибут-
ных деpевьев вывода, а затем - атpибутных последовательностей ак-
тов и атpибутных пеpеводов.
Деpевья опpеделяютя следующими пpоцедуpами постpоения:
1. По соответствующей неатpибутной гpамматике постpоить
деpево вывода последовательности актов, состоящих из входных сим-
волов и символов действия без атpибутов.
2. Пpисвоить значения атpибутов входных символов, входящих в
деpево вывода.
3. Пpисвоить начальные значения наследуемым атpибутам на-
чального символа деpева вывода.
4. Вычислить значения атpибутов символов, входящих в деpево
вывода, повтоpяя следующее действие до тех поp, пока оно не ста-
нет невозможным. Найти атpибут, котоpого еще нет в деpеве, но
аpгументы пpавила его вычисления уже имеются, вычислить значение
этого атpибута и добавить его к деpеву.
5. Если выполнение шага 4 пpиведет к тому, что значения всех
атpибутов всех символов деpева окажутся вычисленными, то будем
называть полученное деpево завеpшенным. В пpотивном случае деpе-
во называется незавеpшенным.
ЛЕКЦИЯ 8
ЛЕКСИЧЕСКИЙ АНАЛИЗАТОР. АВТОМАТНЫЕ ГРАММАТИКИ.
РЕГУЛЯРНЫЕ ВЫРАЖЕНИЯ
Лексический анализатор (иногда также называемый сканером)
представляет собой наиболее простую часть компилятора. Лексичес-
кий анализатор просматривает литеры исходной программы слева нап-
раво и строит символы программы - целые числа, идентификаторы,
служебные слова и т.д. В литературе иногда вместо термина символ
используют термины элемент и атом. Символы передаются затем на
обработку фактическому анализатору. На этой стадии может быть ис-
ключен комментарий (именно такой путь исключения комментария был
избран в данном курсовом проекте). Лексический анализатор также
может заносить идентификаторы в таблицу символов, выполнять дру-
гую простую работу, которая фактически не требует анализа исход-
ной программы и может быть проделана на основе анализа отдельных
лексем. Он может выполнить большую часть работы по макрогенера-
ции в тех случаях, когда требуется только текстовая подстановка.
Обычно лексический анализатор передает символы синтакси-
ческому анализатору во внутренней форме. Каждый разделитель (слу-
жебное слово, знак операции или знак пунктуации) будет представ-
лен целым числом. Идентификаторы иликонстанты можно представить
парой чисел. Первое число, отличное от любого целого числа, ис-
пользуется для представления разделителя, характеризует сам "и-
дентификатор" или "константу"; второе число является адресом или
индексом идентификатора или константы в некоторой таблице. Это
позволяет в остальных частях компилятора работать эффективно,
оперируя с символами фиксированной длины, а не с цепочками литер
переменной длины.
Лексический анализатор по своему строению является конечным
автоматом. В этом пункте мы рассмотрим некторые проблемы, связан-
ные с реализацией конечного автомата или процессора как програм-
мы или подпрограммы для вычислительной машины. В этом пункте мы
рассмотрим три взаимосвязанных вопроса:
1. Как представлять выходы, состояния и переходы конечного
автомата, чтобы удовлетворить зачастую противоречивые требования,
предъявляемые к реализации: быстродействие и небольшие затраты
памяти.
2. Как справиться с некоторыми специфическими проблемами,
постоянно возникающими при компиляции.
3. Как расчленить задачу построения компилятора, чтобы полу-
чить автоматы, допускающие простую реализацию.
Некоторые задачи, решаемые с помощью конечных автоматов,
заключаются всего лишь в распознавании конечного множества слов.
Суть этих проблем в том, что компилятор должен обнаружить появле-
ние некоторого слова из такого множества и затем действовать в
зависимости от того, какое это слово. Задачи такого характера на-
зываются проблемой "идентификации", т.к. действия компилятора за-
висят от идентичности некоторому известному слову данного слова.
Так для решения задач идентификации может потребоваться огромное
число состояний, в подобных случаях часто приходится пользо-
ваться специальными методами реализации. По этой причине целесоб-
разно строить компилятор так, чтобы проблема идентификации реша-
лась отдельным подпроцессором, специально предназначенным для
этого.
Существуют проблемы идентификации, которые, строго говоря,
не могут быть решены с помощью конечного автомата. Например, час-
то встречающаяся проблема распознавания переменных или идентифи-
каторов некоторого языка. Решение этой проблемы обычным методом с
помощью конечного автомата потребует несколько состояний и нали-
чия элемента таблицы имен для каждого допустимого идентификатора.
Однако множество допустимых идентификаторов в большинстве языков
бесконечно или так велико, что его вполне можно считать бесконеч-
ным.
Понятно, что для языков, где число допустимых идентификато-
ров бесконечно или практически бесконечно, невозможно отвести
место в памяти или элемент таблицы имен для каждого возможного
идентификатора. Это затруднение преодолевается с помощью понятия
расширяющегося конечного автомата. При считывании своей входной
цепочки этот автомат отводит для идентификатора необходимое мес-
то в памяти и элемент в таблице, как только он впервые встречает-
ся в программе. Если этот идентификатор встречается в программе
снова, то, чтобы идентифицировать его, автомат использует техни-
ку идентификации слов с помощью конечного автомата. Когда появ-
ляется какой-тоновый идентификатор, автомат снова расширяется и
т.д.. Хотя этот автомат не является, строго говоря, конечным, к
нему применимы многие принципы построения и анализа конечных ав-
томатов.
Автоматные гpамматики
К сожалению, не все КС-гpамматики пpигодны для нисходящего
анализа МП-автоматов, так как для многих гpамматик множество всех
допустимых пpодолжений обpаботанной части входной цепочки не
всегда можно пpедставить единственной цепочкой теpминальных и не-
теpминальных символов. Рассмотpим такой класс гpамматик, для ко-
тоpых нисходящие МП-pаспознаватели можно постpоить - так называе-
мые автоматные гpамматики. (Фоpмальное опpеделение дано выше.)
Фактически контекстно-свободная гpамматика является автомат-
ной тогда и только тогда, когда выполняются следующие два условия:
1. Пpавая часть каждого пpавила начинается теpминалом.
2. Если два пpавила имеют совпадающие левые части, то пpа-
вые части этих пpавил должны начинаться pазличными теpминальными
символами. Для данной автоматной гpамматики МП-pаспознаватель с
одним состоянием задается следующим обpазом:
1. Множество входных символов - это множество теpминальных
символов, pасшиенное концевым маpкеpом.
2. Множество магазинных символов состоит из маpкеpа дна, не-
теpминальных символов и теpминалов, котоpые входят в пpавые час-
ти пpавил, кpоме занимающих кpайнюю левую позицию.
3. В начале магазин состоит из маpкеpа дна и начального не-
теpминала.
4. Упpавление pаботой МП-автомата с одним состоянием описы-
вается упpавляющей таблицей, стpоки котоpой помечены магазинными
символами, столбцы входными символами, а элементы описываются ни-
же.
5. Каждому пpавилу гpамматики сопоставляется элемент табли-
цы. Пpавило имеет вид <A> -> ba, где <A> - нетеpминал, b - теpми-
нал, а a - цепочка из теpминалов и нетеpминалов. Этому пpавилу
будет соответствовать элемент в стpоке <A> и столбце b
ЗАМЕНИТЬ (a'), СДВИГ
где a' - цепочка а, записанная в обpатном поpядке. Если
пpавило имеет вид <A> -> b, то вместо ЗАМЕНИТЬ (e) используется
ВЫТОЛКНУТЬ.
6. Если магазинным символом является теpминал b, то элемен-
том таблицы в стpоке b и столбце b будет ВЫТОЛКНУТЬ, СДВИГ.
7. Элементом таблицы, котоpый находится в стpоке маpкеpа дна
и столбце концевого маpкеpа, является ДОПУСТИТЬ.
8. Элементы таблицы, не описанные ни в одном из пунктов 5-7,
заполняются опеpацией ОТВЕРГНУТЬ.
Два огpаничения, наложенные нами на КС-гpамматики, гаpан-
тиpуют, что эти пpавила постpоения МП-автомата всегда будут pабо-
тать. Огpаничение 1 говоpит, что пpодукции гpамматики имеют тpе-
буемую фоpму, а огpаничение 2 нужно для того, чтобы пpи пpимене-
нии пpавила 5 элемент таблицы содеpжал только одну пpодукцию. Та-
ким обpазом, мы пpишли к заключению, что:
- если язык опpеделяется автоматной гpамматикой, то его мож-
но pаспознать с помощью МП-автомата с одним состоянием, ис-
пользующим pасшиpенную магазинную опеpацию ЗАМЕНИТЬ. Можно го-
воpить о тождественности автоматной гpамматики и МП-автомата,
постpоенного по ней.
ЛЕКЦИЯ 9
КРАТКИЕ СВЕДЕНИЯ О ГЕНЕРАТОРЕ ЛЕКСИЧЕСКИХ
АНАЛИЗАТОРОВ LEX
Лексический анализ - это распознавание лексем во входном
потоке символов. Предположим, что задано некоторое конечное мно-
жество слов (лексем) в некотором языке и некоторое входное
слово.Необходимо установить, какой элемент множества (если он су-
ществует) совпадает с данным входным словом.
Обычно лексический анализ выполняется так называемым лекси-
ческим анализатором. Применяется во многих случаях, например, для
построения пакетного редактора или в качестве распознавателя ди-
ректив в диалоговой программе и т.д. Наиболее важное применение
лексического анализатора - это использование его в компиляторе.
Здесь лексический анализатор выполняет функцию программы ввода
данных.
Лексический анализатор выполняет первую стадию компиляции -
читает строки компилируемой программы, выделяет лексемы и пере-
дает их на дальнейшие стадии компиляции (грамматический разбор,
кодогенерацию и т.д.).
Лексический анализатор распознает тип каждой лексемы и соот-
ветствующим образом помечает ее. Например, при компиляции
Си-программы могут быть выделены следующие типы лексем: число,
идентификатор, оператор, ограничитель и т.д.
Лексический анализатор должен не только выделить лексему, но
и выполнить некоторые преобразования. Например, если лексема
- число, то его необходимо перевести во внутреннюю (двоичную)
форму записи как число с плавающей или фиксированной точкой. А
если лексема - идентификатор, то его необходимо разместить в таб-
лице, чтобы в дальнейшем обращаться к нему не по имени, а по ад-
ресу в таблице.
Хотя лексический анализ по своей идее прост, тем не менее
эта фаза работы компилятора часто занимает больше времени, чем
любая другая. Частично это происходит из-за необходимости прос-
матривать и анализировать исходный текст символ за символом.
Иногда даже бывает необходимо вернуть прочитанный символ во вход-
ной поток с тем, чтобы повторить просмотр и анализ. Происходит
это потому, что часто бывает трудно определить, где проходят гра-
ницы лексемы. В тех случаях, когда выделение лексемы затруднено
либо по причине того, что одно регулярное выражение не позволяет
ее однозначно определить, либо из-за того, что лексема является
частью другой, приходится прибегать к контекстно-зависимым алго-
ритмам анализа с использованием левого и правого направлений
просмотра входной цепочки символов.
Lex - частично или полностью автоматизирует процесс написа-
ния программы лексического анализа. Lex - это программирующая
программа или генератор программ. Lex строит программу - лекси-
ческий анализатор на так называемом host-языке (или "главном"
языке). Это значит, что Lex-программа пишется на "языке" Lex, а
Lex-генератор, в свою очередь, генерирует программу лексического
анализа на каком-либо другом языке. Данная версия Lex генерирует
лексические анализаторы на языках Си и Ратфор (рациаональный диа-
лект Фортрана). В качестве host-языка мы будем использовать язык
Си.
Lex-программа имеет следующий формат:
определения %%
правила %%
подпрограммы, составленные пользователем
Любой из этих разделов может быть пустым. Простейшая
Lexпрограмма имеет вид:
%%
Здесь нет никаких определений и никаких правил. Правило сос-
тоит из двух частей:
РЕГУЛЯРНОЕ_ВЫРАЖЕНИЕ ДЕЙСТВИЕ
По регулярным выражениям, содержащимся в левой части правил,
Lex строит детерминированный конечный автомат. Этот автомат осу-
ществляет интерпретацию, а не компиляцию. Количество правил и их
сложность не влияют на скорость лексического анализа, если только
правила не требуют слишком большого об'ема повторных просмотров
входной последовательности символов. Однако, с ростом числа пра-
вил и их сложности растет размер конечного автомата, интерпрети-
рующего их и, следовательно, растет размер Си-программы, реали-
зующей этот конечный автомат. Lex всегда, если не указано другое,
строит выходной файл с именем lexyy.c - Си-программу - лексичес-
кий анализатор.
Если имеется необходимость получить файл с именем, отличным
от lexyy.c, то можно воспользоваться флагом "-t":
% lex -t source.l > file
По этому флагу результат поступает на стандартный вывод. Ре-
гулярные выражения в Lex-правилах определяют лексему. Они могут
содержать символы латинского и русского алфавитов в верхнем и
нижнем регистрах, другие символы (цифры, знаки препинания и
т.д.) и символы-операторы.
Операторы позволяют осуществлять различные действия над вы-
деленной цепочкой символов. Операторы также обозначаются символа-
ми.
Обозначения символов в выражениях. Можно использовать любой
символ. Символ можно указывать в двойных кавычках. В этом случае
это всегда просто символ - его специальное значение отменяется.
. - точка означает любой символ, кроме символа новой строки
"\n";
\восьмеричный_код_символа - указание символа его восьмерич-
ным кодом (как в языке Си);
\n - символ новой строки;
\t - символ табуляции;
\b - возврат курсора на один шаг назад;
Пробел - любой символ пробела в выражении, если он не нахо-
дится внутри квадратных скобок, необходимо заключать в двойные
кавычки. Это необходимо, так как пробел и табуляция используются
Lex в качестве разделителя между определением и действием в пра-
виле.
Операторы регулярных выражений
Операторы обозначаются символами-операторами, к ним относят-
ся:
\ ^ ? * + | $ / %
[] {} () <>
Каждый из этих символов или пар скобок в регулярном выраже-
нии играет роль оператора. Если необходимо отменить специальное
значение символа, обозначающего оператор, перед ним нужно поста-
вить символ "\" или указать его в двойных кавычках.
Оператор выделения классов символов.Квадратные скобки за-
дают классы символов, которые в них заключены.
[abc] означает либо символ "a", либо "b", либо символ "c";
Знак "-" используется для указания любого символа из лекси-
кографически упорядоченной последовательности: [A-z] означает лю-
бой латинский символ;
Повторители
Когда необходимо указать повторяемость вхождения символа в
регулярном выражении, используют операторы-повторители "*" и "+".
Оператор "*" означает любое (в том числе и 0) число вхожде-
ний символа или класса символов. Например: x* любое число вхожде-
ний символа "x"; Оператор "+" означает одно и более вхождений.
Например: x+ одно или более вхождений "x";
Операторы выбора
Операторы: / | ? $ ^ управляют процессом выбора символов.
Оператор "/": ab/cd "ab" учитывается только тогда, когда за
ним следует "cd".
Опeратор "|": ab|cd или "ab", или "cd".
Опeратор "?": x? означает необязательный символ "x".
_?[A-Za-z]* означает, что перед цепочкой любого количества
латинских букв может быть необязательный знак подчеркивания.
-?[0-9]+ выделит любое целое число с необязательным минусом
впереди.
Оператор "$": x$ означает выбрать символ "x", если он яв-
ляется последним в строке. Стоит перед символом "\n"! abc$ озна-
чает выбрать цепочку "abc", если она завершает строку.
Оператор "^": ^x означает выбрать символ "x", если он яв-
ляется первым символом строки; ^abc означает выбрать цепочку сим-
волов "abc", если она начинает строку. [^A-Z]* означает все сим-
волы, кроме прописных латинских букв. Когда символ "^" стоит пе-
ред выражением или внутри "[]", он выполняет операцию дополнение.
Внутри квадратных скобок символ "^" должен обязательно стоять
первым у открывающей скобки!
Оператор {} имеет два различных применения:
x{n,m} здесь n и m натуральные, m > n. Означает от n до m
вхождений x, например, x{2,7} - от 2 до 7 вхождений x;
{имя} вместо "{имя}" в данное место выражения будет подстав-
лено определение имени из области определений Lex-программы.
yytext - это внешний массив символов программы lex.yy.c,
которую строит Lex. yytext формируется в процессе чтения входно-
го файла и содержит текст, для которого установлено соответствие
какому-либо выражению. Этот массив доступен пользовательским раз-
делам Lex-программы.
Оператор <>. Служебные слова START и BEGIN
Раздел правил Lex-программы может содержать активные и неак-
тивные правила. Активные правила выполняются всегда. Неактивные
выполняются только в тех случаях, когда выполняется некоторое на-
чальное условие.
Начальные условия Lex-программы помещаются в раздел опреде-
лений, а неактивные правила помечаются соответствующими условия-
ми. Оператор Start позволяет указать список начальных условий
Lex-программы, а оператор BEGIN позволяет активировать правила,
помеченные начальными условиями.
Активные правила имеют следующий синтаксис:
РЕГУЛЯРНОЕ_ВЫРАЖЕНИЕ ДЕЙСТВИЕ
Неактивные правила имют следующий синтаксис:
<МЕТКА_УСЛОВИЯ>РЕГУЛЯРНОЕ_ВЫРАЖЕНИЕ ДЕЙСТВИЕ
ВАЖНО: любое правило должно начинаться с первой позиции
строки, пробелы и табуляции недопустимы - они используются как
разделители между регулярным выражением и действием в правиле.
Lex-программа может содержать несколько помеченных на-
чальных условий.
Каждое правило, перед которым указан оператор типа "<МЕТ-
КА>", мы будем называть помеченным правилом. Метка формируется
также как и метка в Си.
Количество помеченных правил не ограничивается. Кроме того,
разрешается одно правило помечать несколькими метками, например:
<МЕТКА1,МЕТКА2,МЕТКА3>x ДЕЙСТВИЕ
Запятая - обязательный разделитель списка меток !
Структура Lex-программы
Lex-программа включает разделы опредeлений, правил и пользо-
вательских программ. Рассмотрим подробнее способы оформления этих
разделов.
Все строки, в которых занята первая позиция, относятся к
Lex-программе. Любая строка, не являющаяся частью правила или
действия, которая начинается с пробела или табуляции, копируется
в сгенерированную программу lex.yy.c - результат работы Lex.
Раздел определений Lex-программы
Определения, предназначенные для Lex, помещаются перед пер-
вым %%. Любая строка этого раздела, не содержащаяся между %{ и %}
и начинающаяся в первой колонке, является определением строки
подстановки Lex. Раздел определений Lexпрограммы может включать:
- начальные условия;
- определения;
- фрагменты программы пользователя;
- таблицы наборов символов;
- указатели host-языка;
- изменения размеров внутренних массивов;
- комментарии в формате host-языка.
НАЧАЛЬНЫЕ УСЛОВИЯ задаются в форме:
%START имя1 имя2 ...
Если начальные условия определены, то эта строка должна быть
первой в Lex-программе.
ОПРЕДЕЛЕНИЯ задаются в форме:
имя трансляция
В качестве разделителя используется один или более пробелов
или табуляций. Имя - как обычно, любая последовательность букв и
цифр, начинающаяся с буквы. Трансляция - это регулярное выраже-
ние (или его часть), которое будет подставлено всюду там, где
указано имя (смотрите третью строку этого примера).
ФРАГМЕНТЫ ПРОГРАММЫ ПОЛЬЗОВАТЕЛЯ указываются двумя способами:
- в виде "пробел фрагмент";
- в виде %{ строки фрагмента программы пользователя %}
Такая форма включения пользовательского фрагмента необходи-
ма для ввода, например, макроопределений Си, которые должны начи-
наться в первой колонке строки. Все строки фрагмента пользова-
тельской программы, размещенные в разделе определений, будут яв-
ляться внешними для любой функции программы lex.yy.c
ТАБЛИЦА НАБОРОВ СИМВОЛОВ задается в виде:
%T
целое_число строка_символов
.........
целое_число строка_символов
%T
Сгенерированная программа lex.yy.c осуществляет ввод-вывод
символов посредством библиотечных функций Lex с именами input,
output, unput. Таким образом, Lex помещает в yytext символы в
представлении, используемом в этих библиотечных функциях. Для
внутреннего использования символ представляется целым числом,
значение которого образовано набором битов, представляющих сим-
вол в конкретной ЭВМ. Пользователю предоставляется возможность
менять представление символов (целых констант) с помощью таблицы
наборов символов. Если таблица символов присутствует в разделе
определений, то любой символ, появляющийся либо во входном пото-
ке, либо в правилах, должен быть определен в таблице символов.
Символам нельзя назначать число 0 и число, большее числа, выде-
ленного для внутреннего представления символов конкретной ЭВМ.
УКАЗАТЕЛЬ host-языка имеет вид:
%C для Си;
%R для Ратфора.
Если указатель host-языка отсутствует, то по умолчанию при-
нимается Си.
ИЗМЕНЕНИЯ РАЗМЕРА ВНУТРЕННИХ МАССИВОВ задаются в форме:
%x число
"число" - новый размер массива;
"x" - одна из букв p - позиции;
n - состояния;
e - узлы дерева;
a - упакованные переходы;
k - упакованные классы символов;
o - массив выходных элементов.
Lex имеет внутренние таблицы, размеры которых ограничены.
При построении программы лексического анализа может произойти пе-
реполнение любой из этих таблиц, о чем Lex сообщает при построе-
нии лексического анализатора. Пользователю предоставляется воз-
можность изменить размеры таблиц (сокращая размеры одних и увели-
чивая размеры других) таким образом, чтобы они не переполнялись.
Естественно, эти изменения возможны лишь в пределах той памяти,
которая выделяется под процесс.
Ниже перечислены размеры таблиц, которые устанавливаются по
умолчанию:
p - позиций 1500;
n - состояний 300;
e - узлов 600;
a - упакованных переходов 1500;
k - упакованных классов символов 1000;
o - выходных элементов 1500.
КОММЕНТАРИИ в разделе определений задаются в форме host-язы-
ка и должны начинаться не с первой колонки строки.
Раздел правил
Все, что указано после первой пары %% и до конца Lexпрограм-
мы или до второй пары %%, если она указана, относится к разделу
правил. Раздел правил может содержать правила и фрагменты прог-
рамм. Фрагменты программ, содержащиеся в разделе правил, стано-
вятся частью функции yylex файла lex.yy.c, в которой осущес-
твляется выполнение действий активных правил. Фрагмент прогрммы
указывается следующим образом:
%{ строки фрагмента программы %}
Раздел правил может включать список активных и неактивных
(помеченных) правил. Активные и неактивные правила могут быть
указаны в любом порядке, в том числе быть "перемешанными" в спис-
ке. Активные правила выполняются всегда, неактивные только по
ссылке на них оператором BEGIN.
Активное правило имеет вид:
ВЫРАЖЕНИЕ ДЕЙСТВИЕ
Неактивное правило имеет вид:
<МЕТКА>ВЫРАЖЕНИЕ ДЕЙСТВИЕ
или
<СПИСОК МЕТОК>ВЫРАЖЕНИЕ ДЕЙСТВИЕ
где СПИСОК_МЕТОК имеет вид:
метка1,метка2,...
В качестве первого правила раздела правил может быть прави-
ло вида:
BEGIN МЕТКА;
В этом правиле отсутствует ВЫРАЖЕНИЕ, и первым действием в
разделе правил будет активизация помеченных правил. Для возвраще-
ния автомата в исходное состояние можно использовать действие:
BEGIN 0;
Важно отметить следующее. Если Lex-программа содержит актив-
ные и неактивные правила, то активные правила работают всегда.
Оператор "BEGIN МЕТКА;" просто расширяет список активных правил,
активируя помеченные меткой МЕТКА. А оператор "BEGIN 0;" удаляет
из списка активных правил все помеченные правила, которые до это-
го были активированы. Кроме того, если из помеченного и активно-
го в данный момент времени правила осуществляется действие BEGIN
МЕТКА, то из помеченных правил активными останутся только те, ко-
торые помечены меткой МЕТКА.
Действия в правилах Lex-программы
Действие можно представлять либо как оператор Lex, например,
"BEGIN МЕТКА;", либо как оператор Си. Если имеется необходимость
выполнить достаточно большой набор преобразований, то действие
оформляют как блок Си-программы (он начинается открывающей фигур-
ной скобкой и завершается закрывающей фигурной скобкой), содержа-
щий необходимые фрагменты.
Действие в правиле указывается через не менее, чем один про-
бел или табуляцию после выражения (обязательно в той же строке,
где и выражение), а его продолжение может быть указано в следую-
щих строках только в том случае, если действие оформлено как блок
Си-программы.
Область действия переменных, об'явленных внутри блока, рас-
пространяется только на этот блок. Внешними переменными для всех
действий будут являться только те переменные, которые об'явлены в
разделе определений Lex-программы.
Действия в правилах Lex-программы выполняются, если правило
активно, и если автомат распознает цепочку символов из входного
потока как соответствующую регулярному выражению данного правила.
Однако, одно действие выполняется всегда - оно заключается в ко-
пировании входного потока символов в выходной. Это копирование
осуществляется для всех входных строк, которые не соответствуют
правилам, преобразующим эти строки. Комбинация символов, не уч-
тенная в правилах и появившаяся на входе, будет напечатана на вы-
ходе. Можно сказать, что действие - это то, что делается вместо
копирования входного потока символов на выход.
Когда необходимо вывести или преобразовать текст, соответ-
ствующий некоторому регулярному выражению, используется внешний
массив символов, который формирует Lex. Называется он yytext и
доступен в действиях правил. Например:
[A-Z]+ printf("%s",yytext);
По этому правилу распознается слово, содержащее прописные
латинские буквы и выводится с помощью printf, если оно выделено.
Операция вывода распознанного выражения используется очень часто,
поэтому имеется сокращенная форма записи этого действия: [A-Z]+
ECHO;
Результат действия этого правила будет аналогичен результа-
ту предыдущего примера. В выходном файле lex.yy.c ECHO определе-
но как макроподстановка:
#define ECHO fprintf(yyout, "%s",yytext);
Когда необходимо знать длину обнаруженной последовательнос-
ти символов, используется счетчик найденных символов yyleng, ко-
торый также доступен в действиях. Например:
[A-Z]+ printf("%c",yytext[yyleng-1]);
В этом примере будет выводится последний символ слова, соот-
ветствующего регулярному выражению [A-Z]+.
Рассмотрим еще один пример:
[A-Z]+ {число_слов++;число_букв += yyleng;}
Здесь ведется подсчет числа распознанных слов и количества
символов во всех словах.
Порядок действий активных правил
Список правил Lex-программы может содержать активные иеак-
тивные правила, размещенные в любом порядке в разделе правил. В
процессе работы лексического анализатора список активных правил
может видоизменяться за счет действий оператора BEGIN. В процес-
се распознавания символов входного потока может оказаться так,
что одна цепочка символов будет удовлетворять нескольким прави-
лам и, следовательно, возникает проблема: действие какого прави-
ла должно выполняться?
Для разрешения этого противоречия можно использовать кванто-
вание (разбиение) регулярных выражений этих правил Lex-программы
на такие новые регулярные выражения, которые дадут, по возможнос-
ти, однозначное распознавание лексемы. Однако, когда это не сде-
лано, Lex использует определенный детерминированный механизм раз-
решения такого противоречия:
- выбирается действие того правила, которое распознает наи-
более длинную последовательность символов из входного потока;
- если несколько правил распознают последовательности симво-
лов одной длины, то выполняется действие того правила, которое
записано первым в списке раздела правил Lex-программы.
Раздел программ пользователя
Все, что размещено за вторым набором %%, относится к разде-
лу программ пользователя. Содержимое этого раздела копируется в
выходной файл lex.yy.c без каких-либо изменений. В файле lex.
yy.c строки этого раздела рассматриваются как функции в смысле
Си. Эти функции могут вызываться в действиях правил и, как обыч-
но, передавать и возвращать значения аргументов.
Комментарии Lex-программы
Комментарии можно указывать во всех разделах Lex- программы.
Формат комментариев должен соответствовать формату комментариев
host-языка. Однако, в каждом разделе Lex- программы комментарии
указываются по разному. В разделе определений комментарии должны
начинаться не с первой позиции строки. В разделе правил коммента-
рии можно указывать только внутри блоков, принадлежащих дей-
ствиям. В разделе программ пользователя комментарии указываются
как и в host-языке.
Структура файла lexyy.c
Lex строит программу - лексический анализатор на языке Си,
которая размещается в файле со стандартным именем lex.yy.c. Эта
программа содержит две основных функции и несколько вспомога-
тельных. Основные - это:
функция yylex()
Она содержит разделы действий всех правил, которые определе-
ны пользователем;
функция yylook()
Она реализует детерминированный конечный автомат, который
осуществляет разбор входного потока символов в соответствии с ре-
гулярными выражениями правил Lex программы.
Вспомогательные функции, которые являются подпрограммами
ввода-вывода. К ним относятся:
input() - читает и возвращает символ из входного потока сим-
волов;
unput(c) - возвращает символ обратно во входной поток для
повторного чтения;
output(c) - выводит в выходной поток символ "c".
Эти функции можно изменить, указав им те же имена и размес-
тив в разделе программ пользователя.
Кроме того имеются функции yywrap(), reject(),yyless() и
yymore().
yywrap()
Используется для определения конца файла, из которого лекси-
ческий анализатор читает поток символов. Если yywrap возвращает
1, лексический анализатор прекращает работу. Однако, иногда
имеется необходимость начать ввод данных из другого источника и
продолжить работу. В этом случае пользователь должен написать
свою подпрограмму yywrap, которая организует новый входной поток
и возвращает 0, что служит сигналом к продолжению работы анализа-
тора. По умолчанию yywrap всегда возвращает 1 при завершению
входного потока символов.
В Lex-программе невозможно записать правило, которое будет
обнаруживать конец файла. Единственный способ это сделать - ис-
пользовать фунцию yywrap. Эта функция также удобна, когда необхо-
димо выполнить какие-либо действия по завершению входного потока
символов, определив в разделе программ пользователя новый ва-
риант функции yywrap.
REJECT
Обычно Lex разделяет входной поток, не осуществляя поиск
всех возможных соответствий каждому выражению. Это означает, что
каждый символ рассматривается один и только один раз. Иногда же-
лательно переопределить этот выбор. Действие функции REJECT озна-
чает "выбрать следующую альтернативу". Это приводит к тому, что
каким бы ни было правило, после него необходимо выполнить второй
выбор.
Функция REJECT полезна в том случае, когда она применяется
для определения всех вхождений какого-либо об'екта, причем вхож-
дения могут перекрываться или включать друг друга.
В обычной ситуации содержимое yytext обновляется всякий раз,
когда на входе появляется следующая строка. Напомним, что в
yytext всегда находятся символы распознанной последовательности.
Иногда возникает необходимость добавить к текущему содержимому
yytext следующую распознанную цепочку символов. Для этой цели ис-
пользуется функция yymore. Формат вызова этой функции:
yymore();
В некоторых случаях возникает необходимость использовать не
все символы распознанной последовательности в yytext, а только
необходимое их число. Для этой цели используется функция yyless.
Формат ее вызова:
yyless( n );
где n указывает, что в данный момент необходимы только n
символов строки в yytext. Остальные найденные символы будут воз-
вращены во входной поток.
Алгоритм лексического анализа
Данный алгоpитм является pасшиpенным индексным методом с
пpовеpкой пеpеходов и возвpатом ( теоpетический подход к этому
методу pешения был описан в пункте 2.1.1.).
ШАГ 1. Вычислить индекс начального сотояния (тек_сост).
ШАГ 2. Пpовеpить сушествует ли пеpеход из этого состояния по
какому либо символу или альтеpнативный пеpеход. Если не сущес-
твует, то пеpейти к ШАГУ 10.
ШАГ 3. Пpочитать один символ ( символ ).
ШАГ 4. Вычислить индекс элемента по введенному символу
тек_сост_1 = тек_сост + код_символа( символ ).
ШАГ 5. Пpовеpить индекс в таблице состояний. Если индекс
таблицы пеpеходов меньше нуля , то пеpейти к ШАГУ 8, если индекс
в таблице пеpеходов pавен нулю, то пеpейти к ША- ГУ 10. Иначе
пеpейти к ШАГУ 6.
ШАГ 6. Пpовеpить пpавильность пеpехода по таблице пеpехо-
дов. Если непpавильно, то пеpейти к ШАГУ 10. Иначе пеpейти ШАГУ 7.
ШАГ 7. Запомнить текущее состояние и введенный символ, ус-
тановить тек_сост в соответствии с таблицей пеpеходов. Пеpейти к
ШАГУ 2.
ШАГ 8. Изменить знак индекса таблицы пеpеходов и попы-
таться осуществить пеpеход как в ШАГАХ 4,6,7. Если попытка закон-
чилась удачно, то пеpейти к ШАГУ 2. Иначе пеpейти к ША- ГУ 9.
ШАГ 9. Если возможен пеpеход по алтеpнативе, то альтеpна-
тивное состояние сделать текущим и пеpейти к ШАГУ 4. В пpотивном
случае пеpейти к ШАГУ 10.
ШАГ 10. Веpнуть последний введенный символ в устpойство вво-
да.
ШАГ 11. Если достигнуто начальное состояние, то пpейти к
ШАГУ 13 в пpотивном случае пеpейти к шагу 12.
ШАГ 12. Если в текущее состояние пpинадлежит множеству воз-
можных конечных состояний, то в таблице конечных состояний уз-
нать номеp pаспознанной лексемы и закончить выполнение алгоpитма.
В пpотивном случае пеpейти в пpедыдущее состояние и пеpейти к
ШАГУ 10.
ШАГ 13. Закончить выполнение алгоpитма и выдать состояние
ошибки.
Данный алгоpитм позволяет стpоить лексический анализатоp,
котоpый получает поток сиволов со входного устpойства и pаспозна-
вая его тpанслиpует в паpы ( атpибут, значение ) для синтаксичес-
кого анализатоpа.
Пpиведем описание стpуктуp данных,пеpеменных и функций, ис-
пользующихся в пpогpамме.
Описание функций
Пpогpамма, постpоенная LEX, состоит из двух функций. Пеpвая
из них является непосpедственным исполнителем действий, пpедпи-
санных к выполнению после pаспознования лексемы.
Функция: INT LEXYY();
Вход: паpаметpы не пеpедаются.
Выход: возвpащает номеp pаспознанной лексемы или 0,если дос-
тигнут конец входного файла.
Реакция на ошибки: в случае, если входная последова-
тельность символов не pаспознана , то возвpащает -1.
Действие: функция pаспознает входную последовательность сим-
волов и пpеобpазует ее в паpы ( атpибут, значение ).
Напpимеp, часть пpогpаммы, написанной на языке LEX, после
генерации имеет следующий вид:
. . .
%%
. . .
"IF" return( if_perfix_symbol );
"THEN" return( if_then_symbol );
"ELSE" return( if_else_symbol );
. . .
%%
. . .
В пpоцедуpе LEXYY она может выглядеть следующим обpазом:
#include <stdio.h>
. . .
int lexyy()
{
int nsl;
. . .
nsl = yylook();
switch( nsl ) {
. . .
case 22: return( if_perfix_symbol );
break;
case 23:
return( if_then_symbol );
break;
case 24:
return( if_else_symbol );
break;
. . .
}
. . .
} /* Конец функции LEXYY */
. . .
/* Конец файла LEXYY.C */
Функция: int YYLOOK()
Вход: ни каких паpаметpов не пеpедается.
Выход: возвpашает номеp конечного состояния.
Обpаботка ошибок: если входная последовательность не pаспоз-
нана, то возвpащает -1.
Выполняемые действия: функция непосpедственно pеализующая
пеpеходы в автомате pаспознования входной последовательности.
Пpиведем усеченный ваpиант исходного текста (отсутствуют вызовы
пpоцедуp отладки). В текст вставим коментаpии.
/***********************************************************
Текст пpогpаммы YYLOOK(), pеализующей функции пеpеходов ко-
нечного автомата в pаспозновании лексем
***********************************************************/
yylook(){
register struct yysvf *yystate, **lsp;
register struct yywork *yyt;
struct yysvf *yyz;
int yych;
struct yywork *yyr;
char *yylastch;
for(;;){
/* Установить указатель стека состояний на начальное состояние */
lsp = yylstate;
/* ШАГ 1. Вычислить индекс начального сотояния (тек_сост) */
yyestate = yystate = yybgin;
/***********************************************************
Если был переход на новую строку, то начальное состоя-
ние смещается для обработки символа переход на новую строку
на одно состояние
***********************************************************/
if (yyprevious==YYNEWLINE) yystate++;
for (;;){
/***********************************************************
ШАГ 2. Пpовеpить сушествует ли пеpеход из этого состоя-
ния по какому либо символу или альтеpнативный пеpеход. Если
не существует, то пеpейти к ШАГУ 10
***********************************************************/
/* Получить адрес элемента таблицы переходов относи-
тельно которого вычисляются основные переходы по символам */
yyt = yystate->yystoff;
/* Если адрес равен адресу начала таблицы переходов, то
переходов нет и осуществляется проверка есть-ли переход по
альтернативному пути*/
if(yyt == yycrank){
/* Получить в табл.состояний адрес альтернативного пе-
рехода */
yyz = yystate->yyother;
/* Если альтернативный переход отсутствует ( т.е. адрес
равен нулю ),то прейти к ШАГУ 10 */
if(yyz == 0)break;
/* Если альтернативное состояние существует (т.е. адрес
не равен 0), то если из альтернативного перехода нет перехо-
да по таблице основных переходов, то прейти к ШАГУ 10 */
if(yyz->yystoff == yycrank)
break;
}
/***********************************************************
ШАГ 3. Пpочитать один символ ( символ )
***********************************************************/
/* input() является макроопределением, которое вводит
один символ из строки, если ранее были отвергнутые символы
или из входного файла
FILE *yyin={stdin}; в противном случае
yylstch - указывает на текущий символ кудаввести в
строке yytext для сохранения значения пары ( нетерминал,
значение ), если входная цепочка будет распознана как до-
пустимая */
*yylastch++ = yych = input();
/***********************************************************
ШАГ 4. Вычислить индекс элемента по введенному символу
( тек_сост_1 = тек_сост + код_символа( символ )
ШАГ 5. Пpовеpить индекс в таблице состояний, если ин-
декс таблицы пеpеходов меньше нуля , то пеpейти к ШАГУ 8,
если индекс в таблице пеpеходов pавен нулю, то пеpейти к ША-
ГУ 10 иначе пеpейти к ШАГУ 6.
***********************************************************/
tryagain:
/* Сохранить адрес таблицы переходов для текущего
состояния */
yyr = yyt;
/* Если адрес таблицы переходов для текущего состояния
больше адреса начала таблиц переходов, то прейти к ШАГУ 6 */
if ( (int)yyt > (int)yycrank){
/* Вычислить адрес нового элемента в таблице переходов
*/
yyt = yyr + yych;
/***********************************************************
ШАГ 6. Пpовеpить пpавильность пеpехода по таблице
пеpеходов. Если непpавильно то пеpейти к ШАГУ 10 иначе
пеpейти к ШАГУ 7.
***********************************************************/
/* if yyt <= yytop - если вычисленное состояние меньше
или pавно максимально допустимого номеpа состояния
if YYU( yyt ->verify)+yysvec == yystate если пеpеход,
котоpый пытаемся осуществить допустим из текущего состояния
*/
if (yyt<=yytop&&YYU(yyt->verify)+yysvec==yystate)
{
/* Если номеp состояния в котоpое мы пытаемся пеpейти
pавно YYERR = 0, то бнаpужен конец лекскмы и пеpейти к ШАГУ
10 */
if(yyt->advance == YYLERR)
{
/* unput(*--yylastch) - веpнуть последний символ во входной
файл */
unput(*--yylastch);
break;
}
/***********************************************************
ШАГ 7. Запомнить текущее состояние и введенный символ,
установить тек_сост в соответствии с таблицей пеpеходов и
пеpейти к ШАГУ 2.
***********************************************************/
/* state = YYU(yyt -> advance )+yysvec - по таблице
пеpеходов пеpейти в новое состояние автомата
*lsp++ = state - сохpанить в стеке состояний но-
вое состояние */
*lsp++ = yystate = YYU(yyt->advance)+yysvec;
/* Пеpейти к ШАГУ 2 */
goto contin;
} }
#ifdef YYOPTIM
/* следующая часть подключается только в случае,ели необходимо
обpабатывать ситуации [^S] return(symbol); если таких ситуаций
нет, то адpес таблицы пеpеходов меньше начального, то обpабаты-
вается, как отсутствие пеpеходов в автомате. */
else if((int)yyt < (int)yycr)
/***********************************************************
ШАГ 8. Изменить знак индекса таблицы пеpеходов и попы-
таться осуществить пеpеход как в ШАГАХ 4,6,7, если попытка закон-
чилась удачно, то пpейти к ШАГУ 2 иначе пеpейти к ШАГУ
9.
***********************************************************/
/* Изменить знак индекса в таблице пеpеходов */
yyt = yyr = yycrank+(yycrank-yyt);
/* Вычислить новый адpес в таблице пеpеходов */
yyt = yyt + yych;
/* if yyt <= yytop - если вычисленное состояние меньше
или pавно максимально допустимого номеpа состояния
if YYU( yyt ->verify)+yysvec == yystate
если пеpеход, котоpый пытаемся осуществить, допустим из
текущего состояния */
if(yyt <= yytop && YYU(yyt->verify)+yysvec == yystate)
{
/* Если номеp состояния в котоpое мы пытаемся пеpейти pавно
YYERR = 0, то бнаpужен конец лекскмы и пеpейти к ШАГУ
10 */
if(yyt->advance == YYLERR)
/* error transitions */
{unput(*--yylastch);break;}
/* state = YYU(yyt -> advance )+yysvec - по таблице
пеpеходов пеpейти в новое состояние автомата
*lsp++ = state - сохpанить в стеке состояний новое
состояние */
*lsp++ = yystate = YYU(yyt->advance)+yysvec;
/* Пеpейти к ШАГУ 2 */
goto contin;
}
/***********************************************************
ШАГ 9. Если возможен пеpеход по алтеpнативе, то аль-
теpнативное состояние сделать текущим и пеpейти к ШАГУ 4 в
пpотивном случае пеpейти к ШАГУ 10.
***********************************************************/
/* yystate = yystate -> yyoter - сделать состояние аль-
теpнативы текущим состоянием
yyt = yystate -> yystoff - плучить новый адpес таблицы
смещений
if ( state ) - если сушествует альтеpнативное состояние
то
if ( yyt != yycrank) - если из этого состояния
существует пpямой пеpеход */
if ((yystate = yystate->yyother) && (yyt =
yystate->yystoff) != yycrank){
/* Пеpейти к ШАГУ 4 */
goto tryagain;
}
#endif
else
{unput(*--yylastch);break;}
contin:
}
/* Конец обpаботки входного потока и начало пpовеpки
что это за лекскма */
/***********************************************************
ШАГ 10. Веpнуть последний введенный символ в устpойство
ввода.
ШАГ 11. Если достигнуто начальное состояние, то пpейти
к ШАГУ 13 в пpотивном случае пеpейти к шагу 12.
***********************************************************/
while (lsp-- > yylstate){
/* Удалить из yytext последний смвол и поставить пpиз-
нак конец стpоки */
*yylastch-- = 0;
/***********************************************************
ШАГ 12. Если в текущее состояние пpинадлежит множеству
возможных конечных состояний, то в таблице конечных состоя-
ний узнать номеp pаспознанной лексемы и закончить выполнение
алгоpитма. В пpотивном случае пеpейти в пpедыдущее состояние
и пеpейти к ШАГУ 10.
ШАГ 13. Закончить выполнение алгоpитма и выдать состоя-
ние ошибки.
***********************************************************/
/* Если номеp текущего состояния в стеке состояний не
нулевой и текущее состояние пpимнадлежит множеству конечных
состояний, то есть (*lsp)->yystop > 0 */
if (*lsp != 0 && (yyfnd= (*lsp)->yystops) && *yyfnd > 0){
/* Сохpанить значения в глобальных пеpеменных */
yyolsp = lsp;
yyprevious = YYU(*yylastch);
yylsp = lsp;
yyleng = yylastch-yytext+1;
/* Установить пpизнак конца стpоки в yytext */
yytext[yyleng] = 0;
/* Возвpатить номеp pаспознанной лексемы */
return(*yyfnd++);
}
/* если лексема не pаспознана, то веpнуть символ вов-
ходной поток */
unput(*yylastch);
}
/* если достигнут конец файла, то веpнуть 0 */
if (yytext[0] == 0 /* && feof(yyin) */)
{
yysptr=yysbuf;
return(0);
}
yyprevious = yytext[0] = input();
if (yyprevious>0)
output(yyprevious);
yylastch=yytext;
}
}
ЛЕКЦИЯ 10
КРАТКИЕ СВЕДЕНИЯ О ГЕНЕРАТОРЕ СИНТАКСИЧЕСКИХ
АНАЛИЗАТОРОВ YACC
Значительная часть создаваемых программ, рассчитанных на оп-
ределенную структуру входной информации, явно или неявно вклю-
чает в себя некоторую процедуру синтаксического анализа. В зада-
чах, где пользователю при задании входной информации предостав-
ляется относительная свобода (в отношении сочетания и последова-
тельности структурных элементов), синтаксический анализ достаточ-
но сложен. При решении подобных задач существенную поддержку мо-
гут оказать сервисные программы, осуществляющие построение син-
таксических (грамматических) анализаторов на основе заданных све-
дений о синтаксической структуре входной информации. YACC отно-
сится к числу этих сервисных программ - генераторов грамматичес-
ких анализаторов.
Поскольку обширной областью, где наиболее явно видна необхо-
димость (нетривиального) грамматического анализа, а следова-
тельно и его автоматизации, является область создания транслято-
ров (в частности, компиляторов), программы, подобные YACC, полу-
чили еще название компиляторов (или генераторов) компиляторов.
Заметим, что понятие транслятора может относиться не только
к языкам программирования, но и к командным языкам, входным язы-
кам любых диалоговых систем, языкам управления технологическими
процессами и т.п.
Пользователь YACC должен описать структуру своей входной ин-
формации (ГРАММАТИКУ) как набор ПРАВИЛ в виде, близком к Бэкусо-
во-Науровской форме (БНФ). Каждое правило описывает грамматичес-
кую конструкцию, называемую НЕТЕРМИНАЛЬНЫМ СИМВО- ЛОМ, и сопос-
тавляет ей имя. С точки зрения грамматического разбора правила
рассматриваются как правила вывода (подстановки). Грамматические
правила описываются в терминах некоторых исходных конструкций,
которые называются лексическими единицами, или ЛЕКСЕМАМИ. Имеет-
ся возможность задавать лексемы непосредственно (литерально) или
употреблять в грамматических правилах имя лексемы. Как правило,
имена сопоставляются лексемам, соответствующим классам об'ектов,
конкретное значение которых не существенно для целей грамматичес-
кого анализа. (Иногда в литературе с понятием лексемы совпадает
понятие терминального символа; однако, ряд авторов называет тер-
минальными символами отдельные символы стандартного набора). При-
мерами имен лексем могут служить "ИДЕНТИФИ- КАТОР" и "ЧИСЛО", а
введение таких лексем позволяет обобщить конкретные способы запи-
си идентификаторов и чисел. В некоторых случаях имена лексем слу-
жат для придания правилам большей выразительности.
Лексемы должны распознаваться программой лексического анали-
за, определяемой пользователем. Пользователь же предварительно
выбирает конструкции,которые более удобно и эффективно распозна-
вать непосредственно, и в соответствии с этим об'являет имена
лексем. Пользовательская программа лексического анализа -
ЛЕКСИЧЕСКИЙ АНАЛИЗАТОР - осуществляет чтение реальной входной ин-
формации и передает грамматическому анализатору распознанные лек-
семы.
Как уже отмечалось, YACC обеспечивает автоматическое пос-
троение лишь процедуры грамматического анализа. Однако, действия
по обработке входной информации обычно должны выполняться по ме-
ре распознавания на входе тех или иных допустимых грамматических
конструкций. Поэтому наряду с заданием грамматики входных тек-
стов YACC предусматривает воможность описания для отдельных кон-
струкций семантических про-
цедур (ДЕЙСТВИЙ) с тем, чтобы они были включены в программу грам-
матического разбора. В зависимости от характера пользовательских
семантических процедур (интерпретация распознанного фрагмента
входного текста, генерация фрагмента об'ектного кода, отметка в
справочной таблице или форматирование вершины в дереве разбора)
генерируемая с помощью YACC программа будет обеспечивать кроме
анализа тот или иной вид обработки входного текста, в частности,
его компиляцию или интерпретацию.
Итак, пользователь YACC подготавливает общее описание
(СПЕЦИФИКАЦИИ) обработки входного потока, включающее правила,
описывающие входные конструкции, кодовую часть, к которой должно
быть организовано обращение при обнаружении этих конструкций, и
программу ввода базовых элементов потока (лексический
анализатор). Kомпилятор компиляторов обеспечивает создание под-
программы (грамматического анализатора), реализующей процедуру
обработки входного потока в соответствии с заданными специфика-
циями.
К компонентам компилятора компиляторов относятся выполняе-
мый файл yacc, библиотека стандартных программ /lib/liby.a, Файл
/usr/lib/yaccpar. Заключительная фаза построения компилятора тре-
бует применения компилятора языка Си.
ПРИНЦИПЫ РАБОТЫ YACC
Грамматические анализаторы, создаваемые с помощью YACC, реа-
лизуют так называемый LALR(1)-разбор, являющийся модификацией од-
ного из основных методов разбора "снизу вверх" - LR(k)-разбора
(буквы L(eft) и R(ight) в обоих сокращениях означают соответ-
ственно чтение входных символов слева направо и использование
правостороннего вывода. Индекс в скобках показывает число предва-
рительно просматриваемых лексических единиц).
Любой разбор по принципу "снизу вверх" (или восходящий раз-
бор) состоит в попытке приведения всей совокупности входных дан-
ных (входной цепочки) к так называемому "начальному символу грам-
матики" путем последовательного применения правил вывода.
В каждый момент грамматического разбора анализатор находит-
ся в некотором СОСТОЯНИИ, определяемом предысторией разбора, и в
зависимости от очередной лексемы предпринимает то или иное дей-
ствие для перехода к новому состоянию. Различают два типа дей-
ствий: "СДВИГ", т.е. чтение следующей входной лексемы, и
"СВЕРТКУ", т.е. применение одного из правил подстановки для заме-
щения нетерминалом последовательности символов, соответствующей
правой части правила. Работа YACC по генерации процедуры грамма-
тического анализа заключается в построении таблиц, которые для
каждого из состояний определяют тип действий анализатора и номер
следующего состояния в соответствии с каждой из входных лексем.
Любой метод разбора требует грамматик с определенными свой-
ствами. В этом смысле YACC предполагает контекстносвободные грам-
матики со свойствами LALR(1). LALR(1)- грамматики, являясь под-
множеством LR(1)-грамматик, допускают при построении таблиц раз-
бора сокращение общего числа состояний за счет об'единения иден-
тичных состояний, различающихся только набором символов-следова-
телей (символов, которые могут следовать после применения одного
из правил вывода, если разбор по этому правилу проходил через
данное состояние). Другие грамматики являются неоднозначными для
принятого в YACC метода разбора и вызовут конфликты.
Однако, если язык, описываемый данной грамматикой, в принци-
пе допускает задание грамматики, однозначной для данного метода
разбора, то YACC позволяет без перестройки грамматики построить
грамматический анализатор, разрешающий конфликты на основе меха-
низма приоритетов.
ВХОДНЫЕ И ВЫХОДНЫЕ ФАЙЛЫ, СТРУКТУРА
ГРАММАТИЧЕСКОГО АНАЛИЗАТОРА
Входная информация для YACC задается в СПЕЦИФИКАЦИОННОМ
ФАЙЛЕ. На выходе компилятора компиляторов в результате обработки
спецификаций создается файл y.tab.c с исходным текстом Сипроце-
дур, составляющих грамматический анализатор. Основной в файле
y.tab.c является процедура yyparse, реализующая алгоритм грамма-
тического разбора. При формировании ее YACC использует файл
/usr/lib/yaccpar, содержащий неизменяемую часть анализатора. Кро-
ме yyparse, в файл y.tab.c YACC включает построенные им таблицы
разбора, описания и программные фрагменты пользовательских специ-
фикаций.
Процедура yyparse представляет собой целочисленную функцию,
возвращающую значение 0 или 1. Значение 0 возвращается в случае
успешного разбора по достижении признака конца файла, значение 1-
в случае несоответствия входного текста заданным спецификациям.
Процедура yyparse содержит многократное обращение к процедуре
лексического анализа yylex, текст которой либо переносится в файл
y.tab.c из спецификационного файла, либо прикомпоновывается впос-
ледствии.
Для организации обращения к процедуре yyparse в библиотеке
YACC существует стандартная процедура main, не содержащая помимо
обращения к yyparse никаких действий. Пользователь может напи-
сать собственную процедуру main, включив в нее как начальные дей-
ствия, предваряющие вызов yyparse (установка нужных режимов, от-
крытие файлов, частичное заполнение таблиц), так и действия по
завершении разбора, которым должен предшествовать анализ возвра-
щаемого yyparse значения; действиями в случае успешного разбора
могут быть закрытие файлов, вывод результатов, вызов следующей
фазы транслятора, в частности, повторный вызов yyparse. Для заме-
ны стандартной процедуры пользовательской программой main она
должна быть описана в спецификационном файле или присоединена на
этапе вызова Си-компилятора для подготовки исполняемой программы.
Кроме выходного файла y.tab.c, YACC может дополнительно гене-
рировать следующие выходные файлы:
y.output содержащий описание состояний анализатора и сообще-
ния о конфликтах;
y.tab.h содержащий описание лексем.
Для генерации этих файлов требуется задание соответствующих
флагов при вызове YACC.
ПРОЦЕДУРА ПОСТРОЕНИЯ ГРАММАТИЧЕСКОГО АНАЛИЗАТОРА
Построение грамматического анализатора осуществляется в два
этапа. На первом этапе файл спецификаций входного языка обрабаты-
вается компилятором компиляторов YACC, для чего задается коман-
дная строка yacc [-vd] yfile. Здесь yfile - имя файла специфика-
ций, а флаги имеют следующий смысл: v - сформировать в файле
y.output подробное описание грамматического анализатора; d -
сформировать в файле y.tab.h описание лексем.
Текстовые файлы y.output и y.tab.h содержат справочную ин-
формацию для пользователя, и никак не используются на втором эта-
пе построения грамматического анализатора. Основной результат ра-
боты YACC - процедура yyparse и грамматические таблицы - поме-
щается в файл y.tab.c. На втором этапе построения грамматическо-
го анализатора для получения в файле a.out исполняемой программы
компилируется файл y.tab.c и присоединяются другие программные
компоненты:
cc y.tab.c [cfile...ofile...lfile...] -ly
где cfile, ofile,lfile - имена исходных, объектных и библио-
течных файлов, содержащих присоединяемые процедуры. В этот спи-
сок не включается имя стандартной библиотеки YACC /lib/liby.a, ее
подключение обеспечивается заданием флага ly. Этот флаг полезно
считать обязательным.
ЗАДАНИЕ ВХОДНОЙ ИНФОРМАЦИИ YACC
Структура спецификационного файла
Пользовательские спецификации, задающие правила организации
входной информации и алгоритм ее обработки, об'единяются в специ-
фикационный файл следующей структуры:
декларации
%%
правила
%%
программы
Ядром спецификационного файла и единственной его обяза-
тельной частью является секция правил. При отсутсивии секции
программ может быть опущена вторая группа "%%"; следовательно,
минимальная допустимая конфигурация входного файла имеет вид:
%%
правила
Пробелы, знаки табуляции и перевода строки игнорируются, не-
допустимо лишь появление их в именах. Комментарий, ограниченный
символами "/*" в начале и "*/" в конце, может находиться между
любыми двумя разделителями в любой секции входного файла.
СЕКЦИЯ ПРАВИЛ состоит из одного или нескольких грамматичес-
ких правил. Эти правила должны определять все допустимые входные
конструкции и связанные с определенными конструкциями действия по
обработке входного потока.
Назначение СЕКЦИИ ДЕКЛАРАЦИИ состоит в основном в задании
информации о лексемах.
СЕКЦИЯ ПРОГРАММ представляет собой некоторый набор процедур
на языке Си, которые должны включаться в текст программы грамма-
тического разбора. Например, это могут быть процедура лексическо-
го анализа yylex и пользовательские процедуры, вызываемые в дей-
ствиях.
СЕКЦИЯ ПРАВИЛ. В данной секции с помощью набора грамматичес-
ких правил должны быть определены все конструкции, из которых
впоследствии будут строиться входные тексты. Не подлежат опреде-
лению в секции правил лишь конструкЦии, выбранные пользователем в
качестве лексем, считающиеся для грамматического анализа исходны-
ми единицами. Правила задаются в форме, близкой БНФ.
Правило, определяющее синтаксический вид конструкции, за-
дается таким образом: <имя нетерминального символа>: определение;
здесь ':' и ';' специальные символы YACC. Правая часть правила -
определение - представляет собой упорядоченную последова-
тельность элементов (нетерминальных символов и лексем), состав-
ляющих описываемую конструкцию. При грамматическом разборе такая
последовательность в результате применения правила заменяется не-
терминальным символом, имя которого указано в левой части. нетер-
минальные символы в определении задаются именами, а лексемы -
именами или литералами. Запись имен и литералов совпадает с за-
писью идентификаторов и символьных констант, принятой в Си.
По виду правила нельзя заключить, относятся эти имена к лек-
семам или нетерминальным символам. YACC считает именами нетерми-
налов все имена, не объявленные в секции деклараций именами лек-
сем. Все нетерминалы должны быть определены, т.е. имя каждого из
них должно появиться в левой части хотя бы одного правила. Допус-
тимо задание нескольких правил, определяющих один нетерминальный
символ, т.е. правил с одинаковой левой частью. Такие правила оп-
ределяют конструкции, идентичные на некотором уровне.
Правила с общей левой частью можно задавать в сокращенной
записи, без повторения левой части, используя для разделения
альтернативных определений символ "|".
При необходимости с любым правилом можно связать действие -
набор операторов языка Си, которые будут выполняться при каждом
распознавании конструкции во входном тексте. Действие не являет-
ся обязательным элементом правил.
Действие заключается в фигурные скобки и помещается вслед за
правой частью правила, т.е. правило с действием имеет вид:
<имя нетерминального символа>: определение {действие};
Точка с запятой после правила с действием может опускаться.
При использовании сокращенной записи правил с общей левой частью
следует иметь в виду, что действие может относиться только к от-
дельному правилу, а не к их совокупности. Следующий пример иллюс-
трирует задание действий в случае правил с общей левой частью.
На операторы, входящие в действия, не накладывается никаких
ограничений. В частности, в действиях могут содержаться вызовы
любых функций. Отдельные операторы могут быть помечены, к ним
возможен переход из других действий.
Существует возможность задания действий, которые будут вы-
полняться по мере распознавания отдельных фрагментов правила.
Действие в этом случае помещается после одного из элементов пра-
вой части так, чтобы положение действия соотвествовало моменту
разбора, в который данному действию будет передано управление.
Для того, чтобы действия имели реальный смысл, они, как пра-
вило, должны использовать некоторые общие переменные,
т.е. переменные, доступные во всех действиях и сохраняющие свои
значения по окончании очередного действия. Такие переменные опи-
сываются в начале секции правил, все описание ограничивается с
двух сторон строками
%{
и %}
Здесь же могут находиться произвольные операторы Си, рас-
сматриваемые как общие действия для всех правил.
Укажем еще на два вида объектов, использующихся в действиях.
Это, во-первых, глобальные переменные, которые описываются в сек-
ции деклараций , и, во-вторых, специальные переменные (псевдопе-
ременные),облегчающие взаимосвязь между действиями и связь их с
лексическим анализатором.Структура входной информации описывает-
ся в наборе правил иерархически, т.е. каждое правило соответ-
ствует определенному уровню структурного разбиения. Однако, пос-
ледовательность задания правил может не отражать этой иерархии и
быть вполне произвольной. Единственно необходимой для YACC яв-
ляется информация о том, какой из нетерминалов задает вершину ие-
рархии, т.е. соотвествует конструкциям, определяющим входной
текст в целом. Этот нетерминал принято называть НАЧАЛЬНЫМ
СИМВОЛОМ. Приведение входного текста к начальному символу являет-
ся целью грамматического анализатора. По умолчанию YACC считает
начальным символом тот нетерминал, имя которого стоит в левой
части первого из правил. Однако, если определять начальный сим-
вол в первом правиле пользователю неудобно, он может явно задать
имя начального символа в секции деклараций.
Существует два специфических вида правил, на которые полез-
но обратить внимание. Во-первых, это пустое правило вида
<имя нетерминального символа>: ;
определяющее пустую последовательность символов. Сочетание
пустого правила с другими правилами, определяющими тот же нетер-
минальный символ, является одним из способов указать на необяза-
тельность вхождения соответствующей конструкции. Например, сово-
купность правил
целое_число:знак целое_без_знака; знак: | '+'|'-';
определяет возможность задания целых чисел со знаком и без
него. Другой способ описать эту возможность состоит в задании
следующей группы правил:
целое_число:знак целое_без_знака | целое_без_знака ; знак:
'+'|'-';
С пустым правилом может быть обычным образом связано дей-
ствие:
ИМЯ: {тело_действия} ;
Во-вторых, правила часто описывают некоторую конструкцию ре-
курсивно, т.е. правая часть может рекурсивно включать определяе-
мый нетерминальный символ. Различают леворекурсивные правила вида:
<имя нетерминала>:<имя нетерминала> <многократно повто-
ряемый фрагмент>;
и праворекурсивные вида
<имя нетерминала>: <многократно повторяемый фрагмент>
<имя нетерминала>;
YACC допускает оба вида рекурсивных правил, однако, при ис-
пользовании правил с правой рекурсией об'ем анализатора увеличи-
вается, а во время разбора возможно переполнение внутреннего сте-
ка анализатора.
Рекурсивные правила необходимы при задании последовательнос-
тей и списков. Следующие примеры иллюстрируют универсальный спо-
соб описания этих конструкций:
последовательность: элемент
| последовательность элемент;
список: элемент | список ',' элемент;
Заметим, что в каждом из этих случаев первое правило (не со-
держащее рекурсии) будет применено только для первого элемента, а
второе (рекурсивное) - для всех последующих. Нетерминальные сим-
волы, связанные с последовательностями или списками разнородных
элементов, могут описываться произвольным числом рекурсивных и
нерекурсивных правил. Например, группа правил
идентификатор: буква |
'$' |
идентификатор буква |
идентификатор цифра |
идентификатор '_' ;
описывает идентификатор как последовательность букв, цифр и
символов "_", начинающуюся с буквы или символа "$". Следует обра-
тить внимание на то, что рекурсивные правила не имеют смысла, ес-
ли для определяемого ими нетерминала не задано ни одного правила
без рекурсии. Для обеспечения возможности задания пустых списков
или последовательностей в качестве нерекурсивного правила ис-
пользуется пустое:
последовательность : | последовательность элемент ;
Сочетание пустых и рекурсивных правил является удобным спо-
собом представления грамматик и ведет к большей их общности, од-
нако,некорректное использование пустых правил может вызывать кон-
фликтные ситуации (раздел 5) из-за неоднозначности выбора нетер-
минала, соответствующего пустой последовательности.
Секция деклараций состоит из последовательности строк двух
видов: строк-директив и строк исходного текста.
Строки исходного текста без изменений копируются в выходной
файл y.tab.c и могут содержать операторы препроцессора и описа-
ние внешних об'ектов. Область действия переменных, описанных в
секции деклараций, распространяется на весь спецификационный
файл, т.е. они доступны как в операторах действий, так и в проце-
дурах, находящихся в секции программ.
Строки-директивы начинаются символом "%" (этот символ в YACC
играет роль своего рода esc-символа). Две специальные директивы
%{ и %} ограничивают с двух сторон группы строк исходного текста.
Остальные директивы служат для задания дополнительной информации
о грамматике.
ДЕКЛАРАЦИЯ ИМЕН ЛЕКСЕМ
%token <список имен лексем>
Пользователь при описании грамматики решает, какие конструк-
ции целесообразнее непосредственно выделять из входного текста на
этапе лексического анализа, и на уровне этих конструкций (лексем)
задает грамматические правила. Все виды лексем, кроме литералов,
обозначаются некоторыми именами и под этими именами фигурируют в
секции правил. Благодаря декларации имен лексем в директиве
%token YACC отличает имена лексем от имен нетерминальных симво-
лов. Однако, декларация ни в коей мере не обеспечивает распозна-
вания лексем, и пользователю рекомендуется считать для себя ди-
рективу %token напоминанием о необходимости построения лексичес-
кого анализатора и руководствоваться этой директивой при его на-
писании.
Заметим, что ключевые слова описываемого входного языка час-
то бывает удобно считать лексемами. Имена лексем могут совпадать
с этими ключевыми словами, недопустимым является лишь совпадение
имен лексем с зарезервированными словами языка Си.
ДЕКЛАРАЦИЯ ПРИОРИТЕТОВ И АССОЦИАТИВНОСТИ ЛЕКСЕМ
%left <список лексем>
%right <список лексем>
%nonassos <список лексем>
Лексемы в данных директивах задаются литералами или именами.
В последнем случае декларация приоритета заменяет также деклара-
цию имени лексемы, хотя в целях обеспечения наглядности описания
является желательным присутствие в списке директивы %token всего
набора имен лексем.
Использование лексемы само по себе не требует задания ее
приоритета или ассоциативности.
При вызове YACC с флагом -d последовательность операторов
#define помещается также в информационный файл y.tab.h.
Переопределив при необходимости ряд номеров типов лек-
сем,пользователь должен проверить уникальность номеров у всех ис-
пользуемых лексем.
ДЕКЛАРАЦИЯ ИМЕНИ НАЧАЛЬНОГО СИМВОЛА
%start <имя начального символа>
Директива отменяет действующий по умолчанию выбор в качес-
тве начального символа нетериминала, определяемого первым грамма-
тическим правилом.
Секция программ-процедуры, которые должны быть включены в
генерируемую программу грамматического анализа. Любая из опреде-
ляемых пользователем программных компонент может находиться в
секции программ спецификационного файла, либо присоединяться на
этапе вызова Си-компилятора для трансляции файла y.tab.c и компо-
новки выходной программы. Перечислим процедуры, которые одним из
этих способов должны быть заданы:
- лексический анализатор - функция с именем yylex();
- все процедуры, вызовы которых содержатся в действиях, свя-
занных с грамматическими правилами;
- главная процедура main() при необходимости заменить ее
стандартный библиотечный вариант, который имеет вид:
main() { return (yyparse(); }
- процедура обработки ошибок yyerror() - также для замены
библиотечного варианта (его текст приводится ниже).
#include <stdio.h>
yyerror(s)char *s; { fprintf(stderr, "%s0, s);}
Для обеспечения корректной работы грамматического анализато-
ра функция лексического анализа yylex должна быть согласована с
конкретной спецификацией грамматики и удовлетворять определенным
требованиям. Основная задача функции yylex состоит во вводе из
входного потока ряда очередных символов до выявления конструкции,
соответствующей одной из лексем, и возвращении номера типа этой
лексемы. Для нелитеральных лексем номером типа может служить
об'явленное в секции деклараций имя лексемы (с помощью механизма
#define YACC обеспечивает замену его нужным номером), в случае
литералов номером типа является числовое значение символа (если
оно не было переопределено). Алгоритм поиска должен заключаться в
попытке нахождения сначала более сложных (нелитеральных) лексем и
лишь при несовпадении ни с одной из них текущих элементов ввода
возвращать номер типа литеральной лексемы (любой однозначный сим-
вол, не начинающий ни одну из возможных лексем, сам образует лек-
сему).
Действия с использованием псевдопеременных.Для обеспечения
связи между действиями, а также между действиями и лексическим
анализатором создаваемые YACC грамматические анализаторы поддер-
живают специальный стек, в котором сохраняются значения лексем и
нетерминальных символов. Значение лексемы автоматически попадает
в стек после ее распознавания лексическим анализатором (напомним,
что им считается текущее значение переменной yylval). После каж-
дой свертки вычисляется значение нетерминала, заместившего свер-
нутую строку, и помещается в вершину стека; значения элементов
правой части примененного правила перед этим выталкиваются из
стека. Заметим, что таким образом к моменту свертки любого прави-
ла все значения нетериналов в правой части оказываются вычислен-
ными в результате сверток. Способ вычисления значения нетермина-
ла будет рассмотрен ниже.
Описанный механизм не требует вмешательства пользователя и
предоставляет ему следующие возможности:
- Использовать в действиях, осуществляемых после свертки по
правилу, значение любого элемента его правой части. Доступ к этим
значениям обеспечивается набором так называемых ПСЕВДОПЕРЕМЕННЫХ
с именами $1,$2,..., где $i соответствует значению i-го элемента;
элементы правой части правила нумеруются слева направо без разли-
чия лексем и нетерминальных символов;
- Формировать в действиях значение, образованного в ре-
зультате свертки нетерминала путем присвоения этого значения
псевдопеременной с именем $$. Eсли в действии не определяется
значение переменной $$ (а также если действие отсутствует), зна-
чением нетерминала после свертки по умолчанию становится значе-
ние первого элемента правой части, т.е. неявно выполняется прис-
ваивание.
Несколько иная ситуация в отношении использования псевдопе-
ременных имеет место для правил, содержащих действия внутри пра-
вой части. На самом деле YACC интерпретирует правило вида
A: B C {действие 1} D {действие 2} K
как
A: B C EMPTY1 D EMPTY2 K;
EMPTY1: {действие 1}
EMPTY2: {действие 2}
и в точке, где вставлено действие, при разборе осуществляет-
ся свертка по пустому правилу, сопровождающаяся выполнением ука-
занного действия. При этом независимо от характера действия оче-
редной элемент в стеке значений отводится для хранения значения
неявно присутствующего "пустого" нетерминала. В действии, находя-
щемся в конце правила, соглашение о значениях псевдопеременных
остается прежним, если иметь в виду наличие дополнительных симво-
лов. В приведенном выше правиле в действии 3 для доступа к значе-
ниям элементов B, C, D, K следовало бы использовать соответствен-
но псевдопеременные $1, $2, $4, $6; псевдоперемнные $3, $5 хра-
нят результаты действий 1 и 2. В действиях, находящихся внутри
правила, с помощью псевдоперемнных $i доступны значения располо-
женных левее элементов, а также результаты предшествующих встав-
ленных в тело действий. Результатом внутреннего действия (т.е.
значением неявного нетерминалА) является значение, присвоенное в
этом действии псевдопеременной $$, при отсутствии такого присваи-
вания результат действия не определен. Заметим, что присваивание
значения псевдопеременной $$ во внутренних действиях не вызывает
предварительной установки значения нетерминала, стоящего в левой
части правила: это значение в любом случае устанавливется только
действием в конце правила или считается равным значению $1.
ЛЕКЦИЯ 11
СЕМАНТИЧЕСКИЕ ПРОГРАММЫ
Генерация какого─либо промежуточного кода большей частью
осуществляется одновременно с синтаксическим анализом. С этой
целью включаются действия, вызов которых обеспечивает не только
генерацию кода, но и построение таблиц символов, обращение к ним
и т.д. Свяжем с каждым правилом грамматики семантическую програм-
му, которая должна выполнять соответствующую семантическую обра-
ботку, когда связанное с ней правило вызывает синтаксическую ре-
дукцию. Рассмотрим как осуществить перевод арифметического выра-
жения с данными правилами в различные внутренние формы:
Правила грамматики:
Z ::= E
E ::= T|E+T|E─T|─T
T ::= F|T*F|T|F
F ::= I|(E)
1. Перевод инфиксной записи в польскую. Всякий раз, когда в
сентециальной форме найдена основа Х, редуцируемая к нетерминалу
U, синтаксический распознаватель вызывает программу, связанную с
правилами U ::= X.
Программа осуществляет обработку символов в Х и выдает ту
часть польской цепочки, которая имеет непосредственное отношение
к Х. Так как : (1) основа редуцируется при каждой редукции,то (2)
если в основе встречается нетерминал V, то часть польской цепоч-
ки, включающая подцепочку, которая приводится к V, уже была сге-
нерирована.
Считаем, что генерируемая польская цепочка хранится в од─
номерном массиве Р. Пусть р ─ содержит адрес первого свободно─
го элемента Р (Рнач=1). Вызванная программа имеет доступ к симво-
лам основы s(1),...,s(i), находящимся в синтаксическом стеке рас-
познавателя.
Программа, связанная с правилом Е1 ::= Е2+Т
В момент ее вызова согласно (2) массив Р содержит
...<код для Е2><код для Т>,
т.к. сначала генерируется код для Е (основа ─ самая левая
простая фраза), потом для Т. Справа от Т только терминалы.
Очевидно данная программа должна поместить знак + в поль─
скую цепочку. При этом Е2+Т переводится в запись Е2 Т +. Следо─
вательно семантическая программа имеет вид Р(р):="+";р:=р+1.
Программа для правила F ::= I (I─любой идентификатор)
По правилам польской записи (ЛК10) идентификаторы пред─
шествуют своим операторам и идут в том же порядке, что и в инфик-
сной записи. То есть необходимо лишь занести идентификатор в Р.
Р(р) := s(i); р:=р+1 s(i)─верхний символ стека
Для правила F ::= (E) действия не включаются, т.к. скобок в
польской записи нет, а для Е польская запись уже сгенерирова─
на. Аналогично рассуждая получим:
правило семантическая программа
(1) Z ::= T нет
(2) E ::= T нет
(3) E ::= E+T P(p):='+';p:=p+1
(4) E ::= E─T P(p):='─';p:=p+1
(5) E ::= ─T P(p):='@';p:=p+1
(6) T ::= F нет
(7) T ::= T*F P(p):='*';p:=p+1
(8) T ::= T/F P(p):='/';p:=p+1
(9) F ::= I P(p):=s(i);p:=p+1
(10)F ::= (E) нет
Очевидно для правил 3,4,7,8 можно иметь одну программу:
P(p):=s(i─1); p:=p+1
2.Преобразование инфиксной записи в тетрады
Будем генерировать теперь для А*(В+С) тетрады:
+В,С,Т1
*А,Т1,Т2
Первую тетраду попытаемся сгенерировать при редукции по прави─
лу Е ::= Е+Т. К этому моменту синтаксическое дерево будет иметь
вид (а).
Е,Т1
Е │
│ ┌──┴──┐
T T T │ │
│ │ │ E,B │
F F F │ │
│ │ │ T,A T,B T,C
A * (B + C) │ │ │
F,A F,B F,C
(a) │ │ │ (б)
A * ( B + C )
На следущем шаге приведения Е+Т к Е семантическая программа
должна выдать тетраду, однако сентенциальная форма {Т*(Е+Т)}не
содержит информации об именах Е и Т. При получении польской
записи имена В и С заносились в выходную цепочку при выполне─
нии редукций F ::= B и F ::= C.
Очевидно, что нам также необходимо в момент редукций F ::=I где─то запомнить информацию об именах редуцируемых
идентификаторов до момента их использования. Заметим, что ска─
нер для каждого идентификатора выдает пары вида (I,B),(I,C),.., причем символ I связан с синтаксической инфор─
мацией, В или С (имя) ─ с семантикой символа.
Привяжем к каждому нетерминалу U (узлу дерева) семанти─
ческую информацию U.SEM. Кроме того, будем генерировать имена
Т1,Т2,.. для обозначения подвыражений, используя для этого
счетчик i. В начале i=0. Операция генерации нового идентифика─
тора и его привязка к U имеет вид
i:=i+1; U.SEM:=Ti
Для генерации тетрады используем процедуру с 4 палитрами:
ENTER(W,X,Y,Z). Тогда получим:
правило семантическая программа
(1) Z ::= Е Z.SEM:=E.SEM
(2) E ::= T E.SEM:=T.SEM
(3) E1 ::= E2+T i:=i+1 E1.SEM:=Ti
ENTER('+',E2.SEM,T.SEM,E1.SEM)
(4) E1 ::= E2─T i:=i+1 E1.SEM:=Ti
ENTER('─',E2.SEM,T.SEM,E1.SEM)
(5) E ::= ─T i:=i+1 E.SEM:=Ti P(p):='@';p:=p+1
ENTER('@',0,T.SEM,E.SEM)
(6) T ::= F T.SEM:=F.SEM
(7) T ::= T*F
(8) T ::= T/F
(9) F ::= I
(10)F ::= (E) F.SEM:=E.SEM
Реализация семантических программ и семантических стеков
Для привязки семантических атрибутов к нетерминалам ис-
пользуются семантические стеки для каждого атрибута (или один
стек с отдельными полями для хранения синтаксического символа и
его семантических атрибутов). Эти стеки должны хранить семанти-
ческую информацию, которая связана с нетерминалами, входящими в
текущую сентенциальную форму. Эти стеки работают параллельно с
синтаксическим стеком S (т.е. удаление и занесение в них синхро-
низировано). Семантическая программа имеет доступ ко всем стекам.
┌───┬────────┬──────┬───────┬────┐
│ E │ │ REAL │ │ 20 │
├───┼────────┼──────┼───────┼────┤
│ + │ │ │ │ │
├───┼────────┼──────┼───────┼────┤
│ T │ │ INT │ │ 40 │
├───┼────────┼──────┼───────┼────┤
│ . │ │ . │ │ . │
│ . │ │ . │ │ . │
│ . │ │ . │ │ . │
└───┴────────┴──────┴───────┴────┘
В действительности не имеет значения, сколько используется
стеков.
Рассмотрим несколько алгоритмов семантической обработки
для языка типа АЛГОЛ.
1.Условные инструкции
<инстр1>::=<условие><инстр2>ELSE<инстр3>│<условие><инстр2>
<условие>::=IF<выраж>THEN,
где <выраж> должно иметь тип BOOLEAN.
Необходимо сгенерировать тетрады одного из двух видов: (1)
тетрады для Т::=<выраж> (1) тетрады для Т::=<выраж> (p) BZ
q+1,T,0 (p) BZ q,T,0
<инстр2> <инстр2>
(q) BR r (q)
(q+1) <инстр3>
(r)
Программа генерации (Р)тетрады связана с правилом:
<условие>::=IF<выраж>THEN и имеет следующий вид:
(1) Р:=<выраж>,ENTRY;─занести в Р адрес элемента TS для <выфаж>
(2) CHECKTYPE(P,BOOLEAN); ─проверить, что тип <выраж> BOOLEAN
(3) <условие>,JUMP:=NEXTQUAD; ─запомнить номер следущей тетрады
(4) ENTER(BZ,0,P,0); ─генерация BZ─тетрады
Общее правило для доступа к семантическим стекам: Если осно-
ва х рассматриваемой сентенциальной формы должна быть приведена с
помощью правила U::=x к U, то для работы с семантикой символов
используется программа, которая:
1) заносит информацию в TS или проверяет ее;
2) проверяет семантическую информацию, связанную с х;
3) генерирует тетрады;
4) связывает семантическую информацию с U;
Для обращения в стеки за семантической информацией SEM, свя-
занной с U, применяем обозначение U.SEM. 1.I.NAME ─ указатель,
используется для доступа к имени идентификатора
2.<пер>ENTRY ─ выдает указатель на элемент таблицы символов для переменной <пер>
3.<выр>ENTRY ─ указатель на значение (уже выполненного) выражения <выр>
4.<условие>JUMP ─ содержит номер BZ тетрады, в которую позднее надо добавить адрес перехода, который еще не известен
Следущая редукция будет связана с правилом: <инстр1>::=<ус-
ловие><инстр2> I:=<условие>.JUMP Занести номер следущей тетрады
во вторую QU- AD(I,2):=NEXTQUAD компоненту тетрады с помощью I,
т.е. получить (BZq+1,p,0)
Операнды во внутренней форме будем представлять указателем Р
на соответствующий элемент таблицы символов. Ссылка на его атри-
бут будет иметь вид: Р.TYPE,P.TYPE1. Очевидно, если инструкция
содержит ELSE, то между <инстр2> и <инстр3> нужно как бы вста-
вить команду BR. Но при таком синтаксисе конструкция <инстр1> с
ELSE будет распознана лишь после разбора <инстр2> и <инстр3>.
Т.е. будет специфицирована цепочка команд для <инстр2> и <инстр3>.
Преобразуем синтаксис в соответствии с желаемой семантикой:
<инстр1>::=<ист.часть><инстр3>
<ист.часть>::=<условие><инстр2>ELSE
<ист.часть>::=<условие><инстр2>ELSE
<ист.часть>.JUMP:=NEXTQUAD; ─ запомнить номер следущей тетрады
ENTER(BR,0,0,0); в стеке
I:=<условие>.JUMP; ─ занести в BZ переход через <инстр1>
QUAD(I,2):=NEXTQUAD;
<инстр1>::=<ист.часть><инстр3>
I:=<ист.часть>.JUMP; - вставить адрес перехода через
QUAD(I,2):=NEXTQUAD; <инстр2> в (BR...)
Выводы:
1. Когда редуцируется основа XY..Z, тетрады для всех нетер-
миналов, которые есть в основе, уже сгенерированы. Чтобы вста-
вить между ними дополнительные тетрады, следует изменить синтак-
сис в соответствии с требуемой семантикой.
2. Показано, как используется семантика, связанная с симво-
лом, для запоминания информации, которая потребуется позже. Оче-
видно, что можно допустить любое количество вложенных друг в дру-
га условных операторов, если на некоторых стадиях разбора иметь
произвольное число символов <условие>. С каждым из них будет свя-
зано свое <условие>.JUMP значение в семантическом стеке. Стеко-
вый механизм обеспечивает автоматическое сохранение каждой такой
компоненты отдельно.
Метки и переходы
Метка I определяется следущим образом:
<инстр1>::=I:<инстр2>,где
I - нетерминал, обозначающий идентификатор.
Метке I нужно присвоить адрес начала <инстр2>. Чтобы это
можно было сделать, изменим синтаксис таким образом:
<инстр1>::=<опред.метки><инстр2>
<опред.метки>::=I:
Имея ввиду, что в программе ссылаться на метку можно раньше,
чем она определялась, составим следущую семантическую программу:
<опред.метки>::=I;
LOOKUPDEC(I.NAME,P);- поиск описания метки с именем NAME среди иден-
IF P=0 THEN тификаторов. Если его нет(возвращается Р=0),то
BEGIN занести новый элемент с именем NAME в ST.
INSERT(I.NAME,P); Присвоить ему тип LABEL.
P.TYPE:=LABEL;
END
ELSE BEGIN
CHECKTYPE(P,LABEL); Cравнивает тип в ST (P.TYPE) с типом LABEL
IF P.DECLARED=1 THEN Проверяет не повторное ли это определение
ERROR (3) Печать ошибки
END
P.DECLARED:=1; Если нет, то установить признак метка опре-
P.ADRESS:=NEXTQUAD; делена и занести значение в поле ADRESS
Замечание:
1.сли Р-указатель на элемент ST, то для ссылки на его атрибуты
достаточно написать P.ADRESS и т.д. 2.Используем следущие атри-
буты(поля ST):
-TYPE (0=UNSIGNED;1=REAL;2=INT;3=BOOLEAN;4=LABEL)
-TYPE1 (1=простая перем.,2=имя массива,3=перем. с индексом)
-TEMPORARY (1=временная перем.,0-нет)
-DECLARED (0=неопределена,1=определена)
-ADBEWS Номер помеченной тетрады или адрес другого элемента ST
-NUMBER Размерность массива
Т.к. правило <инстр1>::=<опред.метки><инстр2> редуцируется
после генерации тетрад <инстр2>, то с ним не связывается никакой
дополнительной семантики ZB: инструкция GOTO в Фортране ис-
пользует подпрограмму просмотра всей ST (LOOKUP) ,т.к. метки ин-
струкций не должны повторяться.
<инстр>::=GOTO I
LOOKUP(I.NAME,P);
IF P=0 THEN
BEGIN INSERT(I.NAME,P);
P.TYPE:=LABEL;
P.DECLARED:=0;
END
ELSE CHECKTYPE(P,LABEL);
ENTER(BRL,P,0,0);
Подпрограмма CHECKTYPE(P,LABEL) сравнивает тип элемента ST,
на который указывает P с LABEL. В случае несовпадения печатается
сообщение об ошибке и ABORT.
3.Переменные и выражения
<пер>::=I|I(<список выр>)
<список выр>::=выр|<списоквыр>,<выр>
<пер>:=I -тетрад не генерирует
LOOKUP(I.NAME,P); поиск идентификатора
IF P=0 THEN ERROR(4);
<пер>.ENTRY:=P; связать с <пер> адрес найденного в ST элемента
В фортране если Р=0 нужно с помощью процедуры INSERT внести
идентифика тор в ST и установить TYPE равнымREAL или INT в зави-
симости от первой буквы идентификатора.
При обработке переменной с индексом I (<список выр>) необхо-
димо:
-сформировать тетрады для вычисления всех индексных выражений;
-вычислить адрес элемента массива, используя известный нам ме-
тод.
Чтобы вычислить VARPART необходимо преобразовать синтаксис
<инд>::=I(<выр>|<инд>,<выр> <пер>::=<инд>)
Программа для <инд>::=I(<выр> должна найти идентификатор
массива, сгенерировать тетрады для VARPART:=<выр> и связать с
<инд> адрес элемента ST для d1. <инд>::=I(<выр> LOOKUP(I.NAME,P);
-поиск идентификатора IF P=0 OR P.TYPE !=2 -он должен быть в ST и
иметь тип ARRAY.
THEN ERROR(5)
<инд>.COUNT:=P.NUMBER-1;-подсчет количества индексов
<инд>.ARR:=P; -запомнить адрес описателя массива
<инд>.ENTRY:=P+1; -занести в ENTRY адрес элемента ST для d1
GENERATETEMP(P); -генерация временной перем. типа INT для VARPART
P.TYPE:=INTEGER; -запомнить ее адрес
<инд>.ENTRY2:=P; -генерация тетрад для преобразования типа, если
CONVERTRI(<выр>.ENTRY); они нужны
P:=<выр>.ENTRY;
ENTER(:=,P,,<инд>.ENTRY2); -генерация тетрады занесения первого
индекса в VARPART
Подпрограмма GENERATETEMP(P) заносит в ST элемент для вре-
менной переменной, а адрес этого нового элемента возвращает в P.
Подпрограмма CONVERTRI(P)проверяет тип P-го элемента ST. Если тип
- INT, то ничего не делается, если REAL, то с помощью
GENERATETEMP заводится новая временная переменная типа INT и ге-
нерируется CVRI-тетрада для преобразования заданной переменной в
тип INT и занесения результата в новую временную переменную. Ука-
затель на временную переменную в ST остается в P. Если тип тип
Р-го элемента не INT и не REAL, то выдается сообщение об ошибке.
Перечислим семантическую информацию (в стеках), связанную с
<инд>:
1.<инд>.ENTRY -адрес элемента ST для d1(для di,если сгенери-
рован код i-го индексного выражения)
2.<инд>.ENTRY2 -адрес элемента ST для VARPART
3.<инд>.COUNT -[размерность - i], если сгенерирован код для
i-го индексного выражения
4.<инд>.ARR -адрес описателя имени массива в ST
Теперь для правила <инд1>::=<инд2>,<выр> надо сгенерировать
код, реализующий формулу VARPART:=VARPART*d1+<выр>, если это
второе по счету индексное выражение.
<инд1>::=<инд2>,<выр>
<инд1>.COUNT:=<инд2>.COUNT-1; -подсчет индексов
<инд1>.ARR:=<инд2>.ARR; -эапомнить тип элемента
<инд1>.ENTRY:=<инд2>.ENTRY+1;-в <инд1>.ENTRY занесли ук-ль на эл-тST для di
P1:=<инд2>.ENTRY2; -в P1 и в ENTRY2 адреса элемента ST для VARPART
<инд1>.ENTRY2:=P1;
ENTER(*,P1,<инд>.ENTRY,P1); -генерация тетрады VARPART=VARPART*di
P:=<выр>.ENTRY; -генерация тетрад преобразования R->I(если надо)
ENTER(+,P!,P,P!); -генерация тетрады VARPART=VARPART+индекс
Заметим, что мы всегда имеем дело не с самим выражением, а с
указателем на элемент ST, описывающий результат вычисления этого
выражения. Для правила <пер>::=<инд>) надо проверить (при компи-
ляции) количество индексных выражений и построить элемент STс
описанием элемента массива.
<пер>::=<инд>)
IF <инд>.COUNT!=0
THEN ERROR(6);
GENERATETEMP(P); -генерирование временной переменной для описателя
P.TYPE1%=3; элемента массива
P.TYPE:=<инд>.ARR.TYPE; -занести тип1,адресс эл-та ST дляVARPART,
P.ADRESS:=<инд>.ENTRY2; адрес эл-та ST, содержащего имя массива
P.NUMBER:=<инд>.ARR.NUMBER;
Трансляция описаний массивов
1) В польской записи описание массива
ARRAY A [L1:U1,...Ln:Un] можно представить в виде
L1U1...LnUn A ADEC
2) Для тетрад в виде
BOUNDS L1,U1
...
BOUNDS Ln,Un
ADEC A
Операция ADEC выполняет при семантической обработке следу-
щие действия:
-заносит запись о каждом массиве в ST;
-выделяет для каждого массива две ячейки:одну для хранеия
адреса начала массива, другую - для хранения адреса допвектора;
-формирует в обьектной программе (при генерации кода) коман-
ды, обеспечивающие перед входом в блок:
-вычисление компонент допвектора;
-вычисление адреса хранения массива;
-вычисление адреса хранения допвектора;
-занесение этих адресов в соответствующие ячейки.
Для массивов с постоянными границами компоненты допвектора
вычисляются в ходе трансляции и помещаются среди констант. Чтобы
отличить переменные граничные пары от постоянных нужно ввести до-
полнительный анализ операндов ADEC, являющихся граничными парами.
Допвектора могут располагаться вслед за парой ячеек, выделенной
последнему массиву или среди констант(для массива с постоянными
границами.
ЛЕКЦИЯ 12
СТРУКТУРЫ ДАННЫХ И ОРГАНИЗАЦИЯ ПАМЯТИ
Некоторые применяемые языки требуют динамического распреде-
ления памяти; блоки оперативной памяти при этом выделяются произ-
вольно, а затем освобождаются. Область данных - это блок ОП, вы-
деленный для данных.
Области данных могут принадлежать также к статическому клас-
су. Статическая область данных имеет постоянное число ячеек, а
динамическая область данных не всегда присутствует во время счета.
Если вызов процедуры происходит рекурсивно, то существует
несколько областей данных в ОП, каждая для отдельного вызова про-
цедуры.
Если компилятор знает все характеристики переменных во вре-
мени, то он может сгенерировать полностью команды обращеиня к пе-
ременным, основываясь на этих характеристиках.
Память выделяется компилятором не только для переменных, но
и для описателей, содержащих атрибуты, определяемые во время сче-
та. Этот описатель заполняется и изменяется во время счета. Типы
данных исходной программы должны быть отображены на типы данных
машины. Целые переменные содержатся обычно в одном слове.
Информационные таблицы
При анализе программы из описаний, заголовков процедур, цик-
лов и т.д. извлекается информация и сохраняется для последующего
использования. Эта информация обнаруживается в отдельных точках
программы и организуется так, чтобы к ней можно было обратиться
из любой части компилятора. В каждом компиляторе в той или иной
форме используется таблица символов. Это таблица идентификаторов,
встречающихся в программе, вместе с их атрибутами.
При разборке компилятора невозможно определить вид и содер-
жание информации, которую следует собирать, до тех пор, пока не
будут достаточно обстоятельно продуманы команды обьектной прог-
раммы для каждой инструкции исходной программы и сама синтезирую-
щая часть компилятора.
При проверке правильности семантики и генерации кода тре-
буются знания характеристики идентификатора. Эти характеристики
выясняются из описания и накапливаются в таблице символов. Наип-
ростейшая таблица символов для каждого элемента имеет поле аргу-
мента и значения. Аргументами таблицы являются символы или иден-
тификаторы, а значениями - их характеристики. Так как число ли-
тер в идентификаторе непостоянно, в аргументе часто помещают сим-
волы вместо самого идентификатора. Это позволяет сохранить фикси-
рованный размер аргумента. Идентификаторы хранятся в специальном
списке строк.
При проходе исходной программы через компилятор при встрече
конструкции описания происходит запись идентификатора исходной
программы в таблицу символов вместе с его атрибутами. В результа-
те дальнейшего чтения исходной программы, в компиляторе при на-
хождении любого идентификатора программа обращается к таблице
символов и ищет в ней данный идентификатор. Если идентификатор не
обнаружен, то выдается сообщение, что данный идентификатор не оп-
ределен. Если же он обнаружен, то производится сравнение данного
идентификатора с записанным в таблице символов и производятся
необходимые преобразования.
При работе с таблицей символов нужно разработать правила ор-
ганизации и обращения к таблице символов. Таблицы символов могут
быть как упорядоченными, так и неупорядоченными.
При упорядоченном списке элементов наиболее результативным
является бинарный или логарифмический поиск. Иногда один и тот же
идентификатор может быть описан и использован много раз в различ-
ных блоках и процедурах, и каждое такое описание должно быть
единственным. Соответственно нужно разделять таблицу симводов.
При этом устанавливается правило нахождения соответствующего
идентификатора. Оно состоит в следующем: сначала просматривается
текущий блок, затем обьемлющий блок и т.д., до тех пор, пока не
будет найдено описание данного идентификатора.
При осуществлении поиска все элементы таблицы хранятся для
каждого блока в смежных ячейках и используется список блоков.
Информация об идентификаторе хранится в той части таблицы,
которую мы определили как "значение".
Таблица символов состоит из 5-ти различных списков:
- список меток;
- список арифметических констант;
- список имен общих блоков,имен подпрограмм и имен перемен-
ных;
- список общих блоков;
- список имен подпрограмм.
Элементы всех этих списков помещаются в одной и той же таб-
лице; первые два байта каждого элемента используются для ссылки
на следующий элемент в том же списке.
ЛЕКЦИЯ 13
ВНУТРЕННИЕ ФОРМЫ ИСХОДНОЙ ПРОГРАММЫ
Ввиду сложности реальных языков программирования и предъяв-
ления повышенных требований к эффективности самого процесса ком-
пиляции и к готовым программам, разработчики вынуждены проектиро-
вать многопроходные компиляторы. При этом для связи между прохо-
дами, необходимы внутренние (промежуточные) формы исходной прог-
раммы. В большинстве внутренних форм, операторы располагаются в
том порядке, в котором они должны выполняться, что означает пос-
ледующий анализ и итерацию об'ектного кода. (Эти внутренние пред-
ставления можно использовать для интерпретации).
Внутренняя форма является более развернутым представлением
исходной программы, к которой предъявлялись требования лаконич-
ности и краткости. Более подробное представление обеспечивает
проведение глубокого анализа и оптимизации программ.
Как правило, в одном компиляторе для разных синтаксических
единиц (выражений, условных операторов, операторов присваивания и
т.д.) используются разные, наиболее подходящие с точки зрения
разработчика, внутренние формы.
1.1. Опреанды и операторы
Внутренние формы содержат операторы и операнды. В различных
видах представлений существенное отличие заключается в форме сое-
динения операторов и операндов.
Операторы: + , - , / , * , BR (branch) и т.п. Операнды : -
простые имена (переменных, процедур и т.д.);
- константы;
- временные переменные, генерируемые компилятором;
- переменные с индексами.
Каждый операнд (за исключением переменных с индексом) пред-
ставляется указателем на соответствующий элемент в таблице симво-
лов, констант или временных переменных.
В поле операнда предусматривается признак косвенной адреса-
ции, чтобы указать таким путем, что значение в таблице, на кото-
рое указывает операнд, является адресом расположения значения,
которое требуется операнду при исполнении команды.
Пременную с индексами А[i,j,...,k] можно обрабатывать сле-
дующим образом:
- сначала включить последовательность операций для вычисления
VARPART и запоминания ее во внешней ячейке Т;
- сам операнд представить двумя указателями: на элемент с име-
нем массива и на значение VARPART, т.е. А[i,j,...,k] можно
представить в виде А[T]. Такой операнд занимает две ячейки
во внутреннем представлении, но зато позволяет генерировать
более эффективную объективную программу. Простая переменная
┌───┬───┬─────────────────────────────────────┐
│ 1 │ I │ указатель на эл-т таблицы символов │ I - признак
└───┴───┴─────────────────────────────────────┘ косвенной
Константа адресации
┌───┬───┬─────────────────────────────────────┐
│ 2 │ │ указатель на эл-т таблицы констант │
└───┴───┴─────────────────────────────────────┘
Временная переменная
┌───┬───┬─────────────────────────────────────┐
│ 3 │ I │ указатель на эл-т табл. врем. перем.│
└───┴───┴─────────────────────────────────────┘
Перменная с индексами
┌───┬───┬─────────────────┬───┬───┬───────────────┐
│ 4 │ I │ ук-ль на эл-т │ х │ I │ ук-ль на эл-т │
│ │ │ с именем массива│ │ │ с индексом │
└───┴───┴─────────────────┴───┴───┴───────────────┘
┌─────────────────────────┘
│ описание индексов
х = 1 - указатель на табл. символов
2 - указатель на табл. констант
3 - указатель на табл. временных переменных
Форматы операндов
Польская запись
1.Польский логик Я.Лукашевич впервые применил запись арифмети-
ческих и логических выражений, которая без скобок указывает точ-
ный порядок выполнения операций. В ней операторы следуют непос-
редственно за операндами (постфиксная запись). Она определяется
следующими правилами:
1) операнды следуют в том же порядке, как они представлены в
префиксной записи;
2) операторы следуют в том же порядке, в каком они должны
вычисляться (слева направо);
3) опер-ры располаг-ся непосредственно за своими оп-дами.
Это можно представить следующими правилами: <операн-
д>::=<идентификатор>|<операнд><операнд><оператор>
<оператор>::= + | - | / | * | ... Для унарных оперций можно
ввести новый символ ( например @ для -) и еще одно правило
<операнд>::=<операнд>@ Пример A * ( B + C / D ) <=> ABCD / + *
A + ( -B + C * D ) <=> AB@CD * + +
(C таким же успехом можно применять префиксную запись).
Вычисление арифметических выражений
Данные правила определяют порядок обработки выражения с по-
мощью стека за один просмотр выражения слева направо, начиная с
самого левого символа входной цепочки:
1. Если сканируемый символ идентификатор, то его значение
заносим в стек и переходим к следующему символу (правило <оп-
реанд>::= идентификатор)
2. Если сканируемый символ - бинарный оператор, он приме-
няется к двум верхним операндам в стеке и замещает их на получен-
ный результат, что эквивалентно правилу <операнд>::= <операнд><о-
перанд><оператор>.
3. Если сканируемый символ - унарный оператор, то он приме-
няется к верхнему символу стека и затем замещает его результатом
(правило <операнд>::= <операнд><оператор> [ Д/З - стр.282 ]
Включение в польскую запись других операторов
1) Присваивание <пер.>::= <выр.> ( <=><пер.><выр.>:= )
прим.: А := В * С + D <=> АВС * D + :=
- После выполнения оператора := из стека исключаются <пер.> и
<выр.>, т.к. этот оператор не имеет результирующего значения в
отличие от бинарных арифметических операторов.
- Кроме того, в стеке находится не значение <пер.> (оно нам
не нужно), а ее адрес, т.к. в рез-те присвоения по нему заносит-
ся значение <выр.>
2) Оператор GOTO А <=> A BRL,
где метка А представлена адресом соответствующего ей эл-та
таблицы символов. Оператор BRL (Branch to label)
3) Условные переходы
<операнд1><операнд2> BP, где первый операнд является значе-
нием арифметического выражения, второй указывает номер (место)
символа в цепочке польской записи. Если операнд1 положителен
(positive), то в качестве следующего берется символ, на который
указывает операнд2, иначе работа продолжается как обычно.
BP - переход по положительному значению, ВМ - по минусу, BZ
- по нулю, BPZ - по неотрицательному значению, и т.д.
4) Условная инструкция
IF<выр>THEN<инстр.1>ELSE<инстр.2><=><выр><С1>BZ<инср.1><С2>BR<инстр.2>
С1 - номер имвола, с которого начинается <инстр.2>.
С2 - номер символа,следующего за <инстр.2>.
Операторы BZ и BR не порождают результирующего значения.
Часть их работы состоит в исключении из стека двух верхних эле-
ментов (значения <выр> и <С1>) для BZ и соответственно одного
<С2> для BR. Оператор безусловного перехода <С2>BR - использует-
ся метка <С2> для внутренних генерируемых переходов. В то время
как оператор <метка> BRL в качестве значения <метка> использует
адрес эл-та таблицы символов.
5) Описание массива. ARRAY A[Li:Ui,...,Ln:Un]
можно представить в виде:
LiUi...LnUn A ADEC, где ADEC - оператор, имеющий переменное
число операндов, зависящее от числа индексов. Операнд А - оче-
видно, адрес элемента таблицы символов для А -> При вычислении
ADEC, следовательно, из этого элемента таблицы извлекается ин-
формация о размерности массива А (т.е. и о числе операндов ADEC)
- с этой целью изменен порядок записи операндов.
6) Переменная с индексами A[<выр.i>,...,<выр.n>] преставляется
в виде <выр.1>...<выр.2> A SUBS
Оператор SUBS используя элемент А таблицы символов и ин-
дексные выражения, вычисляет адрес элемента массива. Затем опе-
ранды исключаются из стека и на их место заносится новый опе-
ранд, определяемый адресом элемента массива и его типом.
Использование для индексирования специального оператора
SUBS - более удобный способ для польской записи.
Пример: BEGIN INTEGER K; ARRAY[1:I-j]; K:=0;
L:IF I>j THEN K:=K+A[I-j]*6 ELSE
BEGIN I:=I+1;I:=I+1;COTOL END
END
(1) BLOCK 1 IJ - A ADEC K0 := Польская запись
(11) IJ - 29 BMZ
(16) K KIJ - A SUBS 6*+:= 41 BR Для каждого символа отво-
(29) II1 + := II1 + := L BRL дится одна строка (место)
(41) BLCEND
Как видно, описание INTEGER K (не требующее генерации ко-
манд) отсутствует во внутреннем представлении. Оно нужно для
формирования элемента таблицы символов для К.
Введены два оператора без операндов BLOCK (начало блока) и
BLCKEND (конец блока).
N содерж. N содерж. таблица
слова слова символ слова слова символ символов
┌────┬────┬────┬───────┬────┬────┬────┬────────┬───┬─────┐
│ 1 │ 11 │ │ BLOCK │ 36 │ 6 │ │ SUBS │ 1 │ I │
├────┼────┼────┼───────┼────┼────┼────┼────────┼───┼─────┤
│ 2 │ 1 │ 1 │ 1 │ 37 │ 1 │ 6 │ 6 │ 2 │ Y │
├────┼────┼────┼───────┼────┼────┼────┼────────┼───┼─────┤
│ 4 │ 2 │ 1 │ I │ 39 │ 15 │ │ * │ 3 │ A │
├────┼────┼────┼───────┼────┼────┼────┼────────┼───┼─────┤
│ 6 │ 2 │ 2 │ Y │ 40 │ 14 │ │ + │ 4 │ K │
├────┼────┼────┼───────┼────┼────┼────┼────────┼───┼─────┤
│ 8 │ 16 │ │ - │ 41 │ 7 │ │ := │ 5 │ L25 │
├────┼────┼────┼───────┼────┼────┼────┼────────┼───┴─────┘
│ 9 │ 2 │ 3 │ A │ 42 │ 1 │ 64 │ 64 │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 11 │ 13 │ │ ADEC │ 44 │ 9 │ │ BR │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 12 │ 2 │ 4 │ K │ 45 │ 2 │ 1 │ I │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 14 │ 1 │ 0 │ 0 │ 47 │ 2 │ 1 │ I │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 16 │ 7 │ │ := │ 49 │ 1 │ 1 │ 1 │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 17 │ 2 │ 1 │ I │ 51 │ 14 │ │ + │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 19 │ 2 │ 2 │ Y │ 52 │ 7 │ │ := │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 21 │ 16 │ │ - │ 53 │ 2 │ 1 │ I │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 22 │ 1 │ 45 │ 45 │ 55 │ 2 │ 1 │ I │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 24 │ 8 │ │ BMZ │ 57 │ 1 │ 1 │ 1 │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 25 │ 2 │ 4 │ K │ 59 │ 14 │ │ + │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 27 │ 2 │ 4 │ K │ 60 │ 7 │ │ := │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 29 │ 2 │ 1 │ I │ 61 │ 1 │ 5 │ 5 │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 31 │ 2 │ 2 │ 7 │ 63 │ 10 │ │ BRL │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 33 │ 16 │ │ - │ 64 │ 12 │ │ BLCEND │
├────┼────┼────┼───────┼────┼────┼────┼────────┤
│ 34 │ 2 │ 3 │ A │ │ │ │ │
└────┴────┴────┴───────┴────┴────┴────┴────────┘
Внутреннее представление польской записи
- операторы занимают по одной ячейке и представлены числами:
6 = SUBS, 7 = :=, 8 = BMZ, 9 = BR, 10 = BRL, 11 = BLOCK,
12 = BLCKEND, 13 = ADEC, 14 = +, 15 = *, 16 = -.
Константа занимает два слова: первое 1, второе - значение
ее. Для идентификатора аналогично: первое слово 2, второе - ад-
рес (индекс) элемента таблицы символов идентификатора.
Метки, генерируемые для внутренних переходов равны соот-
ветств. номерам ячеек.
ТЕТРАДЫ.
1) Арифметические выражения:
(<оператор>,<операнд1>,<операнд2>,<результат>)
т.о. 1. А * В => *,А,В,Т, где Т некоторая переменная, кото-
рой присваивает результат вычисления А * В.
2. А * В + С * D => *, A, B, T1 ┐ тетрады располагаются в
*, C, D, T2 ├ том порядке, в котором
+, T1, T2, T3 ┘ они должны вычисляться
Для унарных операторов (-А) <операнд2> остается пустым
(-,А, ,Т) 2)
2) Тетрады для других операторов.
1] BR i - переход на i-ю тетраду
2] BZ i,P - переход по "0" (BP - по "+", BM - по "-")
3] BG i, P1, P2 - переход, если значение в P1 > чем в P2
( BL - < , BE - = )
4] BRL P - переход на тетраду, номер которой в Р-м
элементе таблицы символов
5] +[*,/,-] P1, P2, P3
6] := P1, ,P3
7] CVRI P1, ,P3 - преобразование значения, описанного в Р1,
из REAL в INT и запоминание в Р3
8] BLOCK
9] BLCKEND
10] BOUNDS P1, P2 - Р1 и Р2 описывают граничную пару массива
11] ADEC P1 - массив описан в Р1. Если он имеет размер-
ность n, то этой тетраде предшествует опе-
ратор BOUNDS, задающий n граничных пар.
ИНДЕКСИРОВАНИЕ
Пример С := А [i, B[j]], если d1
описывает диапазон изменения *, ,d1,T1
второго индекса массива А, то +,T1,B[j],T2
получим следующее представление :=,A[T2], ,C
(1) BLOCK (10) + K,T4,T5
(2) -I,j,T1 (11) := T5, , K
(3) BOUNDSI,T1 (12) BR18
(4) ADEC A (13) +I,1,T6
(5) := 0, ,K (14) := T6, ,I
(6) -I,j,T2 (15) +I,1,T7
(7) BMZ13,T2 (16) := T7, ,I
(8) -I,j,T3 (17) BRL L
(9) *A[T3],6,T4 (18) BLCKEND
ТРИАДЫ
<оператор><операнд1><операнд2>
В ней нет поля результата. За счет этого сокращается запись
и количество временных переменных. При обработке триады, ре-
зультат которой будет в дальнейшем использоваться, генератор ко-
да должен сгенерировать описание ее результата, которое уничто-
жается после его использования.
(1) BLOCK (10) + K,(9)
(2) -I,j (11) := (10), K
(3) BOUNDS 1,(2) (12) BR (18)
(4) ADEC A (13) + I, 1
(5) := 0,K (14) := (13), I
(6) -I,j (15) + I, 1
(7) BMZ(13),(6) (16) := (15), I
(8) -I,j (17) BRL L
(9) * A[(8)],(6) (18) BLCKEND
Здесь (2) - ссылка на результат второй триады. Компилятор
заводит новый тип операнда для результата триад (первое слово
операнда)
ДЕРЕВЬЯ
Для любого арифметического выражения можно построить дерево,
корню которого соответствует последняя триада. Каждая i-я триада
соответствует поддереву, оператор триады - корень поддерева, опе-
ранд - либо идентификатор(лист), либо номер триады, описывающий
поддерево. От того, как рассматриваются триады (как последова-
тельность операций в порядке их выполнения или как дерево), су-
щественным образом зависит генерируемый объектный код. Дерево
иногда позволяет сгенерировать более эффективный объектный код.
Пример 1. A * B + C - D * E
-
┌───┴───┐ (1) ( * A,B )
+ * (2) ( + (1),C )
┌──┴──┐ ┌──┴──┐ (3) ( * D,E )
* C D E (4) ( - (2),(3) )
┌──┴──┐
A B
Пример 2 BEGIN A := B; B := C; D := C; END
<составная инстр.>
┌───────────────────────┼───────────────────────┐
BEGIN <список инстр.> END
┌─────────┬──────────┬──────────┐
<инстр.> <инстр.> <инстр.> <инстр.>
Дерево │ │ │ │ Триады
-------- := := := <пусто> --------
┌─┴─┐ ┌─┴─┐ ┌─┴─┐ (1) (:=B,A)
A B B C D C (2) (:=C,B)
(3) (:=C,D)
При представлении инструкций, блоков, описаний и т.д. триа-
ды не образуют уже полного дерева, т.к. связи между различными
инструкциями и описаниями явно не заданы.
В дереве отражены прямые связи (указатели) с инструкциями, в
то время как в триадах эти связи подразумеваются.
Промежуточная форма исходной программы
Первоначальная исходная программа переводится в некоторую
внутреннюю форму, удобную для простой машинной обработки. Внут-
реннее представление исохдной программы в значительной степени
зависит от дальнейшего использования. Это может быть дерево, от-
ражающее синтаксис исходной программы. Это может быть исходная
программа в польской записи. Используется также форма - список
тетрад (операнд, операнд, операнд, результат) в порядке их выпол-
нения.
После синтаксического анализа можно считать, что исходная
программа преобразована в дерево, называемое синтаксическим. В
этом дереве внутренние вершины в основном соответствуют опера-
циям, а листья представляют операнды, состаящие из указателей
входов в таблицу имен. Структура синтаксического дерева отражает
синтаксические языка программирования, на котором написана исход-
ная программа. Для физического представления существует нес-
колько способов. Во внутренней форме операторы не изменяют поря-
док следования. Все внутренние представления обычно содержат 2
вещи: операторы и операнды. Различие состоит в том, как эти опе-
раторы и операнды соединяются.
Промежуточная программа должна отражать синтаксическую
структуру исходной программы. Операндами являются простые имена
(переменные, константы, процедуры и т.д.). Компиляторы, осущес-
твляющие значительную работу по оптимизации кода, создают де-
тальное представление промежуточной программы, точно описывающее
порядок выполнения исходной программы. В других компиляторах
представлением промежуточной программы служит простое представле-
ние синтаксического дерева, такое как польская запись.
Польская запись
Для представления арифметических и логических выражений ис-
пользуется польская запись. Она имеет ряд преимуществ перед ин-
фиксной: формула может быть записана без скобок; эта форма пред-
ставления очень удобна для ЭВМ со стековой адресацией; если зна-
ки операций в инфиксной форие различаются по старшинству, то
польская запись устраняет эту систему приоритетов).
В польской записи операнды следуют непосредственно за опера-
торами. Вычисление таких записей производится с помощью стека,
где будут находиться все операнды, встретившиеся при просмотре
выражения.
Просмотр начинается с самого левого символа. Прочитав его и
обработав, переходим к следующему. Последовательность обработки
такова:
1) если сканирующий символ - идентификатор или константа, то
его значение заносится в стек и осуществляется переход к следую-
щему;
2) если сканирующий символ-бинарный оператор, то он приме-
няется к двум верхним операндам в стеке и затем они заменяются на
полученный результат;
3) если сканирующий символ - унарный оператор, то он приме-
няется к верхнему операнду в стеке, который затем заменяется на
полученный результат.
Тетрады
Для бинарных операций удобной формой представления являются
тетрады. Тетрада имеет вид: <оператор> <операнд1> <операнд2>
В тетраде отсутствует поле результата. Если позже какой-ли-
бо операнд окажется результатом данной операции, то он будет на
нее непосредственно ссылаться.
Существуют и другие формы внутреннего представления.
Деревья
Мы опpеделили КС-язык, задаваемые некотоpой гpамматикой, как
множество теpминальных цепочек, котоpые можно вывести из на-
чального символа. Можно постpоить деpево вывода цепочки КСязыка.
Это легко сделать, интеpпpетиpуя подстановки, как шаги постpое-
ния деpева.Однако деpево не несет никакой инфоpмации о поpядке
пpименеия пpавил, кpоме того что пpавила должны пpименяться к
каждой веpшине деpева pаньше, чем к нетеpминальным веpшинам pас-
положенным ниже. Поскольку поpядок вывода в деpеве скpыт, то мо-
жет быть несколько выводов, соответствующих одному и тому же
деpеву вывода. Для каждого деpева существует единственный левый и
единственный пpавый вывод, котоpый получается, если всегда заме-
нять самый пpавый нетеpминал. Многие методы обpаботки языков pас-
читаны исключительно на левые или пpавые выводы,так как они очень
удобны для семантической обpаботки. Когда одна цепочка может
иметь несколько деpевьев вывода, говоpят, что соответствующая
гpамматика неоднозначна. Все сказанное можно pезюмиpовать следую-
щим обpазом:
1. Каждой цепочке, вводимой в данной КС-гpамматике, соответ-
ствует одно или несколько деpевьев вывода.
2. Каждому деpеву соответствует один или несколько выводов.
3. Каждому деpеву соответствует один пpавый и один левый вы-
вод.
4. Если каждой цепочке, вводимой в КС-гpамматике, соответ-
ствует единственное деpево вывода, эта гpамматика называется од-
нозначной; в пpотивном случае ее называют неоднозначной.
ЛЕКЦИЯ 14
ОПТИМИЗАЦИЯ ПРОГРАММЫ
Улучшение выходной программы обычно называют ее оптимиза-
цией, а часть транслятора, выполняющая эту функцию - отимизирую-
щей частью транслятора.
Оптимизирующая часть транслятора:
1. Устраняет недостатки программы,вызванные небрежностью или
низкой квалификацией программиста.
2. Устраняет излишние вычислеия, неизбежно возникающие в
процессе трансляции даже при самом тщательном написании програм-
мы на языке высокого уровня.
Если транслятор производит оптимизацию программы, необходи-
мо делать специальный проход, переводящий программу с исходного
языка на промежуточный.
Оптимизировать программу, уже протранслированную в коды ма-
шины, трудно по трем причинам: во-первых, единицы действия прог-
раммы в кодах команд слишком мелки, что уже само по себе затруд-
няет анализ, во-вторых, при трансляции входной программы в коды
машины возможна потеря имеющейся в ней информации. Например, за-
сылка промежуточных результатов в разные рабочие ячейки памяти
делает практически невозможной идентификацию одинаковых частей
программы; в-третьих из-за нестандартности форматов различных
элементов языка и рекурсивных конструкций, широко применяемых в
текстах программ.
Строго сформулировать требования, предьявляемые к промежу-
точному языку, трудно.
Однако уже из самого обоснования необходимости промежуточно-
го языка видно, что:
а) операторы языка не должны быть слишком мелкими;
б) символы, идентификаторы и числа должны иметь фиксирован-
ный формат;
в) в строении операторов желательно отсутствие рекурсивности;
г) должна сохраняться вся информация, необходимая для опти-
мизации, которая есть во входном языке;
д) язык должен быть приспособлен к выполнению оптимизирую-
щих преобразований и удобен для последующей трансляции в коды вы-
числительной машины.
Требования пп. "г" и "д" показывают, что разработать еди-
ный универсальный промежуточный язык для трансляции с любого язы-
ка программирования в коды любой ВМ трудно.
Помимо программы на промежуточном языке, состоящей из после-
довательности операторов, необходимы следующие таблицы:
1. Таблицы идентификаторов и констант с обычной информацией
о переменных и константах;
2. Таблица блоков, определяющая номера блоков, их границы,
непосредственно предшествующие и следующие блоки, а также любую
информацию о частоте повторения блока;
3. Таблица последовательности операторов, определяющая ли-
нейную последовательность операторов в блоке. Она содержит после-
довательность указателей операторов mi. Эта таблица необходима,
поскольку один указатель может принадлежать нескольким операторам.
Подстановка и устранение идентичных операторов
Подстановка - это замена переменной или mi - идентификатора
результата заданной или вычисленной константой, причем эта заме-
на производится во время трансляции, а не в процессе решения.
Подстановка является полностью внутриблочной процедурой и
выполняется перед устранением излишних команд.
Сдвиг инвариантных операторов
Сильно связанной областью называется такое множество его уз-
лов, что для любых двух вершин x и y (x != y) существует путь из
x в y.
Оператор инвариантен в сильно связанной области, если его
операнды не зависят от места определения переменных в данной об-
ласти.
Будем рассматривать сильно связанные области Ri, обладающие
следующими свойствами:
1) Ri является сильносвязанной областью, состоящей из мно-
жества блоков, каждый из которых предшетвует сам себе и следует
сам за собой внутри этого множества;
2) Ri != Rj;
3) для каждого i<j или Ri Rj = 0, или Ri Rj = Ri, т.е.
Rj Ri.
Как уже отмечалось, сдвиг инвариантного оператора из тела
цикла сокращает время выполнения программы. Особенность рассмат-
риваемого метода заключается в том, что оператор сдвигается из
блока во всех случаях, когда он может быть сдвинут независимо от
того, находится он внутри цикла или нет. Ухудшение программы
произойти не может.
Замена переменных в операторах условного перехода
В результате сокращения глубины операции рекурсивная прог-
раммная переменая , являющаяся управляющей в операторе условного
перехода, может быть заменена в нем генерируемой переменной t(mi-
идентификаторов).
Процедура замены переменной в операторе условного перехода
заключается в следующем. После сокращения глубины операции во
всех операторах, использующих рекурсивно определяемые програм-
мные переменные I, находят операторы условного перехода, в кото-
рых I является управляющей переменной.
Определение не используется и может быть устранено, если ре-
зультат определения не является операндом ни одного оператора ре-
курсивного определения и результат этого последнего не ис-
пользуется ни в каком другом операторе.
Как только определение устранено, все вычисления, от кото-
рых оно зависит, если они нигде больше не используются, могут
быть устранены.
Вставка псевдоблока
В процессе оптимизации операторы, сдвигаемые из блоков, со-
бираются в псевдоблок. После оптимизации области Rk операторы
псевдоблока должны быть вставлены в программу так, чтобы они вы-
полнялись до (после) выполнения операторов области Ri.
Для того, чтобы операторы псевдоблока выполнялись на всех
входных (выходых) путях области Rk, они должны вставляться во все
блоки, непосредственно предшествующие (следующие) области либо из
псевдоблока должен быть сформирован блок ,который будет вставлен
на все входные (выходные) пути области Rk.
ЛЕКЦИЯ 15
ОПТИМИЗАЦИЯ ПРОГРАММЫ (ПРОДОЛЖЕНИЕ)
Синтез (генерация) выходного текста
Промежуточный код
Промежуточные коды (или обьектные языки) можно проектиро-
вать на различных уровнях. Так, иногда промежуточный код полу-
чают, просто разбивая сложные структуры языка на более удобные
для обращения элементы. Однако можно в качестве промежуточного
кода ( в этом случае его чаще называют обьектным языком ) ис-
пользовать какой-либо обобщенный машинный код, который затем
транслируется в код реальной машины. Получение промежуточного ко-
да возможно до или после распределения памяти. Если это происхо-
дит до распределения памяти, то операндами могут служить иденти-
фикаторы программы ( или их представления после лексического ана-
лиза ) и присваиваемые компилятором идентификаторы, причем в пос-
леднем варианте используются адреса времени прогона.
Одним из видов промежуточного кода являются четверки.
Например, выражение (-a+b)*(c+d) можно представить как чет-
верки следующим образом: -a = 1
1+b = 2
c+d = 3
2*3 = 4
Здесь целые числа соответствуют идентификаторам, присва-
иваемым компилятором. Четверки можно считать промежуточным
кодом высокого уровня. Такой код часто называют трехадресным
- два адреса для операндов ( кроме тех случаев, когда имеют
место унарные операции ) и один для результата. Другой вари-
ант кода - тройки ( двухадресный код ). Каждая тройка состоит
из двух адресов операндов и знака операции. Если сам операнд
является тройкой, то используется ее позиция, что исключает
необходимость иметь в каждой тройке адрес результата.
Выражение a+b+c*d можно представить в виде четверок:
a+b = 1
c*d = 2
1+2 = 3
и в виде троек:
a+b
c*d
1+2
Тройки компактнее четверок, но если в компиляторе есть
фаза оптимизации, которая пресылает операторы промежуточного
кода, их применение затруднительно. Наилучшее решение этой
проблемы - косвенные тройки, т.е. операнд, ссылающийся на ра-
нее вычисленную тройку, должен указывать на элемент таблицы
указателей на тройки, а не на саму эту тройку.
Как тройки, так и четверки можно распространить не толь-
ко на выражения, но и на другие конструкции языка. Например,
присваивание a := b в виде четверки представляется как
a := b = 1
a в виде тройки - как a := b
Аналогично условное предложение
IF a THEN b ELSE c FI
можно считать выражением с тремя операндами, которому требу-
ются четыре адреса как четверке и три - как тройке.
Не менее популярны в качестве промежуточного кода пре-
фиксная и постфиксная нотации. В префиксной нотации каждый
знак операции появляется перед своими опреандами, а в пост-
фиксной - после. В этом и состоит их отличие от обычной ( ин-
фиксной ) нотации, в которой обозначения двухместных операций
появляются между своими операндами. Например, инфиксное выра-
жение a+b в префиксной нотации примет вид + ab , а в пост-
фиксной - вид ab +.
Префиксная нотация известна также как польская запись, а
постфиксная - как обратная польская запись. С помощью этих
нотаций можно записывать более сложные выражения. Например,
выражение (a+b)*(c+d) в префиксной форме записывается следую-
щим образом: *+ab+cd
а в постфиксной так: ab+cd+*
Каждый знак опреации в префиксной нотации ставится не-
посредственно перед своими операндами, а в постфиксной после
них.
В префиксной и постфиксной нотациях скобки уже не требу-
ются, так здесь никогда не возникает сомнений относительно
того, какие операнды принадлежат к тем или иным знакам опера-
ций. В этих нотациях не существует приоритета знаков опера-
ций, хотя при преобразовании инфиксных выражений в префиксные
или постфиксные их приоритет, несомненно, нужно учитывать.
Перегруппировку в результате преобразования
(a+b)*(c+d)
в
ab+cd+*
можно осуществить с помощью стека. Алгоритм такого преобразо-
вания хорошо известен. Это преобразование можно выполнить
также на основании грамматики инфиксных выражений. В данном
случае оно сведется к трем действиям:
1) напечатать идентификатор, когда он встретится при
чтении инфиксного выражения слева направо;
2) поместить в стек знак операции, когда он встретится;
3) когда встретится конец выражения ( или подвыражения ),
выдать на печать тот знак операций, который находится в вер-
шине стека.
Этот метод подобен методу, который применяется для полу-
чения четверок. Префиксные и постфиксные выражения можно так-
же получить из представления выражения в виде бинарного дере-
ва. Чтобы получить представление префиксного выражения, дере-
во обходят сверху в порядке, определенном Кнутом:
посещение корня;
обход левого поддерева сверху;
обход правого поддерева сверху,
что дает
+*+abcd
Для получения постфиксного представления дерево обходят
снизу. По Кнуту это выглядит так:
обход левого поддерева снизу;
обход правого поддерева снизу;
посещение корня.
В результате имеем: ab+c*d+
Далее будем рассуждать в терминах промежуточного языка (
или обьектного ), состоящего из команд вида
тип-команды параметры
Тип-команды может быть, например, вызовом стандартного
обозначения операции, тогда параметрами могут быть имя знака
операции, адреса опреандов и адрес результата. Например,
STANDOP II+,A,B,C
Здесь II+ обозначает сложение двух целых чисел, а A, B,
C cлужат во время прогона адресами двух операндов и результа-
та. Для того чтобы в промежуточном коде можно было воспользо-
ваться адресами во время прогона, распределение памяти к это-
му времени должно быть уже закончено. При распределении памя-
ти необходимо знать, какой обьем памяти занимает целое, ве-
щественное и другие значения на той машине, для которой выда-
ется обьектный код. Это означает, что промежуточный код не
является в строгом смысле интерфецсом между не зависящей и
зависящей от машины частями компилятора. Тем не менее если
речь идет о переводе фронтальной части компилятора ( т.е.
части, транслирующей исходный код в промежуточный ) с одной
машины на другую, то единственное, что здесь может потребо-
ваться, - это изменение нескольких констант.
Промежуточный код пишется на относительно низком уровне.
Он аналогичен коду, использованному для реализации Алгола 68.
Обычно выдвигается условие, чтобы промежуточный код отражал
структуру реализуемого языка.
Промежуточный код напоминает префиксную нотацию в том
смысле, что знак операции всегда предшествует своим операн-
дам. Но он имеет менее общий характер, так как сами операнды
не могут быть префиксными выражениями. При получении промежу-
точного кода для хранения адресов операндов до тех пор, пока
не будет напечатан знак операции, используется стек. Посколь-
ку знак операции можно установит ( во многих языках ) лишь
после того, как станут известны его опреанды, стек служит
также для хранения каждого знака операции на то время, пока
не определены оба операнда.
Адрес на время прогона обычно соотносится со стеком, и
каждый такой адрес можно представить тройкой вида
( тип-адреса, номер блока, смещение ).
Тип-адреса может быть прямым или косвенным ( т.е. адрес
может содержать значение или указатель на значение ) и ссы-
латься на рабочий стек или стек идентификаторов. Он может
быть также литералом или константой. Номер блока позволяет
найти номер уровня блока в таблице блоков, что обеспечивает
доступ к конкретной рамке стека через диспдей. В сдучае лите-
рала или константы номер блока не используется. Смещение (
для адреса стека ) показывает смещение значения конкретной
рамки по отношению к началу стека идентификаторов или рабоче-
го стека. Если тип-адрес представляет собой литерал, то сме-
щение выражается самим значением, а если тип-адреса - конс-
танта, то смещение нужно найти в таблице констант по заданно-
му им адресу. В том случае, когда в каждой рамке стека рабо-
чий стек помещается сразу же над стеком идентификаторов, сме-
щения адресов рабочего стека по отношению к началу рамки мож-
но рассчитывать, как только станет известным размер стека
идентификаторов для конкретной рамки ( т.е. во время прохода,
следующего за проходом, при котором происходит распределение
памяти ).
Адреса во время прогона для идентификаторов определяются
в процессе распределения памяти и хранятся в таблице символов
вместе с информацией о типе и т.п.
Кроме рассмотренных, существуют и другие команды проме-
жуточного кода ( ICI по Бранкару ):
SETLABEL L1
для установки метки и
ASSIGN type, add1, add2
для присваивания. Тип необходим как параметр, чтобы опреде-
лить размер значения, переписываемого из add1 в add2. В Алго-
ле 68 может потребоваться просмотр типа ( вида ) при трансля-
ции этой команды в фактический код машины, если значения бу-
дут содержать динамические части, поэтому во время генерации
машинного кода нужна таблица видов.
Структуры данных для генерации кода
Как упоминалось выше, для хранения адресов операндов на
то время, пока их нельзя будет выдать как параметры ICI, не-
обходим стек значений. В этом стеке, который Бранкар называет
нижним стеком, можно хранить также и другую информацию. Нап-
ример, значение может быть связано со своими
а) адресом времени прогона;
б) типом;
в) областью действия,
помимо той информации, которая имеет значение для диагности-
ки. Это - статическая информация, так как ( по крайней мере,
для большинства языков ) ее можно получить во время компиля-
ции. Так, при компиляции может быть известно если не факти-
ческое значение, то во всяком случае адрес целого числа.
При трансляции А + В первыми помещаются в нижний стек
статические свойства А. Любой элемент нижнего стека можно
представить в виде структуры, имеющей поле для каждой из сво-
их статических характеристик. В случае идентификаторов стати-
ческие характеристики находятся из таблицы символов. Затем в
стек знаков операции помещается знак операции +, и в нижний
стек добавляются статические характеристики В. Знак операции
берется из стека знаков операций, а его два операнда - из
нижнего стека. Типыоперандов используются для идентификации
знака операции, после чего генерируется код. И наконец, в
нижний стек помещаются статические характеристики результата.
Этот процесс можно распространить и на более сложные вы-
ражения, например нп те, которые генерируются грамматикой с
правилами
EXP -> TERM |
EXP + TERM |
EXP - TERM
TERM -> FACT |
TERM * FACT |
TERM / FACT
FACT -> constant |
identifier |
(EXP)
После чтения идентификатора или константы, знака опера-
ции и второго операнда необходимо выполнить следующие дейс-
твия:
А1. Послечтения идентификатора или константы ( т.е. лис-
та синтаксического дерева ) поместить в нижний стек
соответствующие статические характеристики.
А2. После чтения оператора поместить символ операции в
стек знаков операций.
А3. После чтения правого операнда ( который может быть
выражением ) извлечь из стеков знак операции и его два опе-
ранда, генерировать соответствующий код, так как знак опера-
ции идентифицирован, и поместить в стек статические характе-
ристики результата. Тип результата становится известным во
время идентификации знака операции, например сложение двух
целых чисел всегда дает целое число.
При включении в грамматику этих действий она примет сле-
дующий вид:
EXP -> TERM
EXP+<A2>TERM<A3>
EXP-<A2>TERM<A3>
TERM -> FACT
TERM*<A2>FACT<A3>
TERM/<A2>FACT<A3>
FACT -> constant<A1>
identifier<A1>
(EXP)
Нижний стек частично используется для передачи информа-
ции о типе вверх по синтаксическому дереву. Рассмотрим син-
таксическое дерево, соответствующее выражению:
+
^
/ \
/ \
/ \
* / \ *
/\ /\
/ \ / \
/ \ / \
a b x y
a * b + x * y
Если значения a и b имеют тип целого, а х и у - тип ве-
щественного значения, компилятор может заключить,
воспользовавшись информацией нижнего стека, что "+" в вершине
дерева представляет сложение целого и вещественного значений.
Мы можем переписать выражение, расставив действия А1, А2 и А3
в том порядке, в каком они будут вызываться при трансляции
этого выражения:
а<A1>*<A2>b<A1><A3>+<A2>x<A1>*<A2>y<A1><A3><A3>
Действие А3 соответствует применению знака операции. Из
изложенного выше вытекает, что каждый вызов А3 соответствует
тому месту, где появился бы знак операции в постфиксной фор-
ме. Стек знаков опреаций, по существу, служит для формирова-
ния постфиксной нотации. Поэтому последовательность действий
при трансляции данного выражения должна быть следующей:
А1. Поместить статические характеристики а в нижний
стек.
А2. Поместить знак "*" в стек знаков операций.
А1. Поместить статические характеристики b в нижний
стек.
А3. Извлечь статические характеристики a и b из ниж-
него стека и знак "*" из стека знаков операций, генерировать
код для умножения двух целых чисел, поместить статические ха-
рактеристики результата в нижний стек; тип результата - целый.
А2. Поместить знак "+" в стек знаков операций.
А1. Поместить статические характеристики х в нижний
стек.
А2. Поместить знак "*" в стек знаков операций.
А1. Поместить статические характеристики у в нижний
стек.
А3. Извлечь статические характеристики х и у из ниж-
него стека и знак "*" из стека знаков операций, генерировать
код для умножения двух целых чисел, поместить статические ха-
рактеристики результата в нижний стек; тип результата - ве-
щественный.
А3. Извлечь два верхних элемента из нижнего стека и знак
"+" из стека знаков операций, генерировать код для сложения
целого и вещественного значений, поместить статические харак-
теристики результата в нижний стек; тип результата - вещест-
венный.
Действия А1, А2, А3 и вышеприведенную грамматику легко
расширить, что позволит использовать
а) большее число уровней приоритета для знаков операций;
б) унарные знаки операций.
Другие случаи употребления нижнего стека рассматриваются
в следующем разделе.
Нижний стек обеспечивает передачу информации вверх по
синтаксическому дереву. Для передачи же информации вниз по
дереву применяется так называемый верхний стек. Значение в
него помещается всякий раз, когда во время генерации кода
происходит вход в такую конструкцию, как присваивание или
описание идентификатора. При выходе из этой конструкции зна-
чение из стека удаляется. Следовательно, генератор кода может
заключить, например, что компилируемое выражение находится
справа от знака присваивания; эта информация способствует оп-
тимизации.
Еще одной структурой данных, которая требуется во время
генерации кода, является таблица блоков.
╔══════════╦═══════════╦═══════════════════╦═════════════════╗
║ Блок ║ Уровень ║ Размер стека ║ Размер рабочего ║
║ ║ блока ║ идентификаторов ║ ║
╠══════════╬═══════════╬═══════════════════╬═════════════════╣
║ 1 ║ 1 ║ 14 ║ 16 ║
║ 2 ║ 2 ║ 12 ║ 11 ║
║ 3 ║ 2 ║ 21 ║ 13 ║
║ 4 ║ 3 ║ 4 ║ 9 ║
║ 5 ║ 2 ║ 6 ║ 12 ║
╚══════════╩═══════════╩═══════════════════╩═════════════════╝
В этой таблице есть запись для каждого блока программы,
и эту запись можно рассматривать как структуру, имеющуюю по-
ля, которые соответствуют номеру уровня блока, размеру стати-
ческого стека идентификаторов, размеру статического рабочего
стека и т.д. Такую таблицу можно заполнять во время прохода,
генерирующего код, и с ее помощью в следующем проходе вычис-
лять смещения адресов рабочего стека по отношению к текущей
рамке стека.
Таким образом, во время генерации кода используются сле-
дующие основные структуры данных: нижний стек, верхний стек,
стек знаков операций, таблица блоков и, кроме того, таблица
видов и таблица символов из предыдущих проходов.
Генерация команд
По существу, на этом этапе происходит перевод внутреннего
представления исходной программы на автокод или на машинный
язык.
Возможны три формы об'ектного кода: абсолютные команды, по-
мещенные в фиксированные ячейки; программа на автокоде; програма
на языке машины, предтавленная образами карт и записанная во
вторичную память.
Рассмотрим выражение (A + B) + (X + Y)
Очевидный способ его вычисления в терминах машинного языка
таков:
1. загрузить А в сумматор;
2. прибавить B к сумматору;
3. записать результат A+B во временную рабочую ячейку;
4. загрузить X в сумматор; 5. прибавить Y к сумматору;
6. прибавить временный результат A+B к X+Y в сумматоре.
Каждому из трех сложений предшествует своя последователь-
ность команд загрузки и записи.
Чтобы построить код генератор хранит некоторую информацию о
том, что будет происходить в период исполнения генерируемого им
кода.
При разработке генератора кода первый шаг заключается в
том, что чтобы определить, как будет организована память машины
в перид исполнения скомпилированной прграммы. Предлагаемое расп-
ределение памяти показано на рисунке
----------------------
! Программа !
----------------------
! Константы !
----------------------
! Подпрограммный !
! стек !
----------------------
! Промежуточные !
! результаты !
----------------------
! Хранимые результаты!
----------------------
! Переменные !
----------------------
Область ПРОГРАММА содержит команды об'ектной программы.
ПОДПРОГРАММНЫЙ СТЕК используется для хранения адресов возврата
подпрограмм. Область ХРАНИМЫЕ РЕЗУЛЬТАТЫ используется для хране-
ния результатов атома ХРАНЕНИЕ в FOR-операторах. В областях
КОНСТАНТЫ и ПЕРЕМЕННЫЕ хранятся соответственно значения констант
и переменных.
Область ВРЕМЕННЫЕ РЕЗУЛЬТАТЫ используется для хранения про-
межуточных результатов.
Генератор кода использует табличные эдементы для хранения
информации о параметрах атомов. Каждый табдичный элемент имеет
поле ВИД, указывающее вид об'екта, описываемого этим элементом,
а также одно или два других поля. Поле ВИД содержит одно из сле-
дующих пяти значений: КОНСТАНТА, ПЕРЕМЕННАЯ, ПРОМЕЖУТОЧНЫЙ РЕ-
ЗУЛЬТАТ, ХРАНИМЫЙ РЕЗУЛЬТАТ или МЕТКА.
Мы предполагаем, что генератор кода содержит процедуру, на-
зываемую ГЕН, которая строит двоичное представление генерируемой
команды. ГЕН вызывается с двумя параметрами: кодом операции и
указателем на табличный элемент, соответствующий полю адреса ге-
нерируемой команды. Процедура ГЕН выполняет следующие действия:
1. Формируется двоичный код, соответствующий первому
параметру процедуры ГЕН.
2. Формируется двоичный код, соответствующий второму
параметру процедуры ГЕН.
3. Двоичный код, образующий сгенерированную команду,
помещается в ячейку, соответствующую текущему зна-
чению СЧЕТКОМ.
4. СЧЕТКОМ увеличивается и сравнивается с ГРАНКОМ.
Здесь СЧЕТКОМ и ГРАНКОМ - переменные периода компиляции.
Генерация кода для некоторых типичных конструктов
Покажем, как генерируеися код для некоторых конструктов,
типичных для языков программирования высокого уровня.
I. Присваивания
В соответствии с терминологией Алгола 68 присвамвание
имеет вид
destination := source
Смысл его состоит в том, что значение, соответствующее
источнику, присваивается значению, которое является адресом
( или именем ), заданным получателем. Например, в
p := x + y
значение "х+у" присваивается р.
Допустим, что статические характеристики источника и по-
лучателя уже находятся в вершине нижнего стека. Опишем дейс-
твия, выполняемые во время компиляции для осуществления прис-
ваивания. Прежде всего из нижнего стека удаляются два верхних
элемента, после чего происходит следующее:
1. Проверяется непртиворечивость типов получателя и ис-
точника. Так как получатель представляет собой адрес, источ-
ник должен давать что-нибудь приемлемое для присваивания это-
му адресу. В зависимости от реализуемого языка типы
получателя и источника можно определенным образом изменять до
выполнения присваивания. Например, если тип источника - целое
число, то его можно сначала преобразовать в вещественное, а
затем присвоить адрес, имеющему тип вещественного числа.
2. Там, где это необходимо, проверяются правила области
действия. В Алголе 68 источник не может иметь меньшую область
действия, чем получатель. Например, в
begin ref real xx;
begin real x;
xx := x
end
присваивание недопустимо, и это может быть обнаружено во вре-
мя компиляции, если в таблице символов или в нижнем стеке
имеется информация об области действия. Однако в процессе
компиляции нельзя обнаружить все нарушения правил области
действия, и в некоторых случаях для проверки этой области
приходится создавть код во время прогона.
3. Генерируется код для присваивания, имеющий форму
ASSIGN type,S,D
где S - адрес источника, а D - адрес получателя.
4. Если язык ориентирован на выражения ( то есть само
присвоение имеет значение ), статические характеристики этого
значения помещаются в нижний стек.
II. Условные зависимости
Почти все языки содержат условное выражение или опера-
тор, аналогичный следующему:
if B then C else D fi
При генерации кода для такой условной зависимости во
время компиляции выполняются три действия. Грамматика с вклю-
ченными действиями:
CONDITIONAL -> if B <A1> then C <A2> else D <A3> fi
Действия А1, А2 и А3 означают (next - значение номера
следующей метки, присваиваемое компилятором):
А1. Проверить тип В, применяя любые необходимые преобра-
зования (приведения) типа для получения логического значения.
Выдать код для перехода к L<next>, если В есть "ложь":
JUMPF L<next>,<address of B>
Поместить в стек значение next ( обычно для этого служит
стек знаков операций ). Увеличить next на 1. (Угловые скобки
(<,>), в которые заключаются "next" и "address of B", исполь-
зуются для обозначения значений этих величин, и их не следует
путать со скобками, в которые заключаются действия в порожда-
ющих правилах грамматики.)
А2. Генерировать код для перехода через ветвь else (
т.е. перехода к концу условной зависимости )
GOTO L<next>
Удалить из стека номер метки ( помещенный в стек дейс-
твием А1 ), назвать i, генерировать код для размещения метки
SETLEBEL L<i>
Поместить в стек значение next. Увеличить next на 1.
А3. Удалить из стека номер метки (j). Генерировать код
для размещения метки
SETLABEL L<j>
Если условная зависимость сама является выражением ( а
не оператором ), компилятор должен знать, где хранить его
значение, независимо от того, какая часть вычисляется - then
или else. Это можно сделать, специфицируя адрес, который ука-
зывает на данное значение, или пересылая значение, заданное
частью then либо частью else, по указанному адресу.
Аналогично можно обращаться с вложенными условными выра-
жениями или операторами.
III. Описания идентификаторов
Допустим, что типы всех идентификаторов полностью выяс-
нены в предыдущем проходе и помещены в таблицу символов. Ад-
реса распределяются во время прохода, гененрирующего код.
Рассмотрим описание
somemode x
еречислим действия, выполняемые во время компиляции:
1. В таблице символов производится поиск записи, соот-
ветствующей x.
2. Текущее значение указателя стека идентификаторов дает
адрес, который нужно выделить для x. Этот адрес
(idstack, current block number, idstack pointer)
включается в таблицу символов, а указатель стека идентифика-
торов увеличивается на статический размер значения, соответс-
твующего х. (В Алголе 68, если вид х начинается с ref, обьем
памяти должен выделяться для значения, на которое ссылается
х, а не для самого х; это обозначается с помощью адреса дру-
гого типа. )
3. Если х имеет динамическую часть, например в случае
массива, то генерируется код для размещения динамической па-
мяти во время прогона.
IV. Вход и выход из блока
При входе в блок ( последовательное предложение с описа-
ниями в Алгол 68 ) предположим, что во время предыдущего про-
хода получена определенная информация. Она состоит из таблиц
видов и символов, дающих типы или виды всех идентификаторов и
т.п. Тогда при входе в блок нужно выполнить следующие основ-
ные действия:
1. Прочитать в таблице символов информацию, касающуюся
блока, и связать ее с информацией включающих блоков таким об-
разом, чтобы можно было выполнить "внешние" поиски определяю-
щих реализаций идентификаторов и т.д.
2. Поместить в стек (idstack pointer). Поместить в стек
(wostack pointer). Поместить в стек (block number). Все эти
значения ссылаются на включающий блок и могут потребоваться
вновь после того, как будет покинуть блок, в который только
что осуществлен вход:
idstack pointer := 0
wostack pointer := 0
3. Генерировать код для исправления DISPLAY
BLOCK ENTRY block number
4. Увеличить номер уровня блока на 1. Увеличить наиболь-
ший использованный до сих пор номер блока на 1 и присвоить
это значение номеру блока.
5. Прочитать информацию о виде и добавить ее в таблицу
видов ( если в языке имеются такие "сложные" виды, как в Ал-
голе 68 ).
При выходе из блока требуется:
1. Обновить таблицу блоков, задав размер стека идентифи-
каторов и т.п. для покинутого блока.
2. Исключить информацию в виде таблицы символов для по-
кинутого блока.
3. Генерировать код для изменения дисплея
BLOCK EXIT block number
4. Удалить из стека (block number). Удалить из стека
(wostack pointer). Удалить из стека (idstack pointer). Умень-
шить номер уровня на 1.
5. Поместить результат (при необходимости) в рамку стека
вызывающего блока.
ЛЕКЦИЯ 16
КОНТЕКСТНЫЕ УСЛОВИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ
КОНТЕКСТНЫЕ УСЛОВИЯ 1-ОГО И 2-ОГО ТИПА
1. Условия, связанные с описанием правил идентификаторов
В каждом блоке без внутренних блоков идентификатор нельзя
описывать более одного раза.
Для процедур и функций ни один идентификатор не должен вхо-
дить более одного раза в список формальных параметров в совокуп-
ности спецификаций.
2. Правила соответствия между определяющими и использующими
вхождениями идентификаторов.
.
Правила поиска часто называют алгоритмами идентификации.
Проверим одно контекстное условие:
1. Рассмотрим min блок.
2. Ищем определяющее вхождение идентификатора в рассмотрен-
ном блоке. Если оно найдено, то процедура идентификации законче-
на.
Иначе - шаг 3.
3. Ищем min блок, который мы рассмотрели в шаге 2. Если та-
кой блок найден, рассмотрим его и переходим к шагу 2. Иначе,
процедура закончена.
Это мы проверяем одно контекстное условие.
Однако, задача идентификации сложнее, т.к. обычно рассмат-
ривается группа контекстных условий.
1. Каждый идентификатор, входящий в совокупность специфика-
ций, должен также входить в список формальных параметров.
2. Каждый идентификатор, входящий в список значений, должен
входить в совокупность спецификаций.
3. Идентификаторы, входящие в тело процедуры, могут быть
описаны в блоке, вне этого блока или могут быть включены в
список формальных параметров.
Список спецификаций - заголовок (имя) функции, описание ти-
па функции.
Список значений - те параметры, кот. меняются при изменении
функции, т.е. результат.
КОНТЕКСТНЫЕ УСЛОВИЯ ТРЕТЬЕГО ТИПА
Они регламентируют:
1) Соответствие видов величин, входящих в синтаксические
конструкции программ.
2) Задание допустимых соотношений между формальными парамет-
рами в процессах и формальными описаниями соответствующих проце-
дур.
3) Сравнение индексов переменных в использующем и определяю-
щем вхождениях идентификаторов.
4) Сравнение размерности массива в используемом и определяю-
щем вхождении идентификатора.
КОНТЕКСТНЫЕ УСЛОВИЯ ЧЕТВЕРТОГО ТИПА
Некоторые логические ограничения, которые относятся к реа-
лизации той или иной версии транслятора.
Массив может быть с неограниченным размером.
ЛЕКЦИЯ 17
ПРОГРАММНЫЕ ГРАММАТИКИ
Правила вывода этих грамматик имеют тот же вид, что и у
классических, однако в отличии от последних на каждом шаге выво-
да правила, кот. могут быть применены, зависит не только от це-
почек вывода, но и от того, какие правила применялись на преды-
дущих шагах.
Gp = { Vт, Vn, P, I, S }
G - конечное множество целых положительных чисел
(множество меток)
** **
r) Ф -> psi │ W1 │ W2 │, W1, W2 - некоторое подмножество меток
│ * * *
метка,соотв. Ф,psi ( Vт U Vn )
выводу
* - соответствующий класс грамматическим правилам
** - обозначается некоторое подмножество
В терм. порожденной грамматике сформируем использование
данного грамматического файла.
w - промежуточная цепочка.
Если w содержит вхождения по цепочке Ф, то левое вхождение
Ф в цепочке заменяем на psi.
После чего к полученной цепочке вывода применяется правило,
помеченное метками из множества W1.
Если w не содержит вхождения строки Ф, то строка w не моди-
фицируется, и к ней применяется некоторое правило, полученное
меткой из множества W2.
Язык, порожденный этой грамматикой, содержит только терми-
нальные символы.
В зависимости от ограничений на классическую часть различа-
ют:
1) контекстно-свободные,
2) контекстно-зависимые,
3) укорачивающие грамматики.
Имеют место теории, которые доказывают, что программные кон-
текстно-свободной грамматики более мощные, чем просто контек-
стные-свободные грамматики, в то время как зависимые контек-
стно-свободные грамматики совпадают с программно контекстно-сво-
бодными грамматиками.
Когда применяются программные грамматики процесс трансляций
выполняется в 2 этапа:
1) Порождается промежуточная форма программы.
Имеет место некоторый промежуточный язык, в котором некото-
рые синтаксические конструкции отсутствуют (например, внутр.
блоки), а те, которые представлены - состоят из терминальных
символов-двойников, соответствующим терминальным символам прог-
раммы.
2) Промежуточная форма программы проверяется на предмет
соотношения контекстных условий и выполняется замена симво-
лов-двойников на терминальные символы программы.
Все терминальные символы в т.ч. и двойники должны использо-
ваться в различных модификациях.
После первого этапа выполняется проверка контекстных усло-
вий (2 этап).
Каждая проверка заключается в следующем:
- сначала выделяются конструкции, участвующие в проверке;
- выделение производится путем модификации символов, из ко-
торых эта конструкция состоит (присвоение индекса);
- далее производится сравнение выделенных конструкций;
способ зависит от конкретного контекстного условия (сравнение
может производиться на сравнение и не сравнение, т.д.)
Если результаты положительные, то производится вывод неко-
торых построенных конструкций.
На выходе все двойники заменяются на терминальные символы
(соответствующие).
КОНТЕКСТНЫЕ УСЛОВИЯ ДЛЯ ПРОГРАММНЫХ ГРАММАТИК
1. В каждом блоке без внутренних блоков каждый идентифика-
тор должен быть описан не более одного раза (это условие приме-
няется и для меток).
2. По каждому использующему вхождению идентификатора или
целого, представляющего метку, в данном или объемлющем блоках
должно быть описание этого идентификатора.
3. Все переменные в левых частях при присваивании должны
быть одного типа.
4. Количество индексов у переменных с индексами должно сов-
падать с числом пар граничащих массивов.
5. Количество и типы фактических параметров в операторах
процедур должны совпадать с количеством и типом форм. парамет-
ров, приведенных в описаниях процедур.
6. Фактический параметр, который соответствует формальному
параметру, вызываемому по наименованию и встреч. в левой части,
обязан быть переменной (а не выражением).
7. Идентификаторы,входящие в выражение для границ в описа-
ниях массивов, должны быть описаны в одном из объемлющих блоков.
ЛЕКЦИЯ 18
ГРАММАТИКИ ВАН ВЕЙНГААРДЕНА
ОПРЕДЕЛЕНИЕ: Грамматика ван Вейгаардена - это две связанные
грамматики, которые называются метаграмматикой или грамматикой
метаязыка и строгой грамматикой ( грамматикой ) языка:
Gv = < Gm, Gst >.
Gm = < Vs, Vm, Pm, M >
Vs - конечное множество малых синтаксических знаков ( в
русском издании "Пересмотренного сообщения об Алголе" множество
маленьких букв русского алфавита ).
Vl - конечное множество больших синтаксических знаков ( в
русском издании "Пересмотренного сообщения об Алголе" множество
больших букв русского алфавита ).
Vm - конечное множество метапонятий:
Vm с Vl+;
Vh - конечное множество гиперпонятий:
Vh с ( Vm U Vs )*;
Ah - конечное множество гиперальтернатив:
Ah с Vh ( {,} Vh )*
( гиперпонятия в гиперальтернативах отделяются запятыми ).
Ph - конечное множество гиперправил:
Ph с Vh {:} Ah ( {;} Ah )* {.}
( гиперальтернативы в гиперправилах отделяются точкой с
запятой, в конце гиперправила ставится точка ).
Правила в метаграмматике ( гиперправила ) также называются
формами или схемами.
Pm - конечное множество правил метапорождений:
Pm с Vm {::} Vh ( {;} Vh )* {.}
M - начальный символ грамматики ван Вейгаардена.
M с Vm
L ( Gm ) - ( в общем случае бесконечное ) множество терми-
нальных метапорождений метапонятия M: L ( Gm ) с Vs*
Пусть метапонятие MM имеет одно или более вхождений в ги-
перправило h. Согласованной заменой метапонятия MM в гиперправи-
ле h назовем замену всех его вхождений одним и тем же терминаль-
ным порождением Tm с L ( Gm ).
Правило порождения получается из гиперправила путем согласо-
ванной замены всех входящих в него метапонятий.
Понятием называется непустое ( протопонятие ), если для него
может быть получено правило порождения. Множество понятий как
раз является бесконечным множеством нетерминалов Vn грамматики.
Gst = < Vt, Vn, P, S >
Vt - конечное множество терминалов;
Vn - множество нетерминалов;
P - множество правил;
S - начальный символ грамматики
Множество правил порождения P определяется тем, что
P с Vn {:} A ( {;} A )* {.},
где Vn с Vs+ - множество понятий,
A с F ( {,} F )* - множество альтернатив,
F = 0 U Vt u Vn U B
0 - пустое множество;
B - множество тупиковых протопонятий.
Для тупикового протопонятия никакое правило порождения не
может быть выведено. Возможности тупиков используются в системе
предикатов для учета контекстных условий.
Предикат - это протопонятие имеющее одну из форм: where а (
если верно что а ) или unless а ( если неверно что а ).
Предикат выполняется ( используется правило под ним для по-
рождения ), если для него выводимо правило порождения, и в этом
случае его терминальное порождение всегда пусто, либо такой вы-
вод заводит в тупик, и в этом случае предикат не выполняется.
Таким образом, с правилом связывается контекст его примене-
ния.
Рассмотрим грамматику ван-Вейнгардена, описывающую синтаксис
оператора присваивания PASCAL-подобного языка и контролирующую
следующие контекстные условия (3-го рода по классификации Брат-
чикова)
1. Арифметическое выражение может состоять из выражений при-
надлежащих лишь арифметическим типам
2. Логическое выражение может состоять из выражений лишь ло-
гических типов
3. Не допускается смешивать в арифметических выражения раз-
личные типы
4. Переменной можно присваивать значение того же либо струк-
турно эквивалентного типа
Грамматика 1-го уровня
Нетерминальные символы метаграмматики
TYPE тип
ATYPE арифметический тип
PTYPE указательный тип ( указатель на нечто)
Правила метапорождения
TYPE ::= ATYPE | PTYPE | bool
PTYPE ::= pointer TYPE
ATYPE ::= int | float
Грамматика 2-го уровня
Реализация контексных условий основана на том что имена
терминальных и нетерминальных символов порождаемой граматики не-
сут в себе информацию о типе соответствующих объектов
Нетерминальные символы порождаемой грамматики
ass оператор присваивания
le TYPE левая часть оператора присваивания (выражение типа TYPE)
e TYPE правая часть оператора присваивания (lvalue типа TYPE)
e<n> TYPE выражения различных уровней приоритета
t TYPE терм (константа, переменная или выражение в скобках)
mulop операция *,/
addop +,-
compop <,>,>=,<=
boolop AND,OR
ass := le TYPE, ':=', e TYPE
le TYPE := id TYPE ; '^',le pointer TYPE
e1 ATYPE := e1 ATYPE, mulop, t ATYPE;t ATYPE
e2 ATYPE := e2 ATYPE, addop, e1 ATYPE; e1 ATYPE
t ATYPE := symbol id ATYPE; symbol const ATYPE; '(', e2 ATYPE, ')'
e3 bool := e3 ATYPE,compop ,e2 ATYPE ; symbol id bool ;symbol const bool
e4 bool := e4 bool ,boolop, e3; e3
e TYPE := e2 TYPE ; e4 TYPE
mulop :='*';'/'
addop :='+';'-'
compop :='<' ;'>';'<=';'>=';'='
boolop := 'AND' ;'OR'
Задача построения (конечной) контекстно-свободной грамма-
тики по грамматике ван-Вейнгардена решается путем разбиения бес-
конечных множеств терминальных и нетерминальных символов исход-
ной грамматики на конечное число классов, каждый из которых бу-
дет соответствовать терминалу либо нетеминалу строящейся грамма-
тики. Каждому классу соответствует цепочка символов метаграмма-
тики, из которой он выводится.
Нетерминалы КС-грамматики
ASS соответствует ass
LE ->le TYPE
E -> e
EN -> enTYPE
T -> t TYPE
MULOP ->mulop
ADDOP ->addop
COMPOP ->compop
BOOLOP ->boolop
Выполнив указанные выше замены в продукциях 2-го уровня (и
отбросив продукции грамматики 1-го уровня), получим
ASS := LE ':='E TYPE
LE := id | ^ LE
E1 := E1 MULOP T | T
E2 := E2 ADDOP E1 | E1
T :=id | const | (E2)
E3 :=E3 COMPOP E2 | id | const
E4 :=E4 BOOLOP E3 | E3
E := E2 | E4
MULOP := '*' | '/'
ADDOP :='+' |'-'
COMPOP :='<' ;'>';'<=';'>=';'='
BOOLOP := 'AND' ;'OR'
В заключение хотелось бы отметить различия в синтаксисе за-
писи правил КСграмматики и грамматики ван Вейнгардена. В первой
разделителями символов и метасимволов являются пробелы, раздели-
телями альтернатив являются знаки '|'. Терминальные символы за-
писываются малыми латинскими буквами, нетерминальные симво-
лы-большими. При записи грамматик ван Вейнгардена разделителями
символов являются запятые, разделителями альтернатив являются
точки с запятыми. Терминальные символы порождаемой грамматики
представляются цепочками терминалов метаграмматики, начинающими-
ся с терминала symbol.
ЛЕКЦИЯ 19
ПРИМЕР ГРАММАТИКИ ВАН ВЕЙНГААРДЕНА
Грамматикой ван Вейнгаардена описываются конструкции присваи-
вания вида ( например , преобразование типов в языке СИ) :
Пусть int i,j;
char c,ch;
т.е. описываем переменные i и j как целые,а c и ch как символь-
ные.Необходимо отметить , что для построения правильной конс-
трукции, выражающей присвоение переменной одного вида переменной
другого вида , в языке СИ необходимо в правой части перед соот-
ветствующим идентификатором нужно указать тип к которому должна
быть приведена переменная из правой части. Разумеется, этот тип
- тип переменной в левой части выражения . Если же имеет место
присвоение типа (2) ,т.е. типы переменных правой и левой частей
совпадают,то тип в правой части не указывается.
(1) c= (char) i;
(2) ch=c;
(3) ch= (char) j+c;
(4) i=(int) ch;
(5) c= (char) 20;
Для данных выражений типы правых частей не везде совпадают с
типами соответствующих им левых частей. Необходимо произвести
преобразование типа левой части к типу идентификатора в правой
части.
assign : е TYPE l,'=', e TYPE mod. /*Задание
конструкций присваивания*/
e TYPE l : id./*В левой части конструкции может быть
только идентификатор*/
/*Правая часть конструции может быть предс-
тавлена: */
e TYPE mod: e TYPE r ; /* выражением типа правой
части-(1)*/
TYPE nomber; /*числовым типом*/
'(',TYPE,')',e TYPE1 r;/*конструкцией вида
(тип)выражение_типа_отличного_от_типа_ле-
вой_части -(4)*/
'(',TYPE,')',TYPE1 nomber. /*конструкцией
вида ( тип) номер,имеющий тип,отличный от
типа левой части конструкции -(5) */
e TYPE r : e TYPE l,operation, e TYPE1 l;/* выражение
в правой части может быть представлен
типом, аналогичным типу левой части (2),
операцией - (3),выражением типа ,отличного
от типа левой части. */
TYPE : int symbol;/* в данном примере могут ис-
пользоваться данные цело-
численного или символьного
сhar symbol. типов */
Где :
е- expression -выражение,
TYPE- тип,
l- left -левый,
r- right - правый,
mod- modern -новый(англ.),тогда
e TYPE l -выражение, тип которого может встретиться в
левой части выражения,
e TYPE r -обозначает выражение , тип которого может
встретиться в правой части ( простое ) ,
e TYRE mod-обозначает выражение , тип которго может
встретиться в правой части ( составное или
простое, т.е., может быть состоять только
из выражения типа правой части или из приве-
денного к нему, или из суммы приведенной к
нужному типу и типа левой части переменных.
РАСПРЕДЕЛЕНИЕ ПАМЯТИ ВО ВРЕМЯ ТРАНСЛЯЦИИ
1. ДИСПЛЕЙ
1.1 Взаимодействие Дисплея и стека
После выяснения структуры (и значения) программы необходимо
выделить место в памяти для значений переменных и т.п. и помес-
тить соответствующие адреса, где это необходимо, в таблицу симво-
лов. Фаза распределения памяти почти не зависит от языка и маши-
ны и практически одинакова для подавляющего большинства языков,
имеющих блочную структуру и реализуемых на многих типичных ЭВМ.
Распределение памяти, по существу, заключается в отображении зна-
чений, появляющихся в программе, на запоминающем устройстве маши-
ны. Если допустить, что мы реализуем типичный язык с блочной
структурой, а машина имеет линейное запоминающее устройство, то
наиболее подходящим устройством, на котором мы будем базировать
распределение памяти, представляет стек или память магазинного
типа.
Ниже мы рассмотрим классическую структуру стека времени про-
гона для локального распределения памяти и покажем, как можно
произвести глобальное распределение памяти в отдельной области,
называемой "кучей".
В каждом компиляторе предусмотрена схема распределения памя-
ти, которая до некоторой степени зависит от компилируемого языка.
В Фортране память, выделенная для значений идентификаторов, ни-
когда не освобождается, так что здесь подходящей структурой для
нее является одномерный массив. Если считается, что массив имеет
левую и правую стороны, память можно выделять слево направо. При
этом применяется указатель, показывающий первый свободный эле-
мент в массиве. Например, в результате описания
INTEGER A,B,X,Y
выделяется память, как показано на рис. 20.1.
┌───┬───┬───┬───┬───────────────
│ A │ B │ X │ Y │
└───┴───┴───┴───┴───────────────
^
│
Рис. 20.1 Схема выделения памяти в Фортране
Такая схема не учитывает тот факт, что рабочая память (па-
мять для промежуточных результатов) используется неоднократно и
весьма неэффективна (в смысле использования объема памяти) для
языка с блочной структурой.
В языке, имеющем блочную структуру, память обычно высвобож-
дается при выходе из блока, которому она выделена. В этом случае
оптимальным решением было бы разрешить указателю в вышеприведен-
ном примере отодвигаться обратно влево при высвобождении памяти.
Такой механизм распределения эквивалентен стеку времени прогона
или памяти магазинного типа, хотя принято показывать стек запол-
няющимся снизу вверх.
│ │ │ │ │ │ (1) begin real x,y
│ │ │ │ │ │ .
│ │ ├───┤ ├───┤ .
│ │ │ d │ │ q │ .
│ │ ├───┤ ├───┤ (2) begin int c,d
│ │ │ c │ │ p │ .
├───┤ ├───┤ ├───┤ .
│ │ │ │ │ │ end;
│ │ │ │ │ │ (3) begin int p,q
│ Y │ │ Y │ │ Y │
├───┤ ├───┤ ├───┤ .
│ │ │ │ │ │ .
│ │ │ │ │ │ end;
│ X │ │ X │ │ X │ .
└───┘ └───┘ └───┘ .
end.
(1) (2) (3)
Рис. 20.2 Схема распределения памяти под переменные в раз-
личные моменты в программе на языке с блочной структурой
На рис. 20.2 показаны "моментальные снимки" стека фрагмента
программы во время прогона.
Часть стека, соответствующую определенному блоку, называют
РАМКОЙ стека. Считается, что указатель стека показывает на его
первый свободный элемент.
Кроме указателя стека, требуется также указатель на дно те-
кущей рамки (УКАЗАТЕЛЬ РАМКИ). При входе в блок этот указатель
устанавливается равным текущему значению указателя стека. При вы-
ходе из блока сначала указатель стека устанавливается равным зна-
чению, соответствующему включающему блоку.
Указатель рамки включающего блока может храниться в нижней
части текущей рамки стека, образуя часть статической цепи или
(как мы будем считать) массива, который называется ДИСПЛЕЕМ.
Его можно использовать для хранения во время прогона указа-
телей на начало рамок стека, соответствующих всем текущим блокам
(рис. 20.3).
│ │
│ │
│ │
│ │ ├───┤
│ │ │ q │
│ │ ├───┤
│ │ │ p │
│ │ /───> ├───┤
│ │ / │ │
├──┤ / │ Y │
│ │──/ ├───┤
├──┤ │ │
│ │─────────> │ X │
└──┘ └───┘
Дисплей Стек
Рис. 20.3 Схема связи Дисплея и стека во время выполнения
без учета динамически выделяемой памяти
Это несколько упрощает перенастройку указателя рамки при вы-
ходе из блока.
Если бы вся память была статической, адреса времени прогона
могли бы распределяться во время компиляции, и значения элемен-
тов дисплея также были бы известны во время компиляции.
Рассмотрим следующий отрезок программы на языке Алгол-60:
...
begin int n; read(n);
[1:n] int numbers;
real p;
begin real x,y;
...
Место для numbers должно выделяться в первой рамке стека, а
для x и y - в рамке над ней. Но во время компиляции неизвестно,
где должна начинаться вторая рамка, так как не известен размер
чисел.
Одно из решений в этой ситуации - иметь два стека: один для
статической памяти, распределяемой в процессе компиляции, а дру-
гой - для динамической памяти, распределяемой в процессе прогона.
Однако этого обычно не делают, возможно, из-за тех проблем, кото-
рые возникают в связи с наличием более чем одного увеличивающего-
ся и уменьшающегося стека во время прогона.
Другое решение заключается в том, чтобы при компиляции выде-
лять статическую память в каждом блоке в начале каждой рамки, а
при прогоне - динамическую память над статической в каждой рамке.
Это значит, что когда происходит компиляция, мы все еще не знаем,
где начинаются рамки (кроме первой), но можем распределять стати-
ческие адреса относительно начала определенной рамки. При прого-
не точный размер рамок, соответствующих включающим блокам, извес-
тен, так что при входе в блок нужный элемент дисплея всегда мож-
но установить так, чтобы он указывал на начало новой рамки (рис.
20.4).
│ Рамка 2
│ │ │
│ │ │
│ │ /
├───────┤ \
│ y │ Динами- │
├───────┤ │
│ x │ ческая │
/───> ├───────┤ │
│ │ / │ Числа │ память > Рамка 1
│ │ / │ │ │
├─────┤ / ├───────┤ - - - - │
│ │/ │ p │ Стати- │
├─────┤ ├───────┤ ческая │
│ │──\ │ n │ память │
└─────┘ \────> └───────┘ /
Дисплей Стек
Рис. 20.4 Схема связи Дисплея и стека во время выполнения с
учетом динамически выделяемой памяти
На этом рисунке массив занимает только динамическую память.
Однако некоторая информация о массиве обычно известна во время
компиляции, например, его размерность (а следовательно, и число
границ - две на каждую размерность), и при выборке определенного
элемента массива она может потребоваться. Во многих языках сами
границы могут быть не известны при компиляции, но почти наверня-
ка мы знаем их число, и для значений этих границ можно выделить
статическую память. Тогда мы вправе считать, что массив состоит
из статической и динамической частей. Статическая часть массива
может размещаться в статической части рамки, а динамическая - в
динамической. Кроме информации о границах, в статической части
может храниться указатель на сами элементы массива. Модифицируем
рис. 20.4 в рис. 20.5 с учетом этих особенностей.
│ Рамка 2
│ │ │
│ │ │
│ │ /
├────────┤ \
│ y │ Динами- │
├────────┤ │
│ x │ ческая │
/───> ├────────┤ │
│ │ / │Элемен- │ часть > Рамка 1
│ │ / │ты чисел│ │
├─────┤ / ┌─> ├────────┤ - - - - │
│ │/ │ │ p │ Стати- │
│ │ │ ├────────┤ │
│ │ │ │ Стат. │ │
│ │ └───│ часть │ ческая │
│ │ │ чисел │ │
├─────┤ ├────────┤ │
│ │──\ │ n │ часть │
└─────┘ \────> └────────┘ /
Дисплей Стек
Рис. 20.5 Схема связи Дисплея и стека во время выполнения с
учетом представления массивов в виде статической и динамической
частей
1.2 Выделение памяти под рамку в процессе трансляции
Динамическая рабочая память должна распределяться во время
прогона, статическая же может распределяться во время компиляции.
Объем статической рабочей памяти, который должен выделяться каж-
дой рамке, определяться не рабочей памятью, требуемой в конце
каждого блока (обычно она является нулем), а МАКСИМАЛЬНОЙ рабо-
чей памятью, требуемой в любой точке внутри блока. Для статичес-
кой рабочей памяти эту величину можно установить в процессе ком-
пиляции, если в фазе распределения памяти мы ассоциируем с рабо-
чей стековой областью текущей рамки два указателя, причем один из
них указывает на текущую границу статической рабочей памяти, а
другой - на максимальный размер, до которого она выросла при ра-
боте с текущим блоком. Именно значение этого второго указателя
при выходе из блока и дает объем статического рабочего стека,
включаемого в соответствующую рамку.
2. ПРЕДСТАВЛЕНИЕ МАССИВОВ
2.1 Прямоугольные массивы
Простейшие массивы (одномерные) естественно представляются
векторами, т.к. они хорошо согласуются в том смысле, что для по-
лучения относительного адреса элемента массива в векторе надо вы-
честь нижнюю границу по измерению из индекса элемента.
Из многомерных массивов рассмотрим сначала прямоугольные. Их
также возможно представлять векторами. Для реализации выборки или
засылки в массив надо получить относительный адрес нужного эле-
мента в векторе по значениям индексов этого элемента.
Предполагая линейное расположение всех точек n-мерного прос-
транства (n - размерность), соответствующего массиву, мы можем
считать первые измерения старшими и тогда рядом располагаются
элементы, соседние по последнему измерению - так называемый
ПРЯМОЙ порядок, либо наоборот - ОБРАТНЫЙ порядок. Например, в
языке Алгамс поддерживается прямой порядок, в Фортране - обрат-
ный, а в АЛГОЛе-60 представление многомерных массивов никак не
фиксируется.
Когда в программе выбирается конкретный элемент массива, его
адрес внутри динамической части должен вычисляться в процессе
прогона. Рассмотрим массив:
[1:10,-5:5] int Table
Будем считать, что элементы записаны в лексикографическом
порядке индексов, т.е. элементы хранятся в следующем порядке:
Table[1,-5], Table[1,-4] ......... Table[1,5],
Table[2,-5], Table[2,-4] ......... Table[2,5],
.
.
.
Table[10,-5], ...................... Table[10,5]
Адрес конкретного элемента вычисляется как смещение по отно-
шению к базовому адресу (адресу первого элемента) массива:
ADDR(Table[I,J])=ADDR(Table[l1,l2])+(u2-l2+1)*(I-l1)+(J-l2)
Здесь l1 и u1 - нижняя и верхняя границы первой размерности
и т.д. и считается, что каждый элемент массива занимает единицу
объема памяти.
Для трехмерного массива соответствующая формула имеет вид:
ADDR(Table[I,J,K])=ADDR(Table[l1,l2,l3])+
(u2-l2+1)*(u3-l3+1)(I-l1)+
(u3-l3+1)*(J-l2)+K-l3
Выражение (ui-li+1) задает число различных значений, кото-
рые может принимать i-индекс.
Значение произведения (u2-l2+1)*(u3-l3+1) представляет со-
бой число различных пар значений, которые могут принимать второй
и третий индексы, а также расстояние между элементами массива,
отличающимися только на одну единицу в первом индексе.
Расстояние между элементами, отличающимися на 1 в i-индексе,
известно как i-й шаг. Так, в приведенном выше примере первый шаг
s1 состовляет (u2-l2+1)*(u3-l3+1). Второй шаг s2 равен (u3-l3+1).
Третий шаг s3 составляет 1.
Ясно, что вычисление адресов элементов массива в процессе
прогона может занимать много времени. Поэтому рекомендуется при
программировании по возможности избегать выборки из массивов,
особенно из многомерных. Тем не менее, шаги могут вычисляться
только один раз и храниться в статической части массива наряду с
границами. Такая статическая информация часто называется описате-
лем массива. В этой же части массива наряду с нижней и верхней
границами и шагом для каждой размерности может храниться указа-
тель на элементы массива. Нижняя и верхняя границы требуются для
проверки правильности нахождения индексов в пределах границ, а
шаги и нижние границы - при обращении к конкретным элементам мас-
сива.
2.2 Обращение к элементам массива с помощью векторов Айлифа
Айлиф разработал свой способ обращения к элементам массивов.
Этот способ требует для хранения массива больше памяти, но зато
обращение к элементу выполняется быстрее. Вместе с каждым масси-
вом хранится набор указателей. Так, например, массив, определен-
ный как
B[4:6,-2:1,0:1]
хранился бы в виде структуры, представленной ниже на рис.
20.6.
┌──────┐ ─ ─ ─ ─ i
С │ ──┼───────────────────────────> │ │ (0)
└──────┘ D ─ ─ ─ ─
│ │ (1)
─ ─ ─ ─
│ │ (2)
─ ─ ─ ─
│ │ (3)
┌───────┐
┌─────────────────────────────┼── │ 4
│ ├───────┤
│ ┌────────┼── │ 5
│ │ ├───────┤
│ │ │ │ 6
│ │ └────┼──┘
│ │ │
│ │ │ j
│ │ │
│ │ │
│ │ └──────┐
E │ F │ G │
┌─────┐ │ ┌─────┐ │ ┌─────┐ │
┌─────────┼─ │ │ ┌─────────┼─ │ │ ┌─────────┼─ │ │-2
│ ├─────┤ │ │ ├─────┤ │ │ ├─────┤ │
│ ┌─────┼─ │ │ │ ┌─────┼─ │ │ │ ┌─────┼─ │ │-1
│ │ ├─────┤ │ │ │ ├─────┤ │ │ │ ├─────┤ │
│ │ ┌─┼─ │<─┘ │ │ ┌─┼─ │<─┘ │ │ ┌─┼─ │<─┘ 0
│ │ │ ├─────┤ │ │ │ ├─────┤ │ │ │ ├─────┤
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 1
│ │ │ └───┼─┘ │ │ │ └───┼─┘ │ │ │ └───┼─┘
│ │ │ │ │ │ │ │ │ │ │ │
H┌─┬─┬─┬─┬─┬─┬───┬────┬─┬─┬─┬─┬─┬─┬───┬────┬─┬─┬─┬─┬─┬─┬────┬───┐
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
k└─┴─┴─┴─┴─┴─┴───┴────┴─┴─┴─┴─┴─┴─┴───┴────┴─┴─┴─┴─┴─┴─┴────┴───┘
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
Рис. 20.6 Схема обращения к элементам массива с помощью век-
торов Айлифа
Пусть запись вида (B+5) означает содержимое по адресу B+5.
Тогда к элементу B[i,j,k] можно обратиться следующим образом:
(((C)+i)+j)+k
При этом выбирается содержимое ячейки C и к нему прибавляет-
ся значение индекса i, затем по полученному в результате этого
адресу выбирается содержимое, которое дает указатель на следую-
щий более низкий уровень, и т.д.
Важно отметить, что при данном способе обращения к элементу
массива не требуется выполнения операций умножения. Вместе с тем
в рассмотренном примере, кроме вектора для хранения массива, тре-
буется еще один вектор длины 3 и три вектора длины 4, и таким об-
разом, вместо 24 элементов требуется 39. Этот метод оказывается
наиболее экономичным, когда диапазоны изменения индексов увеличи-
ваются от первого к последнему. Наименее всего этот метод эффек-
тивен, когда первый индекс имеет наибольший диапазон изменения.
В приведенном примере не производилось проверки корректнос-
ти индекса. Это может быть достигнуто путем хранения вместе с
каждым элементом вектора Айлифа граничной пары для соответствую-
щего индекса. Правда, в случае прямоугольного массива это могло
бы оказаться неэкономичным. У каждого из элементов векторов E,F и
G в рассматриваемом примере были бы одинаковые граничные пары.
Однако следует обратить внимание на то, что с каждым из этих эле-
ментов могли бы быть связаны и различные граничные пары, что да-
ло бы возможность, используя векторы Айлифа, обращаться к масси-
вам со значительно более сложной структурой.
Так, например, на рис. 20.7 показан набор векторов Айлифа,
позволяющих обращаться к трехмерному массиву, элементы которого
хранятся в следующем порядке:
B[4,-1,-1] B[5,1,2] B[6,0,0]
B[4,-1, 0] B[5,2,2] B[6,0,1]
B[4,-1, 1] B[5,2,3] B[6,0,2]
B[4,-1, 2] B[5,3,2] B[6,0,3]
B[4, 0, 0] B[5,3,3] B[6,0,4]
B[4, 0, 1] B[5,3,4] B[6,0,5]
B[4, 0, 2] B[5,3,5]
B[4, 0, 3]
B[4, 0, 4]
B[4, 0, 5]
B[4, 0, 6]
┌─────┬─┬─┐
│ │4│6│
└──┼──┴─┴─┘ ─ ─ ─ ─ ─
└────────────────────────────> │ │
─ ─ ─ ─ ─
│ │
─ ─ ─ ─ ─
│ │
─ ─ ─ ─ ─
│ │
┌────┬──┬──┐
┌──────────────────────────────┼── │-1│ 0│
│ ├────┼──┼──┤
│ ┌────────┼── │ 1│ 3│
│ │ ├────┼──┼──┤
│ │ │ │ 0│ 0│
│ │ └─┼──┴──┴──┘
│ │ │
│ │ │
│ ─ ─ ─ ─ ─ ─ │ ┌─────┘
│ │ │<──┘ │
│ ┌─────┬──┬──┐ │
│ ┌───┼─ │ 2│ 2│ │
┌─────┬──┬──┐ │ │ ├─────┼──┼──┤ ┌─────┬───┬──┐
┌─┼─ │-1│ 2│ │ │┌──┼─ │ 2│ 3│ ┌┼─ │ 0│ 5│
│ ├─────┼──┼──┤ │ ││ ├─────┼──┼──┤ │└─────┴───┴──┘
│ │ │ 0│ 6│<┘ ││ │ │ 2│ 5│ │
│ └───┼─┴──┴──┘ ││ └─┼───┴──┴──┘ │
│ │ ┌─┘│ │ │
│ │ │ ┌┘ ┌─┘ ┌────┘
│ │ │ │ │ │
│ │ │ │ │ │
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
Рис. 20.7 Пример векторов Айлифа для трехмерного массива
сложной структуры
2.3 Замечания по поводу "разреженных" массивов
В некоторых случаях, при хранении массивов по схеме векто-
ров Айлифа или подобной ей схеме с использованием дескрипторов
можно избежать хранения пустых, тривиальных или незаданных значе-
ний элементов. В этих случаях можно рассматривать пару "информа-
ционный элемент" - "значения индексов" как элемент информационно-
го множества, а задание значения (нетривиального) элементу масси-
ва - как пополнение множества, а задание неопределенного (или
тривиального) значения - как удаление элемента из множества.
Представление таких "неполных" (или "разреженных") массивов
реализуется как представление множеств.
3. ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ТРАНСЛЯЦИИ
3.1 Проблемы распределения памяти
Распределение памяти для определенных программистом перемен-
ных в языках высокого уровня обычно бывает простым. Часто ло-
кальные переменные размещаются в памяти непосредственно после
соответствующей программной единицы. Однако для современных ЭВМ и
ОС, требующих разделения неизменяемой и изменяемой частей прог-
раммы, возникает необходимость помещать локальные переменные в
отдельные области памяти.
Основные проблемы возникают тогда, когда нужно распределить
память для временных переменных, т.е. промежуточных результатов,
потребность в которых появляется при компиляции. Эти переменные
определяются не в программе на входном языке, а компилятором. Ре-
шение о том, сколько для них требуется памяти и как ее распреде-
лить, полностью зависит от компилятора. Поэтому нужен алгоритм
как можно более эффективного размещения в памяти этих переменных.
Здесь под эффективностью обычно понимается стремление минимизиро-
вать объем используемой памяти.
Распределение памяти становится нетривиальной задачей в том
случае, когда у ЭВМ существует более чем один уровень памяти.
У многих машин имеются ограниченные наборы быстрых регис-
тров, которые отличаются от основной памяти либо более быстрым
доступом, либо иными, более удобными свойствами. Эффективное ис-
пользование этих регистров является задачей компилятора, и нужны
алгоритмы оптимального размещения переменных в быстрых регистрах.
3.2 Распределение памяти для временных переменных
Алгоритмы генерации команд не касаются распределения памяти
для временных переменных. Можно считать, что эти алгоритмы берут
новое имя переменной из бесконечного множества неиспользованных
имен каждый раз, когда, согласно алгоритму, требуется новая вре-
менная переменная. Алгоритм распределения памяти должен размес-
тить эти переменные в памяти так, чтобы потребовалось мини-
мальное число ячеек памяти.
ИНТЕРВАЛОМ для переменной называется отрезок времени между
начальным определением этой переменной и ее последним использова-
нием. Очевидно, переменные, интервалы для которых совсем не свя-
заны, могут быть совмещены в памяти.
Рассмотрим набор переменных V1, V2, V3, ..., Vn. Для каждой
переменной Vi определена команда ST Vi, которая присваивает этой
переменной начальное значение и означает начало интервала для Vi.
Определяется также команда U Vi, которая означает очередное
использование этой переменной. Последняя команда этого вида, ис-
пользующая Vi, означает конец интервала для Vi. Команда U будет
применяться для обозначения таких команд, как ADD, SUB и т.д.
Для вычисления значения выражения (a+b*c)/(f*g-(d+e)/(h+k))
генератор команд по дереву разбора сформировал следующую последо-
вательность машинных команд:
L h
ADD k
ST V1
L d
ADD e
DIV V1
ST V2
L f
MPY g
SUB V2
ST V3
L b
MPY c
ADD a
DIV V3
Имена V1, V2 и V3 были взяты из множества неиспользованных
имен. Алгоритм распределения памяти, который мы хотим определить,
должен показать, что фактически эти три переменные могут быть по-
мещены в одну и ту же ячейку памяти T1.
Поскольку этот алгоритм распределения памяти касается опре-
деленных частей последовательности, можно написать
ST V1
U V1
ST V2
U V2
ST V3
U V3
Так как последнее использование каждой переменной встречает-
ся до определения следующей переменной, то легко видеть, что дос-
таточно одной ячейки памяти.
Предположим, что имеется стек неиспользованных ячеек памяти
[T1, T2, T3, ...]. Предположим далее, что каждый раз, когда нуж-
на ячейка, она берется из вершины стека и что сразу после послед-
него использования какой-либо переменной отведенная для нее ячей-
ка возвращается в стек. Тогда алгоритм распределения памяти для
временных переменных проще всего описать следующим образом.
Последовательность команд просматривается НАЧИНАЯ С КОНЦА, В
ОБРАТНОМ ПОРЯДКЕ. Если встречается команда вида U Vi и при этом
для переменной Vi еще не отведена ячейка памяти, то из вершины
стека берется свободная ячейка, ставится в соответствие перемен-
ной Vi и производится замена символа Vi в данной команде на ад-
рес соответствующей ячейки памяти. Такая же замена производится и
в том случае, когда ячейка памяти для переменной Vi уже отведена
ранее. Если встречается команда ST Vi и для переменной Vi еще не
отведена ячейка памяти, то это означает либо ошибку, либо избы-
точность данной команды, так как подразумевается, что переменная
вычисляется без дальнейшего использования. Если для переменной Vi
ранее отведена ячейка памяти, то производится замена символа Vi в
данной команде на адрес соответствующей ячейки памяти и, пос-
кольку это первое обращение к переменной Vi, соответствующая
ячейка в данный момент может быть возвращена в стек свободной па-
мяти, так как для Vi она больше не понадобится.
Данциг и Рейнольдс показали, что этот алгоритм оптимален,
хотя он и не единственный возможный оптимальный алгоритм.
Например, рассмотрим последовательность
(1) ST V1
(2) ST V2
(3) U V2
(4) U V1
(5) ST V3
(6) U V1
(7) U V3
(8) ST V4
(9) U V3
(10) U V4
и предположим, что стек свободной памяти есть [T1, T2, T3].
Начинаем просмотр последовательности с конца; команда (10) поме-
щает переменную V4 в ячейку T1, команда (9) отводит для перемен-
ной V3 ячейку T2, после этого стек остается в виде [T3]. Команда
(8) освобождает ячейку T1, после этого стек принимает вид [T1,
T3]. Команда (6) помещает V1 в ячейку T1. Подобным же образом ко-
манда (5) освобождает ячейку T2; затем эта ячейка отводится ко-
мандой (3) переменной V2. В результате получается последова-
тельность
(1) ST T1
(2) ST T2
(3) U T2
(4) U T1
(5) ST T2
(6) U T1
(7) U T2
(8) ST T1
(9) U T2
(10) U T1
Этот алгоритм находит интервал для каждой переменной, ис-
пользуя первое появление этой переменной при обратном посмотре
для определения конца интервала, а соответствующую команду ST для
определения начала интервала. Больше от алгоритма ничего не тре-
буется.
Предложенный алгоритм может быть изменен так, чтобы выраба-
тывать ту же информацию путем прямого просмотра последовательнос-
ти команд. При прямом просмотре каждая команда ST определяет
по-прежнему начало интервала. Если для каждой переменной подсчи-
тано число ее использований, то можно найти и последнее использо-
вание любой переменной, так что конец соответствующего интервала
становится известным.
Другой вариант состоит в том, чтобы находить конец интерва-
ла способом, описанным для обратного просмотра, но фактически
осуществлять прямой просмотр. Это достигается образованием цепоч-
ки всех обращений к данной переменной в порядке их появления при
просмотре. С каждой переменной Vi связываются три ячейки, содер-
жащие Fi, Li и Ri. Значение Fi является номером команды ST для
переменной Vi в последовательности команд. Значение Li является
номером команды последнего обращения к переменной Vi. Поэтому ин-
тервал для Vi имеет вид (Fi,Li). Значение Ri является адресом
ячейки, отведенной для переменной Vi, и обычно первоначально ус-
танавливается равным нулю.
Если при просмотре последовательности команд встречается ко-
манда ST Vi, то это вызывает установку значений Fi и Li, равных
номеру этой команды в последовательности. Если встречается коман-
да U Vi, то Vi заменяется в ней на Li, а Li становится равным но-
меру этой команды. Для предыдущего примера переменные Fi и Li
принимают конечные значения:
F1=1 L1=6
F2=2 L2=3
F3=5 L3=9
F4=8 L4=10
а команды принимают вид
(1) ST 0
(2) ST 0
(3) U 2
(4) U 1
(5) ST 0
(6) U 4
(7) U 5
(8) ST 0
(9) U 7
(10) U 8
После завершения просмотра последовательности значение Li
указывает конец цепочки всех обращений к переменной Vi. Каждая
команда содержит в адресной части номер предыдущей команды из той
же цепочки. Если для переменной Vi отведена ячейка в памяти, то
не представляет труда подняться по соответствующей цепочке, заме-
няя адресные части команд на адрес ячейки, отведенной для Vi.
Итак, интервал для каждой переменной определяется парой чисел
(Fi,Li). Теперь можно просмотреть эти пары, чтобы распределить
память для всех переменных. Если Fi=Li, то соответствующая коман-
да ST является избыточной, как отмечалось раньше.
Интервалы просматриваются в порядке возрастания Fi. В нашем
примере переменные уже перенумерованы в этом порядке. Обычно ал-
горитм генерирования команд может быть согласован с алгоритмом
распределения памяти так, чтобы такая нумерация переменных всег-
да имела место. Для этого только требуется, чтобы порядок, в ко-
тором имена переменных впервые появляются в последовательности
команд, совпадал с порядком мест, отведенных в векторах для соот-
ветствующих ячеек Fi и Li. В нашем примере переменная V1 должна
быть определена до переменной V2 и т.д.
При просмотре интервалов в порядке возрастания значений Fi
первая переменная помещается в ячейку T1. Для каждого очередного
интервала (Fj,Lj) исследуются АКТИВНЫЕ интервалы, для которых па-
мять уже распределена, с тем чтобы выяснить, существует ли такой
интервал (Fi,Li), что Li<Fj. Если такой интервал существует, то
переменная Vj помещается в ту же ячейку памяти, что и переменная
Vi, а интервал (Fi,Li) отмечается как НЕАКТИВНЫЙ. Таким образом,
на каждом этапе активные интервалы определяют текущие переменные,
которые размещены в ячейках памяти, а также определяют моменты,
когда эти ячейки освобождаются. Если не существует активного ин-
тервала со значением Li<Fj, то нужно взять из стека новую ячейку
и отвести ее для Vj.
В нашем примере для V1 была бы отведена ячейка T1. Так как
L1>F2, то для V2 нужна новая ячейка, и поэтому для V2 была бы от-
ведена ячейка T2. С другой стороны, L2<F3, так что можно помес-
тить V3 в ту же ячейку памяти, что и V2 (т.е. в ячейку T2).
Интервал для V2 теперь отмечается как неактивный. Так как L1<F4,
то переменная V4 тоже помещается в ячейку T1.
3.3 Алгоритм Биледи
Многие ЭВМ имеют небольшой набор быстрых регистров, которые
сходны с ячейками основной памяти с той разницей, что выборка ин-
формации, запомненной в быстрых регистрах, требует во много раз
меньше времени, чем выборка из основной памяти. На самом деле,
быстрые регистры могут быть индекс-регистрами или добавочными
сумматорами. т.е. их сожержимое может использоваться специальным
образом в арифметических или логических операциях, но здесь мы не
учитываем эти свойства.
Эти быстрые регистры могут быть использованы для хранения
копий переменных, размещенных в основной памяти; что же касается
временной переменной, то она может находиться в быстром регистре
все время своего существования, не требуя места в основной памя-
ти. Если число доступных быстрых регистров превышает число нуж-
ных переменных, то проблемы не возникает и быстрые регистры мо-
гут быть распределены по алгоритму для временных переменных.
Однако если число доступных быстрых регистров не является
достаточным, то возникает ситуация, когда необходимо решать, ка-
кие из переменных должны оставаться в быстрых регистрах. Решение
будет зависеть от частоты использования различных переменных, и
алгоритм должен стремиться максимально использовать быстрые ре-
гистры.
Алгоритм Биледи приводит к оптимальному результату при неко-
торых ограничениях и часто дает хорошие результаты в более общих
случаях. Предполагается, что быстрые регистры должны применяться
для хранения копий переменных из ОП и что использовать эти пере-
менные можно только после занесения их в быстрые регистры.
В дальнейшем будем предполагать, что рассматриваемые пере-
менные не будут изменяться; поэтому, если нужно заменить ка-
кую-то переменную в быстром регистре, то нет необходимости запо-
минать текущее содержимое быстрого регистра, так как копия этого
содержимого уже есть в основной памяти.
Возникающая задача похожа на задачу замещения страниц в сис-
теме с двумя уровнями памяти.
Биледи (1966) сформулировал оптимальный алгоритм для случая,
когда известна полная картина будущих обращений, что использова-
лось в системе с двумя уровнями памяти для получения оценки эф-
фективности многих применяемых эвристических алгоритмов.
Задача распределения быстрых регистров отличается от задачи
с двумя уровнями памяти тем, что для нее нет необходимости выпол-
нять распределение до получения полной последовательности команд.
Поэтому алгоритм Биледи может быть применен непосредственно для
получения оптимальных результатов. Этот алгоритм прменялся в ком-
пиляторах в течение нескольких лет, и только с недавнего времени,
в связи с его использованием в системе с двумя уровнями памяти, с
ним стало связываться имя Биледи.
Интервалы, в которых используются основные переменные Vi,
могут быть изображены диаграммой, как показано на рис. 20.8.
V5 ├────────────────┤
V4 ├───────────────────┤
V3 ├──────────────┼──────────────────┤
V2 ├─────┼─────┼────────┤
V1 ├───────────────────────────┤
1 2 3 4 5 6 7 8 9 10 11 12 13
Рис. 20.8 Диаграмма использования переменных Vi в последова-
тельности команд
Каждая горизонтальная переменная изображает интервал для не-
которой переменной, причем точки указывают номера мест в последо-
вательности команд, где используется эта переменная. Будем пред-
полагать, что в каждой команде используется только одна перемен-
ная, так что на одной вертикали не может появится двух и более
точек. Число горизонтальных линий, пересекающихся с какой-либо
вертикалью, указывает, сколько ячеек памяти содержат в соответ-
ствующий момент нужные значения переменных. Если число пересече-
ний какой-то вертикали с различными горизонтальными линиями пре-
вышает число доступных быстрых регистров, то одна или более чем
одна из переменных не может быть помещена в быстрых регистрах.
Мы будем также полагать, что каждая выборка какой-либо пере-
менной из быстрого регистра требует ничтожной затраты времени.
Если нужной переменной нет в быстром регистре, то необходимо за-
менить содержимое одного из быстрых регистров на значение этой
переменной. Определим функцию S(i,t) следующим образом:
S(i,t)=0, если переменная Vi не находится в быстром регис-
тре в момент t;
S(i,t)=1, если Vi находится в быстром регистре в момент t.
Тогда сумма по всем номерам "t" не должна превышать N, где N
- число доступных быстрых регистров.
Если m(t) - номер переменной, используемой в команде "t", то
функцию U, характеризующую эффективность использования быстрых
регистров, можно определить следующим образом:
U=сумма S(m(t),t)).
t
Максимальное значение функции U (при заданных выше ограниче-
ниях) достигается, когда наибольшее число использований перемен-
ных будет происходить путем непосредственного обращения к быс-
трым регистрам. Единственное доп. ограничение состоит в том, что
S(m(t-1),t)=1
для всех значений t. Это означает, что каждая переменная при
использовании заносится в быстрый регистр.
Алгоритм Биледи состоит в следующем.
Последовательность команд исследуется, начиная с первой ко-
манды, определенной значением t=1. Для каждого значения t рас-
сматривается переменная Vi, где i=m(t), и выполняются действия по
одному из следующих вариантов:
(1) Для переменной Vi отведен быстрый регистр; тогда ис-
пользуется этот регистр.
(2) Для переменной Vi не отведено быстрого регистра, но
имеется неиспользованный быстрый регистр; в таком случае, этот
регистр отводится для Vi.
(3) Для переменной Vi не отведено быстрого регистра, и рас-
пределены все быстрые регистры. Тогда рассматриваются переменные,
для которых в текущий момент имеются копии в быстрых регистрах, и
если значение какой-то одной из них больше не потребуется, то
соответствующий быстрый регистр теперь отводится для Vi. Если со-
держимое всех быстрых регистров все еще необходимо, то для Vi от-
водится быстрый регистр, соответствующий той переменной, следую-
щее использование которой наиболее удалено от данной команды.
Пусть имеются 3 быстрых регистра R1, R2 и R3. Начиная с мо-
мента t=1 первые три команды отведут регистры R1, R2 и R3 для пе-
ременных V1, V2 и V3. В момент t=5 содержимое всех трех быстрых
регистров еще необходимо, а требуется регистр для V4. Переменная
V1 не будет использоваться до момента t=10, тогда как V2 ис-
пользуется при t=6 и V3 - при t=8. Поэтому регистр R1 теперь ис-
пользуется для V4. При t=7 вновь возникает такая же проблема.
Значение V4 не требуется до момента t=11, тогда как V3 ис-
пользуется при t=8 и V2 - при t=9. Поэтому регистр R1 отводится
для V5. При t=10 переменная V1 используется снова. Теперь приме-
няется сформулированное выше условие (2), так как значение V2
больше не потребуется. Поэтому регистр R2 отводится для V1. Ана-
логично при t=11 регистр К2 отводится для V4, так как больше не
потребуется значение V1.
Распределение регистров R1 и R3 для V5 и V3 сохраняется до
конца последовательности команд. Описанное распределение быстрых
регистров показано диаграммой на рис. 20.9.
V5 ├─────────────────┤
V4 ├────── ─ ─ ─ ─ ─ ──┤
V3 ├───────────────┼──────────────────┤
V2 ├─────┼─────┼────────┤
V1 ├──────────── ─ ─ ─ ─ ─ ─ ──┤
1 2 3 4 5 6 7 8 9 10 11 12 13
Рис. 20.9 Диаграмма использования переменных Vi в последова-
тельности команд, поясняющая работу алгоритма Биледи
4. ТАБЛИЦА СИМВОЛОВ
4.1 Описатели
Для каждого вхождения идентификатора в исх. программе осу-
ществляется поиск соотв. элемента в таблице символов (ТС), инфор-
мация, полученная при данном вхождении, сопоставляется с ранее
полученной информацией, выполняется необходимый контроль и регис-
трация новой информации. Таким образом, ТС является очень важной
частью компилятора и, в некотором смысле, узловой точкой всей
трансляции. Ее структура должна допускать эффективный поиск и
внесение информации. В то же время, каждый элемент должен зани-
мать как можно меньше места, для того, чтобы хватало места на
большие программы с большим числом идентификаторов. Вообще гово-
ря, желательно, чтобы возможно меньшее число программ транслято-
ра непосредственно работало с ТС. Это позволит достаточно легко
вносить необходимые изменения. Особенно важно тщательно согласо-
вать формат описателя до того, как начнется программирование
транслятора.
ОПИСАТЕЛЕМ называется часть элемента ТС, в которой описы-
вается идентификатор. Существует другой термин для обозначения
этого же понятия, введенный Фельдманом - "семантическое слово" (У
Ахо и Ульмана это назывется "дескриптором").
Количество информации, которое нужно хранить в описателе,
зависит от того, чем является фактически идентификатор - простой
переменной, массивом, функцией и т.д. Поэтому в некоторых реали-
зацих описатели имеют переменную длину. Это допустимо не при лю-
бой организации ТС. Иногда в целях экономии памяти выбирают эле-
мент ТС малого размера, а для идентификаторов, требующих больше
места, отводят по несколько соседних элементов.
Другая возможность открывается, емли в нужных случаях отво-
дить часть описателя под указатель, ссылающийся на дополни-
тельную таблицу. Это несколько осложняет программирование, пос-
кольку тип описателя может менять свой смысл в зависимости от ти-
па элемента.
4.2 Возможные параметры описания переменных и процедур
в таблице символов
Для переменных и процедур в описателе может потребоваться
следующая информация:
ПЕРЕМЕННАЯ:
Тип (вещ., целый, строка, комплексный, метка и т.д.);
Точность, масштаб, длина;
Вид (простая переменная, массив, структура и т.д.);
Адрес во время выполнения программы;
Если массив, то - число измерений. Если есть граничные пары
(ограниченные множества в Паскале), то их значения;
Если структура или компонента структуры, то связь данной
компоненты с другими компонентами;
Формальный параметр или нет; если да, то тип соответствия
параметров;
Описана ли переменная как extern или как идентификатор объе-
динения (union) ?
Обрабатывалось ли уже ее описание ?
Существует ли инструкция, присваивающая ей значение ?
ПРОЦЕДУРА:
Является ли она внешней по отношению к программе ?
Является ли она функцией ?
Каков ее тип ?
Обрабатывалось ли уже ее описание ?
Является ли она рекурсивной ?
Каковы ее формальные параметры ? Их описатели должны быть
связаны с именем функции для того, чтобы можно было проверить их
соответствие фактическим параметрам.
4.3 Таблица символов компилятора /360 WATFOR
/360 WATFOR - однопроходной компилятор с языка ФОРТРАН IV
для машин IBM/360.
ТС состоит из пяти разных списков:
- списка меток;
- списка арифметических констант;
- списка имен общих блоков, подпрограмм, переменных;
- списка блоков;
- списка подпрограмм.
(Таким образом, некоторые элементы оказываются в двух списках.)
Элементы всех списков помещаются в одной и той же таблице;
первые 2 байта каждого элемента используются для ссылки на сле-
дующий элемент в этом же списке. Следовательно, поиск отдельного
элемента сводится к линейному просмотру по ссылкам.
Элементы различных типов имеют разлмчную длину в пределах от
8 до 20 байтов.
Например, элемент для переменной имеют длину 16 байтов и со-
держит следующую информацию (первые 2 байта опущены):
3-4 5-10 11-12 13-14 15-16
┌────┬───────┬───────────┬──────┬───────────┐
│ B2 │идент-р│размерность│COMMON│EQUIVALENCE│
└────┴───────┴───────────┴──────┴───────────┘
В полях "COMMON" и "EQUIVALENCE" помещаются указатели на
элементы других переменных, связанных с данной при помощи ин-
струкций COMMON и EQUIVALENCE.
В поле "размерность" содержится 0, если переменная не яв-
ляется массивом; в противном случае оно указывает на отдельный
элемент, содержащий всю информацию о размерности: величины
d1,...,dn и длину d1*...*dn.
Два байта, обозначенные B2, содержат всю остальную информа-
цию (см. рис. 20.10). Первоначально все поля содержат нули, и в
них заносится информация по мере обработки программы.
Единица в каждом однобитовом поле на рис. 20.10 говорит о
наличии соответствующего факта.
│ 0-1 │ 2-4 │ 5-6 │ 7 │ 8 │ 9 │
├───────────┼────────┼──────────┼────────┼───────┼─────────┤
│10 - прос- │число │ тип │ длина │ тип │исполь- │
│ тая │индексов│00 - лог. │задается│сформи-│зование │
│ перем.│в случае│01 - цел. │програм-│рован │сформиро-│
│11 - массив│массива │10 - вещ. │мистом │ │вано │
│ │ │11 - ком- │ │ │ │
│ │ │ плекс. │ │ │ │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │
├─────────┼────────┼─────────┼────────┼────────┼───────────┤
│формаль- │текущий │присваи- │имеет │встреча-│встреча- │
│ный пара-│параметр│вается │нач. │ется в │ется в │
│метр │цикла │значение │значение│COMMON │EQUIVALENCE│
│програм- │ │по ASSIGN│ │ │ │
│мы │ │ │ │ │ │
Рис. 20.10 Схема подполей поля B2
4.4 Представление структур в таблице символов
Мы можем представлять структуру, отводя по одному элементу
для каждой компоненты. Кроме обычных полей, каждый элемент имеет
три дополнительных поля: FATHER, SON и BROTHER.
Поле FATHER указывает на ОТЦА, SON - на первого СЫНА компо-
ненты, а поле BROTHER - на ее следующего БРАТА в последова-
тельности братьев группы.
Если данный элемент не имеет отца, сына или следующего бра-
та, то соответствующее поле будет содержать 0.
Ниже на рис. 20.11 приведены иерархические схемы строения
двух структур с указанием перед идентификатором каждой из
(под)компонент уровня ее вложенности.
1 A 1 L
2 B 2 M
3 C 4 C
3 D 4 D
2 E 2 B
2 F 5 C
5 D
Рис. 20.11 Схема построения двух структур
Элементы ТС для этих двух структур приведены ниже на Табли-
це 20.1. Этой информации может хватить с избытком (а может ока-
заться и недостаточно) для эффективной работы со структурами.
Поскольку могут существовать элементы, имена которых совпа-
дают, вводится еще один указатель SAME, связывающий такие элемен-
ты между собой. Первый из этих элементов содержит в этом поле 0.
Таблица 20.1 Схема заполнения элементов ТС для двух струк-
тур на рис. 20.11
┌────┬───┬─────┬──────┬────┬───────┐
│ │ID │ SAME│FATHER│ SON│BROTHER│
├────┼───┼─────┼──────┼────┼───────┤
│ A1 │ A │ 0 │ 0 │ B1 │ 0 │
│ B1 │ B │ 0 │ A1 │ C1 │ E1 │
│ C1 │ C │ 0 │ B1 │ 0 │ D1 │
│ D1 │ D │ 0 │ B1 │ 0 │ 0 │
│ E1 │ E │ 0 │ A1 │ 0 │ F1 │
│ F1 │ F │ 0 │ A1 │ 0 │ 0 │
│ L1 │ L │ 0 │ 0 │ M1 │ 0 │
│ M1 │ M │ 0 │ L1 │ C2 │ B2 │
│ C2 │ C │ C1 │ M1 │ 0 │ D2 │
│ D2 │ D │ D1 │ M1 │ 0 │ 0 │
│ B2 │ B │ B1 │ L1 │ C3 │ 0 │
│ C3 │ C │ C2 │ B2 │ 0 │ D3 │
│ D3 │ D │ D2 │ B2 │ 0 │ 0 │
└────┴───┴─────┴──────┴────┴───────┘
_
|