close

Вход

Забыли?

вход по аккаунту

?

29

код для вставкиСкачать
Никлаус Вирт
Построение компиляторов
Москва, 2010
УДК 32.973.26 018.2
ББК 004.438
В52
Никлаус Вирт
Построение компиляторов / Пер. с англ. Борисов Е. В., Чернышов Л. Н. –
М.: ДМК Пресс, 2010. – 192 с.: ил.
ISBN 978 5 94074 585 3
Книга известного специалиста в области информатики Никлауса Вирта
написана по материалам его лекций по вводному курсу проектирования
компиляторов. На примере простого языка Оберон 0 рассмотрены все эле
менты транслятора, включая оптимизацию и генерацию кода. Приведен
полный текст компилятора на языке программирования Оберон.
Для программистов, преподавателей и студентов, изучающих системное
программирование и методы трансляции.
Содержание компакт диска:
Базовая конфигурация системы Блэкбокс с коллекцией модулей, реализу
ющих оригинальный компилятор с языка Оберон 0 и компилятор, адапти
рованный под Блэкбокс.
Базовые инструкции по работе в системе Блэкбокс.
Полный перевод документации системы Блэкбокс на русский язык.
Конфигурация системы Блэкбокс для использования во вводных курсах
программирования в университетах.
Конфигурация системы Блэкбокс для использования в школах (полная ру
сификация меню, сообщений компилятора, с возможностью использования
ключевых слов на русском и других национальных языках).
Доклады участников проекта Информатика 21 по опыту использования
системы Блэкбокс в обучении программированию.
Оригинальные дистрибутивы системы Блэкбокс 1.5 (основной рабочий) и 1.6rc6.
Инструкции по работе в Блэкбоксе под Linux/Wine.
Дистрибутив оптимизирующего компилятора XDS Oberon (версии Linux и
MS Windows).
OberonScript – аналог JavaScript для использования в Web приложениях.
This is a slightly revised version of the book published by Addison Wesley in 1996
ISBN 0 201 40353 6 (анг.)
ISBN 978 5 94074 585 3
© N. Wirth, 1985 (Oberon version: August 2004)
© Перевод с английского Борисов Е. В.,
Чернышов Л. Н., 2010
© Оформление, издание, ДМК Пресс, 2010
Краткое содержание
ОТ АВТОРОВ ПЕРЕВОДА ................................................... 10
ВВЕДЕНИЕ ................................................................................ 12
ГЛАВА 1. ВВЕДЕНИЕ
........................................................... 15
ГЛАВА 2. ЯЗЫК И СИНТАКСИС ....................................... 19
ГЛАВА 3. РЕГУЛЯРНЫЕ ЯЗЫКИ ..................................... 27
ГЛАВА 4. АНАЛИЗ КОНТЕКСТНО СВОБОДНЫХ
ЯЗЫКОВ ..................................................................................... 33
ГЛАВА 5. АТРИБУТНЫЕ ГРАММАТИКИ
И СЕМАНТИКИ ........................................................................ 45
ГЛАВА 6. ЯЗЫК ПРОГРАММИРОВАНИЯ
ОБЕРОН 0 ................................................................................. 51
ГЛАВА 7. СИНТАКСИЧЕСКИЙ АНАЛИЗАТОР
ДЛЯ ОБЕРОНА 0 ................................................................... 55
ГЛАВА 8. УЧЕТ КОНТЕКСТА, ЗАДАННОГО
ОБЪЯВЛЕНИЯМИ .................................................................. 65
ГЛАВА 9. RISC АРХИТЕКТУРА КАК ЦЕЛЬ .................. 75
ГЛАВА 10. ВЫРАЖЕНИЯ И ПРИСВАИВАНИЯ ........... 81
ГЛАВА 11. УСЛОВНЫЕ И ЦИКЛИЧЕСКИЕ
ОПЕРАТОРЫ И ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ ............ 95
4
Содержание
ГЛАВА 12. ПРОЦЕДУРЫ И КОНЦЕПЦИЯ
ЛОКАЛИЗАЦИИ ................................................................... 109
ГЛАВА 13. ЭЛЕМЕНТАРНЫЕ ТИПЫ ДАННЫХ ......... 125
ГЛАВА 14. ОТКРЫТЫЕ МАССИВЫ,
УКАЗАТЕЛЬНЫЙ И ПРОЦЕДУРНЫЙ ТИПЫ .............. 131
ГЛАВА 15. МОДУЛИ И РАЗДЕЛЬНАЯ
КОМПИЛЯЦИЯ ...................................................................... 141
ГЛАВА 16. ОПТИМИЗАЦИЯ И СТРУКТУРА
ПРЕ/ПОСТПРОЦЕССОРА ................................................. 153
ПРИЛОЖЕНИЕ A. СИНТАКСИС ...................................... 164
ПРИЛОЖЕНИЕ B. НАБОР СИМВОЛОВ ASCII .......... 167
ПРИЛОЖЕНИЕ C. КОМПИЛЯТОР ОБЕРОН 0 ......... 168
ЛИТЕРАТУРА ......................................................................... 191
Содержание
От авторов перевода .......................................................... 10
О книге ......................................................................................... 10
О переводе .................................................................................. 10
Введение .................................................................................. 12
Предисловие ................................................................................ 12
Благодарности ............................................................................. 14
Глава 1. Введение ................................................................ 15
Глава 2. Язык и синтаксис ............................................... 19
2.1. Упражнения ........................................................................... 24
Глава 3. Регулярные языки ............................................. 27
3.1. Упражнение ........................................................................... 32
Глава 4. Анализ контекстно свободных языков ...... 33
4.1. Метод рекурсивного спуска .................................................. 34
4.2. Таблично управляемый нисходящий синтаксический
анализ .......................................................................................... 38
4.3. Восходящий синтаксический анализ ..................................... 40
4.4. Упражнения ........................................................................... 42
Глава 5. Атрибутные грамматики и семантики .... 45
5.1. Правила типов ....................................................................... 46
5.2. Правила вычислений ............................................................. 47
5.3. Правила трансляции .............................................................. 48
5.4. Упражнение ........................................................................... 49
Глава 6. Язык программирования Оберон 0
......... 51
6.1. Упражнение ........................................................................... 54
6
Содержание
Глава 7. Синтаксический анализатор
для Оберона 0 ....................................................................... 55
7.1. Лексический анализатор ....................................................... 56
7.2. Синтаксический анализатор .................................................. 57
7.3. Устранение синтаксических ошибок...................................... 59
7.4. Упражнения ........................................................................... 64
Глава 8. Учет контекста, заданного
объявлениями ........................................................................ 65
8.1. Объявления ........................................................................... 66
8.2. Записи о типах данных .......................................................... 68
8.3. Представление данных во время выполнения ....................... 69
8.4. Упражнения ........................................................................... 73
Глава 9. RISC архитектура как цель
........................... 75
9.1. Ресурсы и регистры............................................................... 76
Глава 10. Выражения и присваивания ...................... 81
10.1. Прямая генерация кода по принципу стека ......................... 82
10.2. Отсроченная генерация кода ............................................... 84
10.3. Индексированные переменные и поля записей ................... 89
10.4. Упражнения ......................................................................... 94
Глава 11. Условные и циклические операторы
и логические выражения .................................................. 95
11.1. Сравнения и переходы ........................................................ 96
11.2. Условные и циклические операторы .................................... 97
11.3. Логические операции ........................................................ 101
11.4. Присваивание логическим переменным ........................... 105
11.5. Упражнения ....................................................................... 106
Глава 12. Процедуры и концепция
локализации ......................................................................... 109
12.1. Организация памяти во время выполнения ....................... 110
12.2. Адресация переменных ..................................................... 112
12.3. Параметры ........................................................................ 114
12.4. Объявления и вызовы процедур ........................................ 116
Содержание
7
12.5. Стандартные процедуры ................................................... 121
12.6. Процедуры функции ......................................................... 122
12.7. Упражнения ....................................................................... 123
Глава 13. Элементарные типы данных .................... 125
13.1. Типы REAL и LONGREAL ..................................................... 126
13.2. Совместимость между числовыми типами данных ............ 127
13.3. Тип данных SET .................................................................. 129
13.4. Упражнения ....................................................................... 130
Глава 14. Открытые массивы, указательный
и процедурный типы ......................................................... 131
14.1. Открытые массивы ............................................................ 132
14.2. Динамические структуры данных и указатели ................... 133
14.3. Процедурные типы ............................................................ 136
14.4. Упражнения ....................................................................... 138
Глава 15. Модули и раздельная компиляция
...... 141
15.1. Принцип скрытия информации .......................................... 142
15.2. Раздельная компиляция .................................................... 143
15.3. Реализация символьных файлов ....................................... 145
15.4. Адресация внешних объектов ............................................ 149
15.5. Проверка конфигурационной совместимости ................... 150
15.6. Упражнения ....................................................................... 152
Глава 16. Оптимизация и структура
пре/постпроцессора ........................................................ 153
16.1. Общие соображения ......................................................... 154
16.2. Простые оптимизации ...................................................... 155
16.3. Исключение повторных вычислений .................................. 156
16.4. Распределение регистров ................................................. 157
16.5. Структура пре/постпроцессорного компилятора .............. 158
16.6. Упражнения ....................................................................... 162
Приложение A. Синтаксис .............................................. 164
A.1. Оберон 0 ............................................................................. 164
A.2. Оберон................................................................................. 164
A.3. Символьные файлы ............................................................. 166
8
Содержание
Приложение B. Набор символов ASCII .................... 167
Приложение C. Компилятор Оберон 0 .................... 168
C.1. Лексический анализатор..................................................... 169
C.2. Синтаксический анализатор ............................................... 172
C.3. Генератор кода ................................................................... 182
Литература ............................................................................ 191
От авторов перевода
О книге
Давно известно, что лучший способ постичь секреты мастерства – это наблюдать
за работой мастера. Эта небольшая, но насыщенная информацией книжка, по сути
дела, представляет собой отчет о такой работе. Ну а то, что ее автор – настоящий
мастер своего дела, сомнению не подлежит, потому что имя профессора Никлауса
Вирта ни в каких дополнительных рекомендациях не нуждается. Эта книга – сво
его рода мастер класс, который дает своим ученикам всемирно известный маэст
ро. Она не является ни «тяжелой» теоретической монографией, ни сборником на
ставлений и поучений увенчанного лаврами мэтра. Эта книжка – практическое
пособие для всех тех любознательных людей, кто желает разобраться и понять,
что такое компилятор и как он устроен. По мнению автора, без этого ни один про
граммист не может называть себя квалифицированным специалистом.
В отличие от многочисленных книг, которые исчерпывающе описывают и тео
рию, и разнообразные методы синтаксического анализа, перевода и компиляции,
эта книжка посвящена реализации одного единственного компилятора современ
ного языка программирования для конкретного компьютера. Но это нисколько не
умаляет ее достоинства. Если обычные книги после прочтения почти всегда ос
тавляют читателя наедине с вопросом «А что же дальше? Где же результат?» или
с загадочными, полными опечаток текстами готовых программ, то эта небольшая
книжка расставляет практически все точки над i, проводя читателя от самого на
чала до самого конца процесса разработки компилятора, попутно предупреждая
его о неверных шагах и давая ему в руки богатый практический материал. Автор
придерживается принципа «Делай со мной. Делай, как я. Делай лучше меня».
Таким образом, книга Н. Вирта – безусловно, не только прекрасное дополне
ние к многочисленным и столь же прекрасным фундаментальным трудам по этой
теме, но может и должна использоваться в качестве практического пособия по
изучению компиляторов. Кроме того, простота и доступность преподнесения до
вольно сложного материала снимает с него покров таинственности и делает его
доступным практически каждому любителю программирования. Остается только
сожалеть о том, что эта книга не была своевременно переведена и издана у нас.
Для практического использования текст компилятора Оберон 0, о котором
идет речь в книге, адаптирован к системе БлэкБокс (BlackBox Component Builder –
вариант системы Оберон). Оригинальные и адаптированные исходные тексты
компилятора можно найти на сайте www.oberoncore.ru.
О переводе
Несколько слов о переводе.
В силу того, что мы имеем дело не с развернутой монографией, а с конспектом
лекций, каждая фраза, часто облекаемая в форму тезиса, до предела насыщена
10
От авторов перевода
информацией. Поэтому наша основная задача при переводе состояла в том, чтобы
сохранить лаконичность и информационную насыщенность авторского текста и
при этом максимально точно довести его суть до читателя, не поддаваясь искуше
нию сдобрить его отсебятиной.
Несмотря на царящие до сих пор «разброд и шатания» в терминологии по этой
теме, мы при переводе, следуя за автором, отдавали предпочтение наиболее усто
явшимся, хотя и не всегда правильным и точным, терминам.
В связи с этим нельзя не упомянуть о терминах «front end» и «back end». Они
уже давно употребляются в разнообразной англоязычной технической литерату
ре, но тем не менее до сих пор не находят адекватных русскоязычных эквивален
тов. Чаще всего их перевод зависит от контекста. Применительно к компиляторам
наиболее точными их русскими аналогами являются, пожалуй, «машинно неза
висимая часть» и «машинно зависимая часть» соответственно. Однако мы, те
перь уже следуя авторской лаконичности, предпочли им более абстрактные и ме
нее точные, но более короткие термины – «препроцессор» и «постпроцессор»
соответственно.
Кроме того, список литературы пронумерован, и именные ссылки на него в
тексте заменены номерными. К списку литературы добавлено несколько более
поздних публикаций.
Авторы перевода выражают благодарность В. Н. Лукину за прочтение перево
да и сделанные замечания.
Введение
Предисловие
Эта книга появилась из моих конспектов лекций по вводному курсу проектирова
ния компиляторов в ETH (Федеральном технологическом институте) в Цюрихе.
Несколько раз меня просили объяснить необходимость этого курса, так как про
ектирование компиляторов рассматривается как некий эзотерический предмет,
применяемый только в нескольких узкоспециализированных программистских
фирмах. Поскольку в наши дни все, что не приносит немедленной выгоды, долж
но быть оправдано, я должен попробовать объяснить, почему я вообще считаю
этот предмет важным и уместным для студентов, изучающих информатику.
Основой любого академического образования является то, что передается не
только знание и, в случае инженерного образования, «ноу хау», но и понимание
сути явления и способность проникнуть в его суть. В частности, в информатике
мало поверхностного знания системы, необходимо еще и понимание ее содер
жания. Каждый образованный программист должен знать возможности компью
тера, понимать способы и методы представления и интерпретации программ.
Компилятор преобразует текст программы во внутренний код, это мост, соединя
ющий программное обеспечение и аппаратные средства.
Однако кому то может показаться, что нет необходимости знать методы транс
ляции для понимания связи между исполняемой программой и кодом и еще менее
важно знать, как на самом деле пишется компилятор. Личный опыт преподавате
ля подсказывает мне, что глубокое понимание предмета лучше всего приходит
при всестороннем проникновении как в общую идею системы, так и в детали ее
реализации. В нашем случае таким проникновением будет написание реального
компилятора.
Конечно, мы должны сосредоточиться на основах. В конце концов, эта книга –
вводный курс, а не справочник для специалистов. Наше первое ограничение каса
ется входного языка. Было бы неуместным рассматривать проектирование ком
пиляторов для больших языков. Язык должен быть небольшим, но тем не менее
должен содержать все поистине фундаментальные элементы языков программи
рования. Для наших целей мы выбрали подмножество языка Оберон. Второе
ограничение касается целевого компьютера. Он должен иметь обычную структу
ру и простой набор команд. Наиболее важна практичность обучающих понятий.
Оберон – это общецелевой гибкий и мощный язык, а наш целевой компьютер иде
альным образом отражает удачную RISC архитектуру. И наконец, третье огра
ничение состоит в отказе от изощренных методов оптимизации кода. При таких
условиях можно объяснить весь компилятор в деталях и даже создать его в огра
ниченные рамками курса сроки.
В главах 2 и 3 рассматриваются основы языка и синтаксиса. Глава 4 посвящена
синтаксическому анализу, то есть методу разбора предложений и программ. Мы
12
Теория и методы построения компиляторов. Введение
сосредоточили внимание на простом, но удивительно мощном методе рекурсив
ного спуска, который используется в нашем иллюстративном компиляторе. Мы
рассматриваем синтаксический анализ как средство для достижения цели, но не
как самоцель. Глава 5 готовит нас к переходу от синтаксического анализатора
к компилятору, а выбранный метод ставится в зависимость от атрибутов синтак
сических конструкций.
После знакомства в главе 6 с языком Оберон 0 в главе 7 приводится разработ
ка его синтаксического анализатора методом рекурсивного спуска. Из практиче
ских соображений обсуждается также обработка синтаксических ошибок. В гла
ве 8 мы объясняем, почему языки, содержащие объявления и, следовательно,
зависимость от контекста, могут тем не менее обрабатываться как контекстно
свободные.
До этого момента не было необходимости в рассмотрении целевого компьюте
ра и набора его команд. Но поскольку последующие главы посвящены теме гене
рации кода, то становится неизбежной спецификация целевого компьютера (гла
ва 9). Это RISC архитектура с небольшим набором команд и набором регистров.
В связи с этим центральная тема разработки компилятора – генерация последова
тельностей команд – разнесена по трем главам: код для выражений и присваива
ний (глава 10), код для условных операторов и операторов цикла (глава 11), код
для объявлений процедур и обращений к ним (глава 12). Вместе они покрывают
все конструкции языка Оберон 0.
Последующие главы посвящены нескольким дополнительным важным конст
рукциям языков программирования общего назначения. Их трактовка поверхност
на и не затрагивает деталей, но подкреплена несколькими упражнениями в конце
соответствующих глав. Рассматриваются следующие темы: элементарные типы
данных (глава 13), открытые массивы, динамические структуры данных и проце
дурные типы, называемые методами в объектно ориентированной терминологии
(глава 14).
Глава 15 касается модульного конструирования и принципов скрытия инфор
мации. Это приводит к теме разработки программного обеспечения в команде, ос
нованной на определении интерфейсов и последующей независимой реализации
частей (модулей). Методика основана на раздельной компиляции модулей с пол
ным контролем совместимости типов всех компонентов интерфейса. Такая мето
дика имеет первостепенное значение для разработки программного обеспечения
в целом и для современных языков программирования в частности.
Наконец, глава 16 дает краткий обзор проблем оптимизации кода. Она необхо
дима, с одной стороны, из за семантической пропасти между исходными языками
и архитектурами компьютеров, а с другой – из за нашего желания как можно пол
нее использовать все доступные ресурсы компьютеров.
Теория и методы построения компиляторов. Введение
13
Благодарности
Я выражаю мои искренние благодарности всем, кто способствовал своими пред
ложениями и критикой этой книги, которая созрела за многие годы преподавания
курса проектирования компиляторов в ETH в Цюрихе. В частности, я обязан Хан
спетеру Месенбоку и Михаэлю Францу, которые внимательно прочли рукопись и
подвергли ее критическому разбору. Кроме того, я благодарю Штефана Геринга,
Штефана Людвига и Джозефа Темпла за их ценные комментарии и сотрудниче
ство в курсе обучения.
Никлаус Вирт
Декабрь 1995
Глава 1
Введение
16
Введение
Компьютерные программы пишутся на языке программирования и определяют
классы вычислительных процессов. Однако компьютеры выполняют не тексты
программ, а последовательности отдельных команд. Поэтому текст программы
должен быть оттранслирован в соответствующую последовательность команд,
прежде чем он может быть выполнен компьютером. Эта трансляция может быть
автоматизирована, то есть она сама может быть описана программой. Трансли
рующая программа называется компилятором, а текст, который должен трансли
роваться, называется исходным текстом (или иногда исходным кодом).
Нетрудно видеть, что этот процесс трансляции от исходного текста до после
довательности команд требует значительных усилий и должен подчиняться
сложным правилам. Построение первого компилятора для языка Фортран (for
mula translator) примерно в 1956 году было смелым предприятием, в успехе кото
рого мало кто был уверен. Создание компилятора потребовало приблизительно
18 человеко лет и поэтому считалось одним из крупнейших программных проек
тов того времени.
Запутанность и сложность процесса трансляции могли быть уменьшены толь
ко при выборе ясно определенного, хорошо структурированного исходного язы
ка. Это произошло впервые в 1960 году с появлением языка Алгол 60, который
заложил технические основы проектирования компиляторов, имеющие значение
и по сей день. Также впервые для определения структуры языка была применена
формальная система записи [12].
Процесс трансляции теперь управляется структурой анализируемого текста.
Текст разбирается на грамматические компоненты согласно заданному синтак
сису. Для простейших компонентов их семантика распознается, а значение (се
мантика) составных компонентов выводится из семантики их составляющих.
Смысл исходного текста конечно же должен быть сохранен при трансляции.
В сущности, процесс трансляции состоит из следующих частей:
1. Последовательность литер исходного текста транслируется в соответст
вующую последовательность символов словаря языка. Например, иденти
фикаторы, состоящие из букв и цифр, числа, состоящие из цифр, разделите
ли и операторы, состоящие из специальных литер, распознаются на этом
этапе, который называется лексическим анализом.
2. Последовательность символов преобразуется в представление, которое не
посредственно отражает синтаксическую структуру исходного текста и де
лает эту структуру легко узнаваемой. Этот этап называется синтаксическим
анализом (грамматическим разбором).
3. Языки высокого уровня характеризуются тем, что объекты программ,
например переменные и функции, классифицируются согласно их типу.
Поэтому в дополнение к синтаксическим правилам в языке определяют
правила совместимости типов операций и операндов. Следовательно, до
полнительная обязанность компилятора – проверка соблюдения програм
мой этих правил. Такая проверка называется контролем типов.
4. На основе представления, полученного на шаге 2, генерируется последова
тельность команд из системы команд целевого компьютера. Этот этап назы
Введение
17
вается генерацией кода. Вообще, это наиболее запутанная часть компилято
ра и не в последнюю очередь потому, что системам команд многих компью
теров недостает желаемой регулярности. Зачастую из за этого генерация
кода разделяется еще на несколько фаз.
Разбиение процесса компиляции на как можно большее число частей было
преобладающей технологией приблизительно до 1980 года, потому что доступная
память была слишком мала, чтобы вместить весь компилятор. Только отдельные
части компилятора, будучи подогнанными по размеру к памяти, могли загружать
ся последовательно одна за другой. Части назывались проходами, а все вместе на
зывалось многопроходным компилятором. Число проходов было обычно от 4 до 6,
но в одном из известных автору случаев (для PL/I) достигло 70. Как правило,
выход прохода k служил входом для прохода k+1, а диск служил промежуточной
памятью (рис. 1.1). Очень частое обращение к дисковой памяти приводило к дли
тельной компиляции.
Рис. 1.1. Многопроходная компиляция
Современные компьютеры с их практически неограниченными объемами па
мяти дают возможность исключить промежуточное хранение данных на диске.
Вместе с этим можно отказаться как от сложного процесса линеаризации структу
ры данных на выходе, так и от воссоздания ее на входе. Поэтому однопроходные
компиляторы могут увеличить скорость компиляции в несколько тысяч раз. Вме
сто того чтобы цепляться одна за другую в строго последовательном порядке, раз
личные части (задачи) чередуются. Например, генерация кода не ждет, пока за
вершатся все подготовительные задачи, а начинается сразу после распознавания
первой сентенциальной структуры исходного текста.
Разумным компромиссом является компилятор, состоящий из двух частей,
называемых препроцессором (front end) и постпроцессором (back end). Первая
часть включает лексический и синтаксический анализы с контролем типов и гене
рирует дерево, представляющее синтаксическую структуру исходного текста. Это
дерево хранится в основной памяти и образует интерфейс для второй части, кото
рая выполняет генерацию кода. Основное преимущество такого решения заклю
чается в независимости препроцессора компилятора от целевого компьютера и
его системы команд. Это преимущество неоценимо, когда нужны компиляторы
одного и того же языка для разных компьютеров, потому что один и тот же пре
процессор служит им всем.
Идея разделения исходного языка и целевой архитектуры привела также к со
зданию проектов с несколькими препроцессорами для различных языков, генери
рующими деревья для единственного постпроцессора. Если для трансляции m
18
Введение
языков для n компьютеров раньше было необходимо m × n компиляторов, то те
перь достаточно m препроцессоров и n постпроцессоров (рис. 1.2).
Рис. 1.2. Препроцессор и постпроцессор
Это современное решение задачи переноса компилятора напоминает нам под
ход, который сыграл значительную роль в распространении Паскаля примерно
в 1975 году [15]. Роль структурного дерева была возложена на линеаризованную
форму – последовательность команд абстрактного компьютера. Постпроцессор
состоял из программы интерпретатора, реализация которой не вызывала боль
ших трудностей, а последовательность команд называлась P кодом. Недостатком
этого решения была потеря эффективности, свойственная интерпретаторам.
Часто встречаются компиляторы, которые генерируют не двоичный код сразу,
а сначала текст ассемблера. Для окончательной трансляции вслед за компилято
ром запускается еще и ассемблер, в силу чего неизбежно увеличивается время
трансляции. Так как эта схема едва ли сулит какие то преимущества, мы не реко
мендуем такой подход.
Более того, языки высокого уровня все чаще используются для программиро
вания микроконтроллеров, применяемых в различных технических устройствах.
Подобные устройства используются прежде всего для сбора данных и автомати
ческого управления машинами. В таких случаях объем рабочей памяти обычно
небольшой и недостаточен для того, чтобы разместить в нем компилятор. И тогда
программное обеспечение генерируется на других компьютерах, способных
к компиляции. Компилятор, создающий код для компьютера, отличного от того,
на котором выполняется компиляция, называется кросс компилятором. Сгене
рированный код в этом случае передается, или загружается, по линии передачи
данных.
В следующих главах мы сосредоточимся на теоретических основах проектиро
вания компиляторов, а затем – на разработке настоящего однопроходного компи
лятора.
Глава 2
Язык и синтаксис
2.1. Упражнения ......................... 24
20
Язык и синтаксис
Каждый язык обладает структурой, называемой грамматикой, или синтаксисом.
Например, правильное предложение (в английском языке – Прим. перев.) всегда
состоит из подлежащего и следующего за ним сказуемого. Под правильным здесь
понимается правильно составленное предложение. Это можно описать следующей
формулой:
=
.
Если мы добавим к этой формуле еще две
="
="
"|"
"|"
".
".
то с их помощью получим ровно четыре возможных предложения, а именно:
где символ | должен произноситься как или. Мы назовем эти формулы синтакси
ческими правилами, продукциями или просто синтаксическими уравнениями.
и
– это синтаксические классы. Краткая запись этих формул
пренебрегает смыслом идентификаторов:
S = AB.
A = "a" | "b".
B = "c" | "d".
L = {ac, ad, bc, bd}
Мы будем использовать такую сокращенную запись в последующих кратких
примерах. Множество L предложений, которые могут быть сгенерированы этим
способом, то есть повторяющейся заменой левых частей уравнений правыми, на
зывается языком.
Приведенный выше пример, очевидно, определяет язык, состоящий только из
четырех предложений. Обычно язык содержит бесконечно много предложений.
Следующий пример показывает, что бесконечное множество может быть очень
просто определено конечным числом уравнений. Символ ∅ обозначает пустую
последовательность.
S = A.
= "a"
| ∅.
L = {∅, a, aa, aaa, aaaa,...}
Метод, позволяющий выполнять подстановку (здесь "a"A вместо ) бесконеч
ное число раз, называется рекурсией.
Наш третий пример опять основан на применении рекурсии. Но он генерирует
не только предложения, состоящие из произвольной последовательности одного
и того же символа, но и вложенные предложения:
S = A.
L = {b, abc, aabcc, aaabccc,...}
A = "a" A "c" | "b".
Понятно, что таким образом может быть выражена произвольно глубокая вло
женность (здесь – для A), что особенно важно в определении структурированных
языков.
Язык и синтаксис
21
Наш четвертый, и последний, пример показывает структуру выражений. Сим
волы E, T, F и V обозначают выражение, слагаемое, множитель и переменную соот
ветственно.
E = T | "+" T.
T = F | T "*" F.
F = V | "(" E ")".
V = "a" | "b" | "c" | "d".
Из этого примера видно, что синтаксис не только определяет множество пред
ложений языка, но и наделяет их структурой. Синтаксис раскладывает предложе
ния на составляющие, как показано в примере на рис. 2.1. Графические представ
ления называются структурными деревьями, или синтаксическими деревьями.
Рис. 2.1. Структура выражений
Сформулируем представленные выше понятия более строго.
Язык (порождающая язык грамматика – Прим. перев.) определяется следую
щим образом:
1. Множество терминальных символов. Это символы, которые появляются
в предложениях. Говорят, что они терминальные, потому что не могут быть
заменены никакими другими символами. Процесс подстановки заканчива
ется терминальными символами. В нашем первом примере это множество
состоит из элементов a, b, c и d. Это множество также называется словарем.
2. Множество нетерминальных символов. Они обозначают синтаксические
классы и могут замещаться в результате подстановок. В нашем первом при
мере это множество состоит из элементов S, A и B.
3. Множество синтаксических уравнений (также называемых продукциями).
Они определяют возможные подстановки нетерминальных символов.
Уравнение задается для каждого нетерминального символа.
22
Язык и синтаксис
4. Начальный символ. Это нетерминальный символ, обозначаемый в примерах
как S.
Таким образом, язык – это множество цепочек терминальных символов, кото
рые могут быть выведены из начального символа многократным применением
синтаксических уравнений, то есть подстановок.
Желательно также строго и точно определить нотацию, в которой записывают
ся синтаксические уравнения. Пусть нетерминальные символы будут идентифи
каторами, как в языках программирования, то есть последовательностями букв
(и, возможно, цифр), например expression, term. Пусть терминальные символы
будут последовательностями символов, заключенными в кавычки (строками), на
пример "=", "|". Для определения структуры этих уравнений удобно воспользо
ваться тем же самым инструментом, который только что был определен:
syntax
production
expression
term
factor
identifier
string
stringhead
letter
digit
=
=
=
=
=
=
=
=
=
=
production syntax | ∅.
identifier "=" expression ".".
term | expression "|" term.
factor | term factor.
identifier | string.
letter | identifier letter | identifier digit.
stringhead""".
""" | stringhead character.
"A" | ... | "Z".
"0" | ... | "9".
Эта нотация почти в таком же виде была введена в 1960 году Дж. Бэкусом и П.
Науром для формального описания синтаксиса языка Алгол 60 и поэтому полу
чила название формы Бэкуса–Наура (БНФ) [12]. Как показывает пример, ис
пользование рекурсии для простых повторений несколько мешает их восприя
тию. Поэтому мы расширим эту нотацию двумя конструкциями, выражающими
повторение и необязательность. Кроме этого, разрешим выражения заключать в
скобки. Таким образом, вводится расширение БНФ, называемое РБНФ [17], ко
торым мы снова воспользуемся для его же точного определения:
syntax
production
expression
term
factor
=
=
=
=
=
identifier
string
letter
digit
=
=
=
=
{ production }.
identifier "=" expression ".".
term {"|" term }.
factor { factor }.
identifier |
| "(" expression ")" | "[" expression "]"
| "{" expression ”}".
letter { letter | digit }.
""" {character} """.
"A" |... | "Z".
"0" |... | "9".
Множитель вида {x} равнозначен произвольно длинной последовательности x,
включая пустую последовательность. Продукция вида
= AB | ∅.
Язык и синтаксис
23
теперь записывается короче: = {B}. Множитель вида [x] равнозначен «x, или нич
то», то есть выражает необязательность. Следовательно, потребность в специаль
ном символе ∅ для пустой цепочки исчезает.
Идея определять языки и их грамматику с математической точностью восхо
дит к Н. Хомскому (N. Chomsky). Однако стало ясно, что предложенная простая
схема правил подстановки недостаточна для представления всей сложности раз
говорных языков. Положение не изменилось даже после того, как формализм был
значительно расширен. Но зато эта работа оказалась чрезвычайно плодотворной
для теории языков программирования и математических формализмов. С его по
мощью Алгол 60 стал первым языком программирования, который был определен
точно и формально. Мимоходом подчеркнем, что эта точность относилась только
к синтаксису, но не к семантике.
Термин язык программирования также обязан формализму Хомского, по
скольку языки программирования, оказывается, обладают структурой, подобной
структуре разговорных языков. Но мы уверены, что этот термин, в общем, доволь
но неудачен, поскольку на языке программирования нельзя разговаривать, и по
этому он не язык в прямом смысле слова. Более подходящими были бы термины
формализм или формальная нотация.
Некоторые удивляются, почему точному определению предложений языка
должно придаваться такое большое значение; ведь в действительности это не так.
Тем не менее очень важно, чтобы предложение было правильно составлено. Хотя
и в этом случае предложение может потребовать уточнения. Но, в конце концов,
структура предложения (правильно составленного) важна потому, что является
инструментом понимания его смысла. Благодаря синтаксической структуре от
дельные составные части предложения и их смысл могут распознаваться незави
симо, а все вместе они придают смысл целому.
Давайте проиллюстрируем этот момент, используя следующий простой пример
выражения со сложением. Обозначим идентификатором E выражение, а N – число:
E = N | E "+" E.
N = "1" | "2" | "3" | "4".
Очевидно, "4 + 2 + 1" – правильное выражение, которое можно получить не
сколькими способами, каждому из которых соответствует своя структура, как по
казано на рис. 2.2.
Обе структуры могут быть представлены скобочными выражениями, а имен
но (4 + 2) + 1 и 4 + (2 + 1) соответственно. К счастью, благодаря ассоциативности
сложения результат в обоих случаях один и тот же и равен 7. Однако это не всегда
так. Если в нашем примере знак сложения заменить на знак вычитания, то две
структуры дадут разные результаты: (4 – 2) – 1 = 1, 4 – (2 – 1) = 3.
Пример иллюстрирует два факта:
1. Интерпретация предложений всегда основывается на распознавании син
таксической структуры.
2. Каждое предложение должно иметь единственную структуру, для того что
бы не быть двусмысленным.
24
Язык и синтаксис
Рис. 2.2. Разные структурные деревья для одного и того же выражения
Если второе требование не выполняется, может возникнуть двусмысленное
предложение. Этим можно обогатить разговорный язык; однако двусмысленные
языки программирования просто бесполезны.
Мы называем синтаксический класс неоднозначным, если ему можно соотнес
ти несколько структур. Язык неоднозначен, если в него входит по крайней мере
один неоднозначный синтаксический класс (конструкция).
2.1. Упражнения
2.1. Сообщение об Алголе 60 содержит следующий синтаксис (приведенный
к РБНФ):
primary = unsignedNumber | variable | "(" arithmeticExpression ")" |... .
factor
= primary | factor "↑" primary.
term
= factor | term ("*" | "/" | "÷") factor.
simpleArithmeticExpression = term | ("+" | "–") term
| simpleArithmeticExpression ("+" | "–") term.
arithmeticExpression = simpleArithmeticExpression |
"IF" BooleanExpression "THEN" simpleArithmeticExpression
"ELSE" arithmeticExpression.
relationalOperator = "=" | "≠"| "≤" | "<" | "≥" | ">".
relation
= arithmeticExpression relationalOperator arithmeticExpression.
BooleanPrimary = logicalValue | variable | relation
| "("BooleanExpression")" |... .
BooleanSecondary = BooleanPrimary | "¬" BooleanPrimary.
BooleanFactor
= BooleanSecondary | BooleanFactor "∩" BooleanSecondary.
BooleanTerm
= BooleanFactor | BooleanTerm "⊆" BooleanFactor.
implication
= BooleanTerm | implication "=>" BooleanTerm.
simpleBoolean
= implication | simpleBoolean "≡" implication.
BooleanExpression = simpleBoolean |
"IF" BooleanExpression "THEN" simpleBoolean "ELSE" BooleanExpression.
Определите синтаксические деревья следующих выражений, в которых буквы
обозначают переменные:
Упражнения
25
x+y+z
x*y+z
x+y*z
(x – y) * (x + y)
–x ÷ y
a+b<c+d
a + b < c V d ≠ e ∩ ¬f => g>h ? i *j = k ↑ L V m–n + p ≤ q
2.2. Следующие продукции также являются частью первоначального опреде
ления Алгола 60. Они содержат двусмысленности, которые были устранены в
Пересмотренном сообщении.
forListElement = arithmeticExpression
| arithmeticExpression "STEP" arithmeticExpression "UNTIL" arithmeticExpression
| arithmeticExpression "WHILE" BooleanExpression .
forList
= forListElement | forList "," forListElement .
forClause
= "FOR" variable ":=" forList "DO" .
forStatement
= forClause statement .
compoundTail = statement "END" | statement ";" compoundTail .
compoundStatement = "BEGIN" compoundTail .
unconditionalStatement = basicStatement | forStatement | compoundStatement|...
ifStatement
= "IF" BooleanExpression "THEN" unconditionalStatement.
conditionalStatement = ifStatement | ifStatement "ELSE" statement .
statement
= unconditionalStatement | conditionalStatement .
Найдите по крайней мере две различные структуры для следующих выраже
ний и операторов. Пусть A и B обозначают «простые операторы».
IF a THEN b ELSE c = d
IF a THEN IF b THEN A ELSE B
IF a THEN FOR ... DO IF b THEN A ELSE B
Предложите альтернативный однозначный синтаксис.
2.3. Рассмотрите следующие конструкции и попытайтесь разобраться, какие
из них являются корректными для Алгола, а какие – для Оберона (см. приложе
ние 2):
a+b=c+d
a * –b
a<b & c<d
Вычислите следующие выражения:
5 * 13 DIV 4 =
13 DIV 5 *4 =
Глава 3
Регулярные языки
3.1. Упражнение ......................... 32
28
Регулярные языки
Синтаксические уравнения в том виде, как они определены в РБНФ, генерируют
контекстно свободные языки. Термин «контекстно свободный» принадлежит
Хомскому и происходит из того факта, что замена символа слева от знака = це
почкой, порожденной выражением справа от знака =, возможна всегда, независи
мо от контекста этого символа внутри предложения. Оказывается, что эта свобода
контекста (по Хомскому) очень подходит и даже желательна для языков програм
мирования. Но контекстная зависимость в ином смысле все таки необходима. Мы
вернемся к этой теме в главе 8.
Здесь мы прежде всего хотим исследовать подкласс, а не контекстно свобод
ные языки вообще. Этот подкласс, известный как регулярные языки, играет суще
ственную роль в области языков программирования. В сущности, это контекстно
свободные языки, синтаксис которых не содержит иной рекурсии, кроме
спецификации повторения. Так как в РБНФ повторение определено непосред
ственно и без использования рекурсии, можно дать простое определение:
Язык регулярен, если его синтаксис может быть выражен единствен
ным выражением РБНФ.
Требование достаточности единственного уравнения подразумевает, что вы
ражение состоит только из терминальных символов. Такое выражение называют
регулярным выражением.
Видимо, достаточно двух кратких примеров регулярных языков. Первый оп
ределяет идентификаторы, поскольку они встречаются в большинстве языков,
а второй – целые числа в десятичной записи. Для краткости мы используем нетер
минальные символы letter и digit. Их можно исключить подстановкой, в результа
те чего и identifier, и integer будут регулярными выражениями.
identifier = letter {letter | digit}.
integer = digit {digit}.
letter = "A" | "B" | ... | "Z"
digit = "0" |"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9".
Причина нашего интереса к регулярным языкам заключается в том, что про
граммы для распознавания регулярных выражений очень просты и эффективны.
Под «распознаванием» мы подразумеваем определение структуры предложения
и, следовательно, его правильность, то есть принадлежность языку. Распознава
ние выражения называется синтаксическим анализом.
Для распознавания регулярных выражений необходимо и достаточно иметь
конечный автомат, также называемый машиной состояний (state machine). На
каждом шаге машина состояний читает следующий символ и меняет свое состоя
ние. Следующее состояние определяется исключительно предыдущим состояни
ем и очередным прочитанным символом. Если очередное состояние уникально, то
машина состояний является детерминированной, иначе недетерминированной.
Если машина состояний реализована программно, то состояние машины опреде
ляется текущей точкой выполнения программы.
Анализирующая программа может быть выведена непосредственно из опреде
ления синтаксиса в РБНФ. Для каждой РБНФ конструкции K существует прави
Регулярные языки
29
ло перевода, которое порождает фрагмент программы Pr(K). Правила перевода из
РБНФ в текст программы показаны ниже. В них sym – глобальная переменная,
представляющая собой последний символ исходного текста, прочитанный проце
дурой next. Процедура error завершает выполнение программы, сигнализируя
о том, что последовательность символов не принадлежит языку.
K
"x"
(exp)
[exp]
{exp}
fac0 fac1 ... facN
term0|term1|...|termN
Pr(K)
IF sym = "x" THEN next ELSE error END
Pr(exp)
IF sym IN first(exp) THEN Pr(exp) END
WHILE sym IN first(exp) DO Pr(exp) END
Pr(fac0); Pr(fac1); ... Pr(facN);
CASE sym OF
first(term0): Pr(term0);
| first(term1): Pr(term1);
...
| first(termN): Pr(termN);
END
K ) содержит все символы, с которых могут начинаться пред
Множество first(K
ложения, полученные из конструкции K . Это множество начальных символов кон
струкции K . Для двух примеров – идентификаторов и целых чисел – они таковы:
first(integer) = digits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
first(identifier) = letters = {"A", "B",..., "Z"}
Однако применение этих простых правил перевода, генерирующих синтакси
ческий анализатор по заданному синтаксису, возможно лишь при условии детер
минированного синтаксиса. Это предварительное условие может быть сформули
ровано более конкретно следующим образом:
K
term0 | terml
fac0 fac1
[exp]
{exp}
Cond(K)
Символы term0 и term1 не должны иметь никаких общих
начальных символов
Если fac0 содержит пустую последовательность, то fac0 и
fac1 не должны иметь никаких общих начальных символов
Множества начальных символов exp и символов, которые
могут следовать за K , не должны пересекаться
Очевидно, эти условия выполняются в примерах для идентификаторов и це
лых чисел, и мы, таким образом, получаем следующие программы для их распоз
навания:
IF s IN letters THEN next ELSE error END;
WHILE sym IN letters + digits DO
CASE sym OF
"A" .. "Z": next
| "0" .. "9": next
END
END
30
Регулярные языки
IF sym IN digits THEN next ELSE error END;
WHILE sym IN digits DO next END
Обычно программу, полученную применением правил перевода, можно упрос
тить путем устранения условий, которые уже явно установлены предыдущими ус
ловиями. Условия sym IN letters и sym IN digits записываются следующим образом:
("A" <= sym) & (sym <= "Z")
("0" <= sym) & (sym <= "9")
Важность регулярных языков для языков программирования следует из того,
что последние обычно определяются в два этапа. На первом этапе их синтаксис
определяется в терминах словаря абстрактных терминальных символов. На вто
ром эти абстрактные символы определяются как цепочки конкретных терминаль
ных символов, таких как ASCII символы. Это второе определение обычно имеет
регулярный синтаксис. Разделение на два этапа обладает тем преимуществом, что
определение абстрактных символов и, таким образом, языка не зависит от конк
ретного представления, то есть от некоторого конкретного набора символов, ис
пользуемого на некотором конкретном оборудовании.
Такое разделение оказывает влияние и на структуру компилятора. Процесс
синтаксического анализа использует процедуру получения следующего абстракт
ного символа, а эта процедура, в свою очередь, распознает конкретные символы –
цепочки из одной или более литер. Эту последнюю процедуру называют лекси
ческим анализатором, а синтаксический анализ на этом втором, более низком
уровне – лексическим анализом. Определение символов, выражаемых литерами,
обычно задается в форме регулярного языка, и поэтому лексический анализатор –
типичная машина состояний.
Суммируем различия между этими двумя уровнями следующим образом:
Процесс
Входной Алгоритм
элемент
Лексический анализ
Литера
Синтаксический анализ Символ
Синтаксис
Лексический анализатор
Регулярный
Синтаксический анализатор Контекстно свободный
В качестве примера приведем лексический анализатор для разбора РБНФ. Его
терминальные символы и их определение в терминах литер таковы:
symbol
= {blank} (identifier | string | "(" | ")" | "[" | "]"
| "{" | "}" | "|" | "=" | ".") .
identifier = letter {letter | digit}.
string
= """ {character} """.
Из этих определений мы выведем процедуру GetSym, которая при каждом вы
зове присваивает глобальной переменной sym числовое значение, представляю
щее следующий прочитанный символ. Если символ является идентификатором
(identifier) или строкой (string), то конкретная цепочка литер присваивается до
полнительной глобальной переменной id. Также отметим, что лексический анали
Регулярные языки
31
затор обычно подразумевает следующее правило о пробелах и концах строк: про
белы и концы строк разделяют цепочки литер и другого назначения не имеют.
Процедура GetSym на Обероне использует следующие объявления:
CONST IdLen = 32;
ident = 0; literal = 2; lparen = 3; lbrak = 4; lbrace = 5; bar = 6;
eql = 7; rparen = 8; rbrak = 9; rbrace= 10; period = 11; other = 12;
TYPE Identifier = ARRAY IdLen OF CHAR;
VAR ch: CHAR;
sym: INTEGER;
id: Identifier;
R: Texts.Reader;
Отметим, что абстрактная операция чтения теперь представлена конкретным
вызовом Texts.Read(R,ch). R – это глобально объявленный объект типа Reader, яв
ляющийся источником исходного текста. Отметим также, что переменная ch дол
жна быть глобальной, потому что по завершении GetSym она может содержать
первую литеру следующего символа. Это нужно иметь в виду при очередном об
ращении к GetSym.
PROCEDURE GetSym;
VAR i: INTEGER;
BEGIN (*
*)
WHILE –R.eot & (ch <= " ") DO Texts.Read(R, ch) END ;
CASE ch OF
"A".. "Z", "a" .. "z":
sym := ident; i := 0;
REPEAT id[i] := ch; INC(i);
Texts.Read(R, ch)
UNTIL (CAP(ch) < "A") OR (CAP(ch) > "Z");
id[i] := OX
| 22X: (*
*)
Texts.Read(R, ch); sym := literal; i := 0;
WHILE (ch # 22X) & (ch > "") DO
id[i] := ch; INC(i); Texts.Read(R, ch)
END;
IF ch <= " " THEN error(l) END ;
id[i] := OX; Texts.Read(R, ch)
| "=": sym := eql; Texts.ReadfR, ch)
| "(" : sym := lparen; Texts.Read(R, ch)
| ")" : sym := rparen; Texts.Read(R, ch)
| "[" : sym := lbrak; Texts.Read(R, ch)
| "]" : sym := rbrak; Texts.Read(R, ch)
| "{" : sym := lbrace; Texts.Read(R, ch)
| "}" : sym := rbrace; Texts.Read(R, ch)
| "|" : sym := bar; Texts.Read(R, ch)
| ".": sym := period; Texts.Read(R, ch)
32
Регулярные языки
ELSE sym := other; Texts.Read(R, ch)
END
END GetSym
3.1. Упражнение
Предложения регулярных языков могут быть распознаны конечными автомата
ми. Они обычно описываются диаграммами переходов. Каждый узел представля
ет состояние, а каждая стрелка – переход из одного состояния в другое. Стрелка
помечена символом, который прочитан непосредственно перед переходом. Рас
смотрите следующие диаграммы и опишите синтаксис соответствующих языков
в РБНФ.
Глава 4
Анализ
контекстно свободных
языков
4.1. Метод рекурсивного
спуска ........................................
4.2. Таблично управляемый
нисходящий синтаксический
анализ .......................................
4.3. Восходящий
синтаксический анализ ..............
4.4. Упражнения .........................
34
38
40
42
34
Анализ контекстно свободных языков
4.1. Метод рекурсивного спуска
Регулярные языки ограничены тем, что не могут выразить вложенные структуры.
Вложенные структуры могут быть выражены только при помощи рекурсии (см.
главу 2).
Поэтому конечного автомата недостаточно для распознавания предложений
контекстно свободных языков. Мы все таки попытаемся получить программу
синтаксического анализатора для третьего примера из главы 2, используя метод
главы 3. Там, где этот метод будет терпеть неудачу (а он должен терпеть неудачу),
лежит ключ для возможного обобщения. Просто удивительно, насколько малыми
оказываются необходимые для этого дополнительные усилия по программирова
нию!
Конструкция
A = " " A " " | "b".
приводит, после полезных упрощений и использования IF вместо CASE, к следую
щему фрагменту программы:
IF sym = " " THEN
next;
IF sym = THEN next ELSE error END;
IF sym = " " THEN next ELSE error END
ELSIF sym = "b" THEN next
ELSE error
END
Здесь мы вслепую обработали нетерминальный символ тем же способом, что
и терминальные символы. Это, конечно, неприемлемо. Цель третьей строки про
граммы заключается в анализе конструкции , а не в чтении символа . Ведь
именно в этом и состоит задача нашей программы. Поэтому простое решение на
шей проблемы – дать программе имя, то есть придать ей вид процедуры, и заме
нить в ней третью строку вызовом этой процедуры. Процедура рекурсивна так
же, как сама конструкция в синтаксисе:
PROCEDURE ;
BEGIN
IF sym = " " THEN
next;
;
IF sym = " " THEN next ELSE error END
ELSIF sym = "b" THEN next
ELSE error
END
END A
Необходимое расширение набора правил перевода крайне простое. Един
ственное дополнительное правило:
Метод рекурсивного спуска
35
Алгоритм разбора выводится для каждого нетерминального символа
и оформляется в виде процедуры, получающей имя этого символа.
Появление такого символа в синтаксисе переводится в вызов соот
ветствующей процедуры.
Внимание: это правило справедливо независимо от того, рекурсивна процеду
ра или нет.
Важно только проверить, что соблюдены условия детерминированности алго
ритма. Это подразумевает среди прочего, что в выражении вида
term0 | term1
символы term0 и term1 не должны иметь никаких общих начальных символов. Это
требование исключает левую рекурсию. Если мы рассмотрим леворекурсивную
продукцию
=
" " | "b" .
то увидим, что требование нарушено только потому, что b – начальный символ
(b IN first(A)), и, следовательно, множества first(A"a") и first("b") пересекаются.
"b" является для них общим элементом.
Простой выход: левая рекурсия может и должна быть заменена повторением.
В примере выше = " "|"b" заменяется на = "b"{" "}.
Другой взгляд на переход от машины состояний к ее обобщению состоит в том,
чтобы представить ее как множество машин состояний, которые ссылаются друг
на друга и на себя. В принципе, единственным новым условием является то, что
состояние вызывающей машины должно восстанавливаться по завершении рабо
ты вызванной машины состояний. Следовательно, это состояние должно сохра
няться при вызове. Так как машины состояний вложенные, то стек является са
мым подходящим для этого типом памяти. В силу чего наше расширение машины
состояний называется магазинным автоматом. Теоретически стек (магазинная
память) должен быть бесконечно глубоким. В этом состоит существенное отличие
конечной машины состояний от бесконечного магазинного автомата.
Общий принцип, который предложен здесь, состоит в следующем: рассматри
вайте распознавание сентенциальной конструкции, которая начинается с началь
ного символа основного синтаксиса, как высшую цель. Если в процессе достиже
ния этой цели, то есть во время анализа продукции, встречается нетерминальный
символ, распознавание соответствующей ему конструкции рассматривается как
подчиненная цель, тогда как достижение вышестоящей цели временно приоста
навливается. Эту стратегию называют целенаправленным синтаксическим анали
зом. Если мы посмотрим на структурное дерево анализируемого предложения, то
увидим, что сначала мы сталкиваемся с вышестоящей по дереву целью (сим
волом), а от нее идем к нижестоящим целям (символам). Этот метод называется
нисходящим разбором [9, 1]. Представленная реализация такой стратегии, осно
ванная на рекурсивных процедурах, известна также как синтаксический анализ
методом рекурсивного спуска.
36
Анализ контекстно свободных языков
В заключение повторим, что решение об очередном выполняемом шаге всегда
принимается на основании только одного очередного входного символа. Синтак
сический анализатор смотрит вперед только на один символ. Предпросмотр
нескольких символов значительно усложнил бы процесс принятия решения и, та
ким образом, замедлил бы его. По этой причине мы ограничимся языками, кото
рые можно анализировать с предпросмотром только одного символа.
В качестве примера демонстрации метода рекурсивного спуска рассмотрим
анализатор для РБНФ, синтаксис которого приведем здесь еще раз:
syntax = {production} .
production = identifier "=" expression ".".
expression = term {"|" term}.
term = factor {factor}.
factor = identifier | string |"(" expression ")"
= | "[" expression '']'' | "{" expression "}".
Применяя данные правила перевода и последующее упрощение, получим сле
дующий анализатор. Он оформлен в виде модуля Оберона:
MODULE EBNF;
IMPORT Viewers, Texts, TextFrames,
b ron;
CONST IdLen = 32;
ident = 0; literal = 2; lparen = 3; lbrak = 4; lbrace = 5;
bar = 6; eql = 7; rparen = 8; rbrak = 9; rbrace = 10;
period = 11;other = 12;
Identifier = ARRAY IdLen OF CHAR;
VAR h: CHAR;
sym: INTEGER;
lastpos: LONGINT;
id: Identifier;
R: Texts.Reader;
W: Texts.Writer;
PROCEDURE err r(n: INTEGER);
VAR pos: LONGINT;
BEGIN pos := Texts.Pos(R);
IF pos > lastpos+4 THEN (*
Texts.WriteString(W, "
"); Texts.Writelnt(W, pos, 6);
Texts.WriteString(W,"
"); Texts.Writelnt(W, n, 4);
lastpos := pos;
Texts.WriteString(W,"
"); Texts.Writelnt(W, sym, 4);
Texts.WriteLn(W); Texts.Append(Oberon.Log, W.buf)
END
END error;
PROCEDURE GetSym;
BEGIN ... (*
.
3*)
END GetSym;
*)
Метод рекурсивного спуска
37
PROCEDURE expression;
PROCEDURE term;
PROCEDURE factor;
BEGIN
IF sym = ident THEN record(T0, id, 1); GetSym
ELSIF sym = litera) THEN record( l, id, ); GetSym
ELSIF sym = lparen THEN
GetSym; expression;
IF sym = rparen THEN GetSym ELSE error(2) END
ELSIF sym = lbrak THEN
GetSym; expression;
IF sym = rbrak THEN GetSym ELSE error(3) END
ELSIF sym = lbrace THEN
GetSym; expression;
IF sym = rbrace THEN GetSym ELSE error(4) END
ELSE error(5)
END
END factor;
BEGIN (*term*) factor;
W ILE sym < bar DO factor END
END term;
BEGIN (*expression*) term;
W ILE sym = bar DO GetSym; term END
END expression;
PROCEDURE production;
BEGIN (*sym = ident*) GetSym;
IF sym = eql THEN GetSym ELSE error(7) END ;
expression;
IF sym = period THEN GetSym ELSE error(8) END
END production;
PROCEDURE syntax;
BEGIN
W ILE sym = ident DO prod ction END
END syntax;
PROCEDURE
mpile*;
BEGIN (*
R
lastpos := ; Texts.Read(R, ch); GetSym; syntax;
Texts.Append(Oberon.Log, W.buf)
END Compile;
BEGIN Texts.OpenWriter(W)
END EBNF.
*)
38
Анализ контекстно свободных языков
4.2. Таблично управляемый нисходящий
синтаксический анализ
Метод рекурсивного спуска – только один из нескольких методов, реализующих
принцип нисходящего синтаксического анализа. Здесь мы представим другой ме
тод – таблично управляемый разбор.
Идея построения общего алгоритма нисходящего синтаксического анализа
с подробным синтаксисом в качестве параметра вполне естественна. Синтаксис
принимает вид структуры данных, которая обычно представлена графом или таб
лицей. Эта структура затем интерпретируется общим синтаксическим анализато
ром. Если она представлена графом, мы можем интерпретировать ее как обход
графа, управляемый анализируемым исходным текстом.
Сначала мы должны определить представление данных в структурном графе.
Мы знаем, что РБНФ содержит две конструкции повторения, а именно цепочку
символов factor и цепочку символов term. Естественно представить их как спис
ки. Каждый элемент структуры данных представляет (терминальный) символ.
Следовательно, каждый элемент должен иметь как минимум двух потомков, на
которые должны быть ссылки. Назовем их next – для очередного символа factor и
alt – для очередного альтернативного символа term. Запишем на языке Оберон
следующие объявления типов данных:
Symbol
SymDesc
=
=
POINTER TO SymDesc;
RECORD alt, next: Symbol END
Теперь сформулируем этот абстрактный тип данных для терминальных и не
терминальных символов, используя механизм расширения типов Оберона [14].
Записи, обозначающие терминальные символы, определяют их посредством до
полнительного атрибута sym:
Terminal
TSDesc
= POINTER TO TSDesc;
= RECORD (SymDesc) sym: INTEGER END
Элементы, представляющие нетерминальные символы, содержат ссылку (ука
затель) на структуру данных, представляющую этот символ. Из практических со
ображений введем косвенную ссылку, когда указатель ссылается на дополнитель
ный элемент заголовок, который, в свою очередь, ссылается на структуру данных.
Заголовок содержит имя структуры, то есть имя нетерминального символа. Стро
го говоря, в этом дополнительном элементе нет необходимости; его полезность
станет понятной позже.
Nonterminal
NTSDesc
Header
HDesc
= POINTER TO NTSDesc;
= RECORD (SymDesc) this: Header END
= POINTER TO HDesc;
= RECORD sym: Symbol; name: ARRAY n OF CHAR END
Для примера возьмем следующий синтаксис простых выражений. На рис. 4.1
изображена соответствующая структура данных в виде графа. Горизонтальные
стрелки – next указатели, вертикальные стрелки – alt указатели.
Таблично управляемый нисходящий синтаксический анализ
39
expression = term {("+" | "–") term}.
term
= factor {("*"|"/") factor}.
factor
= id | "(" expression ")".
Теперь мы имеем возможность записать общий алгоритм разбора в виде конк
ретной процедуры:
PROCEDURE Parsed(hd: Header): BOOLEAN;
VAR x: Symbol; match: BOOLEAN;
BEGIN x := hd.sym; Texts.WriteString(Wr, hd.name);
REPEAT
IF x IS Terminal THEN
IF x(Terminal).sym = sym THEN match := TRUE; GetSym
ELSE match := (x = empty)
END
ELSE match := Parsed(x(Nonterminal).this)
END;
IF match THEN x := x.next ELSE x := x.alt END
UNTIL x = NIL;
RETURN match
END Parsed
Рис. 4.1. Синтаксис как структура данных
40
Анализ контекстно свободных языков
Мы должны иметь в виду следующие замечания.
1. Мы молчаливо полагаем, что символы term всегда определяются в виде
= f0 | f1 | ... | fn
где все символы fk, кроме последнего, начинаются с разных терминальных
символов. Только последний символ fn может начинаться как терминаль
ным, так и нетерминальным символом. При таком условии можно переби
рать альтернативы, выполняя на каждом шаге только одно сравнение.
2. Структура данных может быть получена из синтаксиса (на РБНФ) автома
тически, то есть программой, которая компилирует синтаксис.
3. В приведенной выше процедуре имя каждого распознанного нетерминаль
ного символа является ее выходом. Именно этой цели служит элемент за
головок.
4. Empty – это специальный терминальный символ и элемент, представ
ляющий пустую цепочку. Он служит для пометки выхода из повторений
(циклов).
4.3. Восходящий синтаксический анализ
И рекурсивный спуск, и таблично управляемый разбор, представленные здесь, –
это методы, основанные на принципах нисходящего синтаксического анализа. Их
основная цель – показать, что анализируемый текст выводится из начального
символа. Любые нетерминальные символы, встречающиеся при анализе, счита
ются подцелями. Процесс синтаксического анализа создает синтаксическое дере
во с начальным символом в корне, то есть в направлении сверху вниз.
Однако согласно принципу дополнения можно пойти и в восходящем направ
лении, когда текст читается без преследования конкретной цели. После каждого
шага происходит проверка, соответствует ли прочитанная подцепочка некоторой
сентенциальной конструкции, то есть правой части продукции. Если это так, про
читанная подцепочка заменяется соответствующим нетерминальным символом.
Процесс распознавания снова состоит из последовательности шагов двух различ
ных типов:
1) перенос входного символа в стек (шаг переноса);
2) свертывание цепочки символов в стеке в один нетерминальный символ со
гласно продукции (шаг свертки).
Восходящий синтаксический анализ называют также разбором типа «перенос
свертка». Синтаксические конструкции сначала накапливаются в стеке, а затем
сворачиваются; синтаксическое дерево растет от основания к вершине [8, 1, 7].
И опять мы продемонстрируем этот процесс на примере простых выражений.
Пусть их краткий синтаксис имеет следующий вид:
E = | E "+" T. expression
= F | "*" F. term
F = id | "(" E ")". factor
Восходящий синтаксический анализ
41
и пусть распознается предложение x * (y + z). Для иллюстрации процесса разбора
нераспознанную часть текста будем показывать справа, тогда как слева будет из
начально пустая цепочка распознанных конструкций. Еще левее символами S (пе
ренос) и R (свертка) обозначен тип применяемого шага:
S
R
R
S
S
S
R
R
R
S
S
R
R
R
S
R
R
R
x
F
T
T*
T*(
T*(y
T*(F
*(
*(
*( +
*( + z
*( + F
*( +
*(
*( )
T*F
T
E
x*
*
*
*
(y
(y
(y
(y
(y
y
+
+
+
+
+
+
+
+
+
+
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
)
)
)
)
В итоге исходный текст сворачивается в начальный символ E, который здесь
лучше назвать конечным. Как упомянуто ранее, вспомогательный левый накопи
тель является стеком.
Для сравнения приведем процесс нисходящего разбора того же предложения.
Два типа шагов обозначены (сопоставление) и P (порождение, развертка). На
чальный символ – E.
*F
F*F
id * F
id * F
id * F
( )
)
+
+
F+
id +
+
*
*
*
x*
*
*
)
)
)
)
)
)
F)
id)
)
(
(
(
(y
(
(
(
(y
y
y
y
y
y
+
+
+
+
+
+
+
+
+
+
+
+
+
+
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
z)
)
42
Анализ контекстно свободных языков
Очевидно, при восходящем методе читаемая цепочка символов всегда свора
чивается с ее правой стороны, тогда как при нисходящем методе разворачивается
всегда самый левый нетерминал. Согласно Д. Кнуту, восходящий метод по этой
причине называют LR анализом, а нисходящий – LL анализом. Первая буква L
значит, что текст читается слева направо. Обычно к этим обозначениям добавля
ют параметр k (LL(k), LR(k)). Он задает длину предпросмотра. Мы будем всегда
считать k = 1, если не оговаривается иное.
Но давайте все таки вернемся к восходящему принципу. Здесь существует се
рьезная проблема определения, какой тип шага должен быть следующим, а в слу
чае свертки – сколько символов в стеке должно быть захвачено этим шагом. На
этот вопрос дать ответ непросто. Скажем только: чтобы обеспечить эффектив
ность процесса разбора, информация, на основании которой принимаются реше
ния, должна быть заранее собрана и представлена в надлежащем виде. Восходя
щие синтаксические анализаторы всегда используют таблицы, то есть данные,
структурно аналогичные данным таблично управляемого нисходящего синтак
сического анализатора, представленного выше. Но, помимо синтаксиса в виде
структуры данных, им нужны дополнительные таблицы для эффективного опре
деления следующего шага разбора. По этой причине восходящий синтаксический
анализ в целом более запутанный и сложный, чем нисходящий.
Существуют различные алгоритмы LR анализа. Они налагают различные ог
раничения на обрабатываемый синтаксис. Чем слабее эти ограничения, тем слож
нее процесс синтаксического анализа. Упомянем здесь, не вдаваясь в их подроб
ности, методы SLR [2] и LALR [10].
4.4. Упражнения
4.1. Алгол 60 содержит многократные присваивания вида vl := v2 := ... vn := e,
которые определяются следующим синтаксисом:
assignment
leftpartlist
leftpart
expression
variable
=
=
=
=
=
leftpartlist expression.
leftpart | leftpartlist leftpart.
variable ":=".
variable | expression "+" variable.
ident | ident"[" expression "]".
Какой длины должен быть предпросмотр для выполнения грамматического
разбора этого синтаксиса с помощью нисходящего принципа? Предложите аль
тернативный синтаксис для многократного присваивания, требующий предпрос
мотра только одного символа.
4.2. Определите множества символов FIRST и FOLLOW для конструкций
РБНФ production, expression, term и factor. Используя эти множества, проверь
те, что РБНФ является детерминированной.
syntax
= {production}.
production = id "=" expression ".".
expression = term {"|" term}.
Упражнения
term
factor
id
string
43
= factor {factor}.
= id string |"(" expression ")" "[" expression "]"
|"{" expression "}".
= lette r {letter | digit}.
= """ {character} """.
4.3. Напишите синтаксический анализатор для РБНФ и расширьте его коман
дами генерации структуры данных (для таблично управляемого синтаксического
анализа), соответствующей читаемому синтаксису.
Глава 5
Атрибутные грамматики
и семантики
5.1. Правила типов .....................
5.2. Правила вычислений ...........
5.3. Правила трансляции ...........
5.4. Упражнение .........................
46
47
48
49
46
Атрибутные грамматики и семантики
В атрибутных грамматиках с отдельными конструкциями, то есть с нетерминаль
ными символами, связываются определенные атрибуты. Символы параметризу
ются, и представляют целые классы вариантов. Это служит упрощению синтак
сиса, а на практике способствует превращению синтаксического анализатора
в реальный транслятор [13]. Процесс трансляции характеризуется сопоставлени
ем вывода (возможно, пустого) каждой распознанной сентенциальной конструк
ции. Каждое синтаксическое уравнение (продукция) сопровождается дополни
тельными правилами, определяющими отношение между значениями атрибутов
символов правой части, нетерминала левой части и правила вывода. Предлагаем
три применения атрибутов и атрибутных правил.
5.1. Правила типов
В качестве простого примера рассмотрим язык, имеющий несколько типов дан
ных. Вместо определения отдельных синтаксических правил для выражений каж
дого типа (как было сделано в Алголе 60) мы определим выражение только один
раз и свяжем с каждой входящей в него конструкцией атрибут типа данных Т.
Например, если выражение типа Т обозначить exp(T), то есть exp со значением
атрибута T, то правила совместимости типов можно рассматривать как дополне
ния к отдельным синтаксическим уравнениям. Требование о том, что оба операн
да и результат сложения и вычитания должны быть одного типа, может быть зада
но такими дополнительными атрибутными правилами:
Синтаксис
Атрибутное правило Контекстное условие
exp(T0) = term(T1)
T0 := T1
| exp(T1) "+" term(T2) T0 := T1
| exp(T1) "–" term(T2). T0 := T1
T1 = T2
T1 = T2
Если же в смешанных выражениях допускаются операнды типов INTEGER и
REAL, правила смягчаются, но при этом становятся более сложными:
T0 := if (T1 = INTEGER) & (T2 = INTEGER) then INTEGER else REAL,
T1 = INTEGER or T1 = REAL
T2 = INTEGER or T2 = REAL
На самом деле правила совместимости типов тоже статические в том смысле,
что они могут быть проверены без выполнения программы. Поэтому их отделение
от чисто синтаксических правил оказывается совершенно необоснованным, зато
их интеграция в синтаксис в виде атрибутных правил вполне оправдана. Однако
заметим, что атрибутные грамматики получают новый аспект, если возможные
значения атрибута (здесь – тип данных) и их количество заранее неизвестны.
Если синтаксическое уравнение содержит повторение, то было бы неплохо
выразить его в атрибутных правилах с помощью рекурсии. А при наличии вариан
тов каждый из них лучше выразить отдельно. Это показано в следующем примере,
где два правила для выражений
Правила вычислений
exp(T0) = term(T1) {"+" term(T2)}.
47
exp(T0) = ["–"] term(T1).
разбиты на пары правил, а именно
exp(T0) = term(T1) |
exp(T0) = exp(T1) "+" term(T2).
exp(T0)= term(T1) |
exp(T0)= "–" term(T1).
Связанные с продукцией правила типов срабатывают всякий раз, когда соот
ветствующая продукции конструкция распознана. В случае синтаксического ана
лиза методом рекурсивного спуска эта связь реализуется очень просто: операто
ры, реализующие атрибутные правила, просто вставляются между операторами
программы синтаксического анализа, а атрибуты передаются в качестве парамет
ров в процедуры синтаксических конструкций (нетерминальных символов) ана
лизатора. Первым примером демонстрации этого процесса может быть процедура
распознавания выражений, которая берется за основу:
PROCEDURE expression;
BEGIN term;
WHILE (sym = "+") OR (sym = "–") DO
GetSym; term
END
END expression
и расширяется включением в нее атрибутных правил типа:
PROCEDURE expression(VAR typ0: Type);
VAR typ1,typ2: Type;
BEGIN term(typ1);
WHILE (sym = "+") OR (sym = "–") DO
GetSym; term(typ2);
typ1 := ResType(typ1, typ2)
END;
typ0 := typ1
END expression
5.2. Правила вычислений
В качестве второго примера мы рассмотрим язык, состоящий из выражений, в ко
торых операнды представлены числами. Это кратчайший путь к расширению син
таксического анализатора до программы, которая не только распознает выраже
ния, но одновременно вычисляет их. С каждой конструкцией свяжем ее значение
в виде атрибута val. По аналогии с правилами совместимости типов из предыду
щего раздела теперь мы должны реализовать обработку правил вычисления
в процессе разбора. Таким образом, мы неявно уже ввели нотацию для семантик.
Синтаксис
Атрибутное правило (семантика)
exp(v0)
v0 := v1
v0 := v1 + v2
v0 := v1 – v2
= term(v1) |
exp(v1) "+" term(v2) |
exp(v1) "–" term(v2).
48
Атрибутные грамматики и семантики
term(v0)
= factor(v1) |
term(v1) "*" factor(v2)|
term(v1) "/" factor(v2).
factor(v0) = number(v1) |
"(" exp(v1) ")".
v0
v0
v0
v0
v0
:=
:=
:=
:=
:=
v1
v1 * v2
v1 / v2
v1
v1
Здесь атрибут – это вычисленное числовое значение распознанной конструк
ции. Необходимое расширение соответствующей процедуры синтаксического
анализа приводит к следующей процедуре для выражений:
PROCEDURE expression(VAR val0: INTEGER);
VAR val1, val2: INTEGER; op: CHAR;
BEGIN term(val1);
WHILE (sym = "+") OR (sym = "–") DO
op : = sym; GetSym; term(val2);
IF op = "+" THEN val1 : = val1 + val2 ELSE val1 := val1 – val2 END
END; val0:=val1
END expression
5.3. Правила трансляции
Третий пример применения атрибутных грамматик иллюстрирует основную
функцию компилятора. Здесь дополнительные, связанные с продукцией правила
уже не управляют атрибутами символов, а определяют выход (код), выдаваемый
продукцией в процессе разбора. Генерация выхода может рассматриваться как
побочный эффект синтаксического разбора. Как правило, выход – это последова
тельностью команд. В этом примере команды заменены абстрактными символа
ми, а их вывод осуществляется оператором put.
Синтаксис
Выходное правило (семантика)
exp
–
put("+")
put("–")
–
put("*")
put("/")
put(number)
–
= term
| exp "+" term
| exp "–" term.
term = factor
| term "*" factor
| term "/" factor.
factor = number
| "(" exp(v1) ")".
Легко убедиться, что последовательность символов на выходе есть постфикс
ная запись анализируемого выражения. Синтаксический анализатор теперь рас
ширен до транслятора.
Инфиксная запись
Постфиксная запись
2+3
2*3+4
2+3*4
(5–4) * (3+2)
2
2
2
5
3
3
3
4
+
*4+
4*+
–32+*
Упражнение
49
Процедура разбора и трансляции выражений имеет следующий вид:
PROCEDURE expression;
VAR op: CHAR;
BEGIN term;
WHILE (sym = "+") OR (sym = "–") DO
op := sym; GetSym; term; put(op)
END
END expression
При использовании таблично управляемого синтаксического анализатора ат
рибутные правила могут быть также легко включены в синтаксические таблицы.
И если в этих таблицах содержатся также правила вычислений и правила транс
ляции, возникает соблазн говорить о формальном определении языка. Общий
таблично управляемый синтаксический анализатор перерастает в общий таблич
но управляемый транслятор. Однако это пока остается утопией, хотя сама идея
восходит к 1960 м годам. Схематично она представлена на рис. 5.1.
Рис. 5.1. Схема общего параметрического транслятора
В конечном счете основная идея любого языка состоит в том, что он должен
служить средством общения. Это значит, что партнеры должны использовать и
понимать один и тот же язык. Поэтому проталкивание простоты изменения и
расширения языка может сыграть скорее негативную роль. Тем не менее стало
общепринятым строить трансляторы, использующие таблично управляемые
синтаксические анализаторы, и строить синтаксические таблицы автоматически
с помощью инструментальных средств. Семантика выражается процедурами, чьи
вызовы также автоматически интегрируются в синтаксический анализатор. Та
ким образом, трансляторы становятся не только более громоздкими и менее эф
фективными, чем хотелось бы, но также и гораздо менее прозрачными. Последнее
остается одним из наших главных интересов, и поэтому мы не будем далее следо
вать этим курсом.
5.4. Упражнение
5.1. Расширить программу синтаксического анализа текстов РБНФ таким обра
зом, чтобы она генерировала (1) список терминальных символов, (2) список не
терминальных символов и (3) множества начальных (first) и последующих
(follow) символов для каждого нетерминального символа. Затем на базе этих мно
жеств программа должна определить, может ли данный синтаксис анализиро
50
Атрибутные грамматики и семантики
ваться нисходящим способом с предпросмотром одного символа. И если это не
так, программа должна показать в удобном виде все противоречащие продукции.
Подсказка: используйте алгоритм Уоршелла (R.W. Floyd, Algorithm 96, Comm.
ACM, June 1962).
TYPE matrix = ARRAY [1..n],[1..n] OF BOOLEAN;
PROCEDURE ancestor(VAR m: matrix; n: INTEGER);
(*
m[i,j]
TRUE,
(*
m[i,j]
TRUE,
i–
VAR i, j, k: INTEGER;
BEGIN
FOR i := 1 TO n DO
FOR j := 1 TO n DO
IF m[j, i] THEN
FOR k := 1 TO n DO
IF m[i, k] THEN m[j, k] := TRUE END
END
END
END
END
END ancestor
i–
j *)
j.
Можно допустить, что количество терминальных и нетерминальных символов
анализируемых языков не превышает заданный предел, например, 32.
Глава 6
Язык программирования
Оберон 0
6.1. Упражнение ......................... 54
52
Язык программирования Оберон 0
Чтобы избежать потерь в общности и абстрактных теориях, мы построим вполне
конкретный компилятор и объясним различные проблемы, которые возникают
при его проектировании. Для этого мы должны предложить определенный исход
ный язык.
Чтобы остаться в рамках вводного учебного курса, мы, с одной стороны, долж
ны придерживаться достаточной простоты как компилятора, так и самого языка.
Но, с другой – мы хотим охватить как можно больше фундаментальных конст
рукций языков и способов компиляции. Из этих соображений вытекает условие
выбора языка: он должен быть простым, но достаточно выразительным. Мы вы
брали подмножество языка Оберон [14], который вобрал в себя основные черты
своих предков Модула 2 [18] и Паскаль [15]. Можно сказать, что Оберон – это
последний отпрыск в традициях Алгола 60 [12]. Наше подмножество называется
Оберон 0, и этот выбор достаточно обоснован для освоения основ теории и прак
тики современных методов программирования.
Оберон 0 довольно хорошо проработан в отношении программной структуры.
Элементарный оператор – это присваивание. Составные операторы охватывают
понятия последовательности операторов, условного и повторного выполнения,
представленных в виде традиционных операторов IF и WHILE. Оберон 0 также
содержит важное понятие подпрограммы, представленное объявлением процеду
ры и вызовом процедуры. Его мощь опирается в основном на возможности пара
метризованных процедур. Мы различаем в Обероне параметры значения и пара
метры переменные.
Однако в отношении типов данных Оберон 0 довольно скромен. Единствен
ные элементарные типы данных – целые числа и логические значения, обозначае
мые INTEGER и BOOLEAN. Таким образом, можно объявлять целочисленные
константы и переменные, а также строить выражения с арифметическими опера
циями. Сравнения выражений дают логические значения, которые могут быть
подвергнуты логическим операциям.
Доступные структуры данных – массив и запись. Они могут быть произвольно
вложенными. Однако указатели исключены.
Процедуры представляют функциональные блоки операторов. Поэтому нуж
но увязать понятие локализации имен с описанием процедуры. Оберон 0 предос
тавляет возможность объявлять идентификаторы в процедуре так, что они дос
тупны (видимы) только в пределах процедуры.
Этот очень краткий обзор Оберона 0 должен прежде всего предоставить чита
телю сведения, необходимые для понимания следующего синтаксиса, определен
ного в терминах РБНФ.
ident
integer
= letter {letter | digit}.
= digit {digit}.
selector
number
factor
term
=
=
=
=
{"." ident | "[" expression "]"}.
integer.
ident selector | number | "(" expression ")" | "~" factor.
factor {("*" | "DIV" | "MOD" | "&") factor}.
Язык программирования Оберон 0
SimpleExpression
expression
assignment
ActualParameters
ProcedureCall
IfStatement
53
= ["+"|"–"] term {("+"|"–" | "OR") term}.
= SimpleExpression
[("=" | "#" | "<" | "<=" | ">" | ">=") SimpleExpression].
=
=
=
=
=
=
WhileStatement
=
statement
=
=
StatementSequence =
IdentList
=
ArrayType
=
FieldList
=
RecordType
=
type
=
FPSection
=
FormalParameters
=
ProcedureHeading
=
ProcedureBody
=
ProcedureDeclaration =
declarations
=
=
=
=
module
=
=
ident selector ":=" expression.
"(" [expression {"," expression}] ")" .
ident selector [ActualParameters].
"IF" expression "THEN" StatementSequence
{"ELSIF" expression "THEN" StatementSequence}
["ELSE" StatementSequence] "END".
"WHILE" expression "DO" StatementSequence "END".
[assignment | ProcedureCall | IfStatement |
WhileStatement].
statement {";" statement}.
ident {"," ident}.
"ARRAY" expression "OF" type.
[IdentList ":" type].
"RECORD" FieldList {";" FieldList} "END".
ident | ArrayType | RecordType.
["VAR"] IdentList ":" type.
"(" [FPSection {";" FPSection}] ")".
"PROCEDURE" ident [FormalParameters].
declarations ["BEGIN" StatementSequence] "END" ident.
ProcedureHeading ";" ProcedureBody.
["CONST" {ident "=" expression ";"}]
["TYPE" {ident "=" type ";"}]
["VAR" {IdentList ":" type ";"}]
{ProcedureDeclaration ";"}.
"MODULE" ident ";" declarations
["BEGIN" StatementSequence] "END" ident "." .
Следующий пример модуля поможет читателю почувствовать характер языка.
Модуль содержит различные, хорошо известные учебные процедуры, названия
которых очевидны.
MODULE Sample;
PROCEDURE Multiply;
VAR x, y, z: INTEGER;
BEGIN Read(x); Read(y); z := 0;
WHILE x > 0 DO
IF x MOD 2 = 1 THEN z := z + y END ;
y := 2*y; x := x DIV 2
END ;
Write(x); Write(y); Write(z); WriteLn
END Multiply;
PROCEDURE Divide;
VAR x, y, r, q, w: INTEGER;
BEGIN Read(x); Read(y); r := x; q := 0; w := y;
54
Язык программирования Оберон 0
WHILE w <= r DO w := 2*w END ;
WHILE w > y DO
q := 2*q; w := w DIV 2;
IF w <= r THEN r := r – w; q := q + 1 END
END ;
Write(x); Write(y); Write(q); Write(r); WriteLn
END Divide;
PROCEDURE BinSearch;
VAR i, j, k, n, x: INTEGER;
a: ARRAY 32 OF INTEGER;
BEGIN Read(n); k := 0;
WHILE k < n DO Read(a[k]); k := k + 1 END ;
Read(x); i := 0; j := n;
WHILE i < j DO
k := (i+j) DIV 2;
IF x < a[k] THEN j := k ELSE i := k+1 END
END ;
Write(i); Write(j); Write(a[j]); WriteLn
END BinSearch;
END Sample.
6.1. Упражнение
6.1. Для компьютера, описанного в главе 9, определите машинный код, генерируе
мый программой, приведенной в конце этой же главы.
Глава 7
Синтаксический анализатор
для Оберона 0
7.1. Лексический анализатор .....
7.2. Синтаксический
анализатор ................................
7.3. Устранение
синтаксических ошибок .............
7.4. Упражнения .........................
56
57
59
64
56
Синтаксический анализатор для Оберона 0
7.1. Лексический анализатор
Прежде чем начать разработку синтаксического анализатора, мы остановимся на
проектировании его лексического анализатора. Лексический анализатор должен
распознавать терминальные символы в исходном тексте. Сначала приведем его
словарь:
*
=
OF
END
ARRAY
DIV
#
THEN
ELSE
MOD
<
DO
ELSIF
RECORD
&
<=
(
IF
CONST
+
>
[
WHILE
TYPE
–
>=
~
OR
.
:=
VAR
PROCEDURE
,
;
:
)
]
BEGIN MODULE
Слова, записанные заглавными буквами, представляют уникальные терми
нальные символы, и их называют зарезервированными словами. Они должны
быть распознаны лексическим анализатором и поэтому не могут использоваться
как идентификаторы. Помимо перечисленных символов, терминальными счита
ются также идентификаторы и числа. Поэтому лексический анализатор отвечает
также за распознавание идентификаторов и чисел.
Лексический анализатор целесообразно описать как модуль. Действительно,
он – классический пример использования понятия модуля. Он позволяет скрыть
от клиента анализатора определенные детали и сделать доступными (экспорти
ровать) только те свойства, которые необходимы клиенту. Экспортируемые свой
ства образуют определение интерфейса модуля.
DEFINITION OSS; (*
*)
IMPORT Texts;
CONST IdLen = 16;
(*
*) null = 0;
times = 1; div = 3; mod = 4; and = 5; plus = 6; minus = 7; or = 8;
eql = 9; neq = 10; lss = 11; geq = 12; leq = 13; gtr = 14;
period = 18; comma = 19; colon = 20; rparen = 22; rbrak = 23;
of = 25; then = 26; do = 27;
lparen = 29; lbrak = 30; not = 32; becomes = 33; number = 34; ident = 37;
semicolon = 38; end = 40; else = 41; elsif = 42;
if = 44; while = 46;
array = 54; record = 55;
const = 57; type = 58; var = 59; procedure = 60; begin = 61; module = 63;
eof = 64;
TYPE Ident = ARRAY IdLen OF CHAR;
VAR val: LONGINT;
id: Ident;
error: BOOLEAN;
PROCEDURE Mark(msg: ARRAY OF CHAR);
PROCEDURE Get(VAR sym: INTEGER);
PROCEDURE Init(T: Texts.Text; pos: LONGINT);
END OSS.
Синтаксический анализатор
57
Символы отображаются в целые числа. Отображение задается множеством
определений констант. Процедура Mark служит для вывода диагностики ошибок,
обнаруженных в исходном тексте. Обычно в журнал ошибок вместе с позицией
обнаруженной ошибки заносится краткое пояснение к ней. Процедура Get пред
ставляет собственно лексический анализатор. При каждом ее вызове она выдает
следующий распознанный символ. Процедура выполняет следующие действия
(полный ее листинг приведен в приложении C).
1. Пробелы и концы строк пропускаются.
2. Распознаются зарезервированные слова, подобные BEGIN и END.
3. Цепочки букв и цифр, начинающиеся с буквы и не являющиеся зарезерви
рованными словами, распознаются как идентификаторы. Параметр sym по
лучает значение ident, а последовательность самих литер записывается
в глобальную переменную id.
4. Цепочки цифр распознаются как числа. Параметр sym получает значение
number, а число записывается в глобальную переменную val.
5. Комбинации специальных литер, такие как := и <= , распознаются как один
символ.
6. Комментарии, представляющие последовательности произвольных литер,
начинающиеся с (* и заканчивающихся *), пропускаются.
7. Если лексический анализатор читает недопустимую литеру (вроде $ или %),
то возвращается символ null. Если достигнут конец текста, возвращается
символ eof. Ни один из этих символов не встречается в тексте правильно
построенной программы.
7.2. Синтаксический анализатор
Построение синтаксического анализатора в точности соответствует правилам
глав 3 и 4. Однако прежде чем начать его построение, необходимо проверить, со
ответствуют ли синтаксические правила ограничениям, обеспечивающим детер
минированный разбор с предпросмотром одного символа. С этой целью построим
сначала множества First и Follow. Они приведены в следующих таблицах:
S
selector
factor
term
SimpleExpression
expression
assignment
ProcedureCall
Statement
StatementSequence
FieldList
type
FPSection
First(S)
.[*
( ~ integer ident
( ~ integer ident
+ – ( ~ integer ident
+ – ( ~ integer ident
ident
ident
ident IF WHILE *
ident IF WHILE *
ident *
ident ARRAY RECORD
ident VAR
58
Синтаксический анализатор для Оберона 0
FormalParameters
ProcedureHeading
ProcedureBody
ProcedureDeclaration
declarations
module
(
PROCEDURE
END CONST TYPE VAR PROCEDURE BEGIN
PROCEDURE
CONST TYPE VAR PROCEDURE *
MODULE
S
selector
Follow(S)
* DIV MOD + – = # < <= > >= , ) ] OF THEN DO ;
END ELSE ELSIF
* DIV MOD + – = # < <= > >= , ) ] OF THEN DO ;
END ELSE ELSIF
+ – = # < <= > >= , ) ] OF THEN DO ;
END ELSE ELSIF
= # < <= > >= , ) ] OF THEN DO ; END ELSE ELSIF
, ) ] OF THEN DO ; END ELSE ELSIF
; END ELSE ELSIF
; END ELSE ELSIF
; END ELSE ELSIF
END ELSE ELSIF
; END
);
);
);
;
ident
;
END BEGIN
factor
term
SimpleExpression
expression
assignment
ProcedureCall
statement
StatementSequence
FieldList
type
FPSection
FormalParameters
ProcedureHeading
ProcedureBody
ProcedureDeclaration
declarations
Дальнейшая проверка правил на детерминированность показывает, что син
таксис Оберона 0 действительно может быть обработан методом рекурсивного
спуска с предпросмотром одного символа. Каждая процедура соответствует каж
дому нетерминальному символу. Прежде чем писать процедуры, полезно посмот
реть, как они зависят друг от друга. Для этой цели нарисуем граф зависимостей
(рис. 7.1). Каждая процедура в нем представлена узлом, а дуги от него проводятся
ко всем узлам, от которых эта процедура зависит, то есть вызывает их прямо или
косвенно. Обратите внимание, что некоторых нетерминальных символов не
оказалось в этом графе, так как они тривиальным образом включены в другие
символы. Например, ArrayType и RecordType содержатся только в type, поэтому
явно не показаны. Кроме этого, напомним, что символы ident и integer являются
терминальными символами, потому что так они определяются лексическим
анализатором.
Каждая петля в диаграмме соответствует рекурсии. Отсюда ясно, что анализа
тор должен записываться на языке, который допускает рекурсивные процедуры.
Кроме того, диаграмма показывает, каким образом процедуры могут быть вложе
ны. Module – единственная процедура, которую не вызывает ни одна другая. Ди
аграмма отражает структуру программы. Полный текст программы приводится
Устранение синтаксических ошибок
59
Рис. 7.1. Диаграмма зависимости процедур синтаксического анализатора
в приложении C. Синтаксический анализатор, подобно лексическому анализато
ру, также оформлен в виде модуля.
7.3. Устранение синтаксических ошибок
До сих пор рассматривалась довольно простая задача проверки соответствия ис
ходного текста лежащему в его основе синтаксису. В качестве побочного эффекта
синтаксический анализатор распознавал также структуру прочитанного текста.
Но как только появлялся недопустимый символ, задача анализатора считалась
выполненной, и процесс синтаксического анализа завершался. Однако для прак
тических применений такое положение дел недопустимо. Настоящий компиля
тор должен в этом случае выдать диагностическое сообщение об ошибке и затем
продолжить анализ. Тогда, вполне возможно, будут обнаружены и последующие
ошибки. Однако продолжение анализа после обнаружения ошибки возможно
только при допущении определенных гипотез относительно природы ошибки.
В зависимости от этих допущений часть последующего текста должна быть про
пущена или же в нее должны быть вставлены определенные символы. Такие меры
необходимы, даже когда уже нет надежды на исправление или выполнение оши
бочной исходной программы. Без верной, хотя бы отчасти, гипотезы продолжать
процесс разбора бесполезно [4, 13].
Методы выбора хороших гипотез сложны. Они в конечном счете опираются на
эвристики, поскольку проблема все еще не поддается формализации. Основная
причина заключается в том, что формальный синтаксис игнорирует факторы, ко
60
Синтаксический анализатор для Оберона 0
торые существенны для восприятия предложения человеком. Например, пропуск
знака пунктуации – частая ошибка, причем не только в текстах программ, а вот
знак операции в арифметическом выражении пропускается довольно редко.
И если для синтаксического анализатора оба этих символа – равнозначные син
таксические единицы, то для программиста точка с запятой кажется почти не
нужной, а знак плюс – суть выражения. Это различие нужно иметь в виду, если
ошибки должны обрабатываться осмысленно. Подводя итог, мы утверждаем сле
дующие качественные критерии обработки ошибок.
1. За один просмотр текста должно быть выявлено как можно больше ошибок.
2. Должно быть сделано как можно меньше дополнительных предположений
о языке.
3. Средства обработки ошибок не должны существенно замедлять работу ана
лизатора.
4. Программа анализатора не должна значительно увеличиваться в размере.
Можно сделать вывод, что обработка ошибок сильно зависит от конкретной
ситуации и описывается общими правилами только с ограниченным успехом.
Тем не менее есть несколько эвристических правил, которые, кажется, должны
быть полезны и за рамками нашего конкретного языка Оберон. В основном они
касаются влияния проекта языка на методы обработки ошибок. Несомненно, про
стая структура языка значительно упрощает диагностику ошибок, или, иначе,
сложный синтаксис безусловно усложняет обработку ошибок.
Будем различать два варианта неправильного текста. Первый – когда предпо
лагаемый символ отсутствует. Его обработать относительно легко. Синтаксичес
кий анализатор, выясняя ситуацию, продолжает обращаться к лексическому ана
лизатору один или более раз. Примером может служить оператор, где в конце
выражения ожидается закрывающая круглая скобка. Если она отсутствует, син
таксический анализатор возобновляет свою работу после выдачи сообщения об
ошибке:
IF sym = rparen THEN Get(sym) ELSE Mark(')
') END
Фактически не прерывают процесс разбора только пропуски слабых символов,
символов, которые имеют исключительно синтаксическую природу, таких как за
пятая, точка с запятой и завершающие символы. Случай неправильного использо
вания знака равенства вместо оператора присваивания обрабатывается так же
легко.
Второй вариант – когда появляется недопустимый символ. Тут не остается
ничего иного, как пропустить его и продолжить разбор со следующей позиции
текста. Для облегчения продолжения разбора в Обероне предусмотрены опреде
ленные конструкции, начинающиеся с характерных символов, которые в силу
своей сути редко используются неправильно. Например, последовательность
объявлений всегда начинается с символов CONST, TYPE, VAR или PROCEDURE,
а структурные операторы всегда начинаются c IF, WHILE, REPEAT, CASE и т. д. Поэто
му такие сильные символы никогда не пропускаются. Они служат в тексте точка
Устранение синтаксических ошибок
61
ми синхронизации, в которых процесс разбора может быть продолжен с высокой
вероятностью успеха. В синтаксисе Оберона мы зафиксируем четыре точки синх
ронизации, а именно factor, StatSequence, declarations и Type. В начале соответ
ствующей им процедуры разбора выполняется пропуск символов. А когда прочи
тан либо правильный начальный символ, либо сильный символ, процесс разбора
возобновляется.
PROCEDURE factor;
BEGIN (*
*)
IF sym < lparen THEN Mark("
REPEAT Get(sym) UNTIL sym >= lparen
END;
END factor;
?");
PROCEDURE StatSequence;
BEGIN (*
*)
IF sym < ident THEN Mark("
?");
REPEAT Get(sym) UNTIL sym >= ident
END;
END StatSequence;
PROCEDURE Type;
BEGIN(*
*)
IF (sym#ident) & (sym>=const) THEN Mark("
?");
REPEAT Get(sym) UNTIL (sym=ident) OR (sym>=array)
END;
END Type;
PROCEDURE declarations;
BEGIN(*
*)
IF sym<const THEN Mark("
?");
REPEAT Get(sym) UNTIL sym>=const
END;
...
END declarations;
Очевидно, что здесь предполагается определенный порядок символов. Этот
порядок был выбран таким образом, чтобы определенной группировкой символов
обеспечить простые и эффективные проверки их диапазонов. Как видно из описа
ния интерфейса лексического анализатора, сильным символам, которые нельзя
пропускать, назначен высокий приоритет (порядковый номер).
В общем, правило гласит, что программа анализатор порождается синтакси
сом согласно методу рекурсивного спуска и правилам перевода. Если прочитан
ный символ не отвечает ожиданиям, то посредством вызова процедуры Mark сооб
щается об ошибке и анализ продолжается со следующей точки синхронизации.
Зачастую обнаруживаются наведенные ошибки, о которых можно не сообщать,
потому что они являются просто следствием выявленных ранее ошибок. Дей
62
Синтаксический анализатор для Оберона 0
ствия, которые должны выполняться в каждой точке синхронизации, в общем мо
гут быть описаны следующим образом:
IF ~(sym IN follow(SYNC)) THEN Mark(msg);
REPEAT Get(sym) UNTIL sym IN follow(SYNC)
END
где follow(SYNC) обозначает множество символов, которые можно ожидать в этой
точке.
В определенных случаях бывает выгодно отступить от утверждений, вытекаю
щих из метода. Примером может служить конструкция StatSequence (последова
тельность операторов). Вместо
Statement;
WHILE sym = semicolon DO Get(sym); Statement END
запишем
LOOP (*
*)
IF sym < ident THEN Mark("
?"); ... END;
Statement;
IF sym = semicolon THEN Get(sym)
ELSIF sym IN follow(StatSequence) THEN EXIT
ELSE Mark("
?")
END
END
Это заменяет два вызова Statement одним вызовом, который здесь может быть
заменен телом процедуры, что избавляет от необходимости объявлять явную про
цедуру. Две проверки после Statement соответствуют двум допустимым вариан
там, когда после прочтения точки с запятой либо анализируется следующий опе
ратор, либо последовательность операторов заканчивается. Вместо условия sym
IN follow (StatSequence) мы используем логическое выражение, которое снова по
зволяет воспользоваться специально выбранным порядком символов:
(sym >= semicolon) & (sym < if) OR (sym >= array)
Приведенная конструкция – пример общего случая, когда последователь
ность, возможно, пустая, однотипных элементов (здесь – операторов) разделяет
ся слабым символом (здесь – точкой с запятой). Второй подобный пример имеет
место в списке параметров вызова процедуры. Действие
IF sym = lparen THEN
Get(sym); expression;
WHILE sym = comma DO Get(sym); expression END;
IF sym = rparen THEN Get(sym) ELSE Mark(")?") END
END
заменяется на
IF sym = lparen THEN Get(sym);
LOOP expression;
Устранение синтаксических ошибок
63
IF sym = comma THEN Get(sym)
ELSIF (sym=rparen) OR (sym>=semicolon) THEN EXIT
ELSE Mark(")
, ?");
END
END;
IF sym = rparen THEN Get(sym) ELSE Mark(") ?") END
END
Другой пример такого рода – последовательность объявлений. Вместо
IF sym = const THEN ... END;
IF sym = type THEN ... END;
IF sym = var THEN ... END;
мы используем более свободную запись
LOOP
IF sym = const THEN ... END;
IF sym = type THEN ... END;
IF sym = var THEN ... END;
IF (sym >= const) & (sym <= var) THEN
Mark("
END
END
") ELSE EXIT
Причина отступления от заданного метода состоит в том, что неправильная
последовательность объявлений (скажем, переменных перед константами) долж
на вызывать сообщение об ошибке, в то время как каждое отдельное объявление
может быть разобрано без проблем. Еще один похожий пример можно найти
в объявлении типов Type. Во всех таких случаях абсолютно необходимо гаранти
ровать, чтобы синтаксический анализатор не мог быть втянут в бесконечный
цикл. Простейший способ добиться этого – убедиться в том, что при каждом по
вторе читается по крайней мере один символ, то есть каждый путь содержит по
меньшей мере один вызов Get. Таким образом, даже в худшем случае синтаксиче
ский анализатор достигает конца исходного текста программы и останавливается.
За дальнейшими деталями можно обратиться к листингу в приложении С.
Теперь должно стать ясно, что не существует идеальной стратегии обработки
ошибок, которая с большой эффективностью транслировала бы все корректные
предложения и в то же время точно диагностировала все ошибки в неправильных
исходных текстах. Любая стратегия обработает некоторые непонятные предложе
ния таким способом, который окажется неожиданным для ее авторов. Существен
ными характеристиками хорошего компилятора, не вдаваясь в детали, являются:
(1) ни одна из последовательностей символов не приводит к нарушению работы
компилятора и (2) часто встречающиеся ошибки диагностируются корректно и,
следовательно, не генерируют вообще (или совсем немного) ложных сообщений.
Представленная здесь стратегия работает удовлетворительно, хотя может быть
усовершенствована. Она интересна тем, что обработчик ошибок получается не
посредственно из синтаксического анализатора применением нескольких про
64
Синтаксический анализатор для Оберона 0
стых правил. Эти правила пополняются благодаря разумному выбору небольшо
го числа параметров, которые определяются на основе достаточного опыта ис
пользования языка.
7.4. Упражнения
7.1. Для того чтобы определить, является ли последовательность букв ключевым
словом, приведенный в приложении C лексический анализатор компилятора ис
пользует линейный поиск по массиву KeyTab. Так как этот поиск выполняется
очень часто, то усовершенствованный метод поиска мог бы, несомненно, привести
к повышению производительности. Замените линейный поиск в массиве на:
1) двоичный поиск в упорядоченном массиве;
2) поиск в двоичном дереве;
3) поиск в хэш таблице.
Выберите хэш функцию так, чтобы потребовалось не более двух сравнений
для определения, является ли последовательность букв ключевым словом.
Определите общий прирост производительности компилятора для каждого из
трех решений.
7.2. Где синтаксис Оберона нарушает ограничения LL(1), то есть где необхо
дим предпросмотр более чем одного символа? Измените синтаксис так, чтобы он
удовлетворял свойству LL(1).
7.3. Расширьте лексический анализатор таким образом, чтобы он допускал
вещественные числа, как они определяются синтаксисом Оберона (см. прило
жение А.2).
Глава 8
Учет контекста,
заданного объявлениями
8.1. Объявления .........................
8.2. Записи о типах данных ........
8.3. Представление данных
во время выполнения ................
8.4. Упражнения .........................
66
68
69
73
66
Учет контекста, заданного объявлениями
8.1. Объявления
Хотя языки программирования основаны на контекстно свободных грамматиках
в смысле Хомского, они не являются контекстно свободными в обычном понима
нии этого слова. Контекстная зависимость проявляется в том, что каждый иден
тификатор в программе должен быть объявлен. Таким образом, он связывается
с объектом вычислительного процесса, который имеет определенные неизменные
свойства. Например, идентификатор связывается с переменной, и эта переменная
имеет конкретный тип данных, указанный в объявлении идентификатора. Если
появляющийся в операторе идентификатор ссылается на заданный его объявле
нием объект, а само объявление идентификатора находится за пределами этого
оператора, то мы скажем, что объявление находится в контексте оператора.
Очевидно, что учет контекста находится за пределами возможностей контек
стно свободного анализа, но, несмотря на это, легко обрабатывается. Контекст
представляется в виде структуры данных, которая содержит запись для каждого
из объявленных идентификаторов. Эта запись (или вход) связывает идентифика
тор с соответствующим объектом и его свойствами. Такая структура данных на
зывается таблицей символов. Данный термин восходит к временам ассемблеров,
когда идентификаторы назывались символами. Хотя структура таблицы симво
лов обычно сложнее, чем простой массив.
Синтаксический анализатор будет расширен теперь таким образом, что при
разборе объявления таблица символов будет пополняться. Для каждого объяв
ленного идентификатора добавляется запись. Итак:
• каждое объявление влечет добавление новой записи в таблицу символов;
• каждое появление идентификатора в операторе влечет поиск по таблице
символов для определения атрибутов (свойств) объекта, обозначенного
этим идентификатором.
Типичный атрибут – класс объекта. Он указывает, будет ли идентификатор
обозначением переменной, константы, типа или процедуры. Еще одним атрибу
том во всех языках программирования с типизацией данных служит тип объекта.
Простейшей формой структуры данных для представления множества эле
ментов является список. Главный его недостаток заключается в относительно
медленном поиске, потому что он должен быть просмотрен от начала до искомого
элемента. Поскольку структуры данных не являются предметом нашего исследо
вания, то ради простоты мы объявим следующие типы, представляющие линей
ные списки:
Object = POINTER TO ObjDesc;
ObjDesc = RECORD
name: Ident;
class: INTEGER;
type: Type;
next: Object;
val: LONGINT
END
Объявления
67
Так, например, следующие объявления представлены списком, показанным на
рис. 8.1.
CONST N = 10;
TYPE T = ARRAY N OF INTEGER;
VAR x, y: T
Рис. 8.1. Таблица символов,
представляющая объекты с их именами и атрибутами
Для добавления новых записей введем процедуру NewObj с явным параметром
class, неявным параметром id и результатом obj. Процедура проверяет, присут
ствует ли новый идентификатор (id) в списке. Это означает многократное его оп
ределение, что вызывает программную ошибку. Новая запись добавляется в ко
нец списка, и таким образом список отражает очередность объявлений в исходном
тексте. В конце списка находится элемент страж (guard), которому до начала
просмотра списка присваивается значение нового идентификатора. Такая мера
упрощает условие завершения оператора WHILE.
PROCEDURE NewObj(VAR obj: Object; class: INTEGER);
VAR new, x: Object;
BEGIN x := origin; guard.name := id;
WHILE x.next.name # id DO x := x.next END;
IF x.next = guard THEN
NEW(new); new.name := id; new.class := class; new.next := guard;
x.next := new; obj := new
ELSE obj := x.next; Mark('
')
END
End NewObj
Для ускорения поиска список часто заменяют древовидной структурой. Ее
преимущество становится заметным только при довольно большом количестве
записей. Для структурированных языков с локальными областями, то есть зонами
видимости идентификаторов, таблица символов тоже должна быть структури
рованной, и тогда число записей в каждой области становится относительно не
большим. Опыт показывает, что древовидная структура не дает ощутимого пре
имущества над списком, хотя требует более сложного процесса поиска и наличия
в записи трех указателей на потомков вместо одного. Отметим, что очередность
записей все равно должна быть отражена в структуре, так как это существенно
в случае параметров процедуры.
68
Учет контекста, заданного объявлениями
8.2. Записи о типах данных
В языках, имеющих типы данных, проверка их совместимости является одной из
важнейших задач компилятора. Проверка основана на атрибуте type, вносимом
в каждую запись таблицы символов. Поскольку сами типы данных тоже могут
объявляться, очевидным решением оказывается указатель на соответствующую за
пись о типе. Однако тип может быть объявлен анонимно, как в следующем примере:
VAR a: ARRAY 10 OF INTEGER
Здесь тип переменной a не имеет имени. Простое решение этой проблемы –
заводить в компиляторе собственные типы данных, чтобы представлять типы как
таковые. Тогда именованные типы представляются в таблице символов записями
типа Object, которые, в свою очередь, ссылаются на элемент типа Type.
Type = POINTER TO TypDesc;
TypDesc = RECORD
form, len: INTEGER;
fields: Object;
base: Type
END
Атрибут form различает элементарные типы (INTEGER, BOOLEAN) и струк
турные типы (массивы, записи). Остальные атрибуты добавляются в зависимости
от значения атрибута form. Характеристиками массива служат его длина (количе
ство элементов) и тип элементов (base). Для записей (RECORD) должен быть пре
дусмотрен список, представляющий их поля. Его элементы должны иметь класс
Рис. 8.2. Таблица символов, представляющая объявленные объекты
Представление данных во время выполнения
69
Field. В качестве примера на рис. 8.2 показана таблица символов, которая получи
лась в результате следующих объявлений:
TYPE R = RECORD f,g: INTEGER END;
VAR x: INTEGER;
a: ARRAY 10 OF INTEGER;
r,s: R;
Что касается методологии программирования, лучше было бы ввести допол
нительный тип данных для каждого класса объектов, используя только базовый
тип с полями id, type и next. Мы воздержимся от этого не только потому, что все
такие типы должны объявляться внутри одного и того же модуля, но и потому, что
использование числового признака class вместо отдельных типов избавляет от не
обходимости использования большого числа излишних стражей для этих типов и
таким образом повышает производительность. Наконец, мы не хотим способство
вать чрезмерному размножению типов данных.
8.3. Представление данных
во время выполнения
До сих пор все аспекты архитектуры целевого компьютера, то есть компьютера,
для которого должен генерироваться код, игнорировались, потому что нашей
единственной задачей было распознать исходный текст и проверить его соответ
ствие синтаксису. Однако, как только синтаксический анализатор расширяется
до компилятора, знание архитектуры целевого компьютера становится обяза
тельным.
Во первых, мы должны определить формат, в котором данные должны пред
ставляться в памяти во время выполнения. Тут выбор всецело зависит от целевой
архитектуры, хотя это не столь очевидно из за сходства в этом отношении практи
чески всех компьютеров. Здесь мы обратимся к общепринятой форме хранения
в виде последовательности отдельно адресуемых байтов, то есть к байт ориенти
рованной памяти. В этом случае последовательно объявляемые переменные раз
мещаются в памяти с монотонно убывающими или возрастающими адресами. Это
называется последовательным размещением.
Каждый компьютер снабжен конкретными элементарными типами данных
вместе с соответствующими командами, такими как целочисленное сложение и
сложение чисел с плавающей запятой. Эти типы, безусловно, скалярные и зани
мают небольшое число последовательных ячеек памяти (байтов). Пример архи
тектуры с довольно богатым набором типов – семейство процессоров NS3200
фирмы National Semiconductor:
INTEGER
LONGINT
SHORTINT
REAL
2
4
1
4
LONGREAL
CHAR
BOOLEAN
SET
8
1
1
4
70
Учет контекста, заданного объявлениями
Из вышесказанного заключаем, что каждый тип имеет размер, а каждая пере
менная – адрес.
Атрибуты type.size и obj.adr определяются, когда компилятор обрабатывает
объявления. Размеры элементарных типов задаются машинной архитектурой,
а соответствующие им записи генерируются, когда компилятор загружается и
инициализируется. Для объявляемых структурных типов их размер рассчиты
вается.
Размер массива – это размер его элемента, умноженный на количество его эле
ментов. Адрес элемента – это адрес массива плюс индекс массива, умноженный на
размер элемента. Пусть даны следующие общие объявления:
TYPE T = ARRAY n OF T0
VAR a: T
Тогда размер типа и адрес элемента получаются следующим образом:
size(T) = n*size(T0)
adr(a[x]) = adr(a) + x*size(T0)
Многомерным массивам
TYPE T = ARRAY nk–1,… ,n1,n0 OF T0
соответствуют следующие формулы (см. рис. 8.3):
Рис. 8.3. Представ
size(T) = nk–1 * … * n1 * n0 * size(T0)
ление матрицы
adr(a[xk–1, … , x1, x0]) = adr(a)+
+ xk–1 * nk–2 * … * n0 * size(T0) + …
+ x2 * n1 * n0 * size(T0) + x1 * n0 * size(T0) + x0 * size(T0)
)
= adr(a) + (((…xk–1*nk–1 + … + x2)*n1 + x1)*n0 + x0) * size(T0) (
Заметьте, что при вычислении размера массива длины всех его размерностей
известны, потому что они встречаются в тексте программы как константы. Одна
ко значения индексов при вычислении адреса элемента, как правило, неизвестны
до выполнения программы.
a: ARRAY 2 OF ARRAY 2 OF REAL
Напротив, для записей и размер типа, и адрес поля известны во время компи
ляции. Давайте рассмотрим следующие объявления:
TYPE T = RECORD f0: T0; f1: T1; … ; fk–1: Tk–1 END
VAR r: T
Тогда размер типа и адреса полей вычисляются в соответствии со следующими
формулами:
size(T) = size(T0) + … + size(Tk–1)
adr(r.fi) = adr(r) + offset(fi)
offset(fi) = size(T0) + … + size(Ti–1)
Абсолютные адреса переменных обычно неизвестны на этапе компиляции. Все
генерируемые адреса должны рассчитываться относительно общего базового ад
Представление данных во время выполнения
71
реса, который устанавливается во время выполнения. Тогда исполнительный ад
рес – это сумма этого базового адреса и относительного адреса, определяемого
компилятором.
Если память компьютера адресуется побайтно, как это часто бывает, то нужно
иметь в виду следующее. Хотя каждый байт имеет собственный адрес, данные
в память или из памяти обычно передаются пакетом из небольшого количества
байтов (скажем, 4 или 8), называемым словом. Если выделение памяти произво
дится строго последовательно, то возможно, что переменная может занять (час
тично) несколько слов (см. рис. 8.4). Этого следует решительно избегать, так как
иначе обращение к одной переменной потребует нескольких обращений к памяти,
приводя к ощутимому замедлению. Простой метод решения этой проблемы – ок
ругление адреса каждой переменной до числа, кратного ее размеру. Этот процесс
называется выравниванием. Такое правило применяется для элементарных типов
данных. Точно так же подгоняется размер элементов массива, а размеры полей
записей мы просто округляем до длины слова компьютера. Цена выравнивания –
потеря некоторых байтов памяти компьютера, количество которых довольно не
значительно.
VAR a:CHAR; b,c: INTEGER; d: REAL
Невыровненные,
рваные поля
Выровненные поля
Рис. 8.4. Выравнивание при вычислении адресов
Для генерации необходимых записей в таблице символов нужны следующие
дополнения к процедуре синтаксического анализа объявлений:
If sym = type THEN(* “TYPE” ident ”=” type *)
Get(sym);
WHILE sym=ident DO
NewObj(obj, Typ); Get(Sym);
IF sym = eql THEN Get(sym) ELSE Mark(“;?”) END;
Type1(obj.type);
IF sym = semicolon THEN Get(sym) ELSE Mark(“;?”) END
END
END;
IF sym = var THEN (* “VAR” ident {“,” ident}”:” type*)
Get(sym);
WHILE sym =ident DO
72
Учет контекста, заданного объявлениями
IdentList(Var, first); Type1(tp); obj := first;
WHILE obj # guard DO
Obj.type:=tp; INC(adr, obj.type.size); obj.val:=–adr; obj:=obj.next
END;
IF sym = semicolon THEN Get(sym) ELSE MARK(“;?”) END
END
END;
Здесь процедура IdentList используется для того, чтобы обработать список
идентификаторов, а рекурсивная процедура Type1 служит для компиляции
объявлений типов.
PROCEDURE IdentList(class: INTEGER; VAR first:Object);
VAR obj: Object;
BEGIN
IF sym = ident THEN
New Obj(first,class); Get(sym);
WHILE sym = comma DO
Get(sym);
IF sym=ident THEN NewObj(object,class);Get(sym) ELSE Mark(“
END;
IF sym = colon THEN Get(sym) else Mark(“:?”) END
END
END IdentList;
?”) END
PROCEDURE Type1(VAR type: OSG.Type);
VAR obj, first: Object; tp: OSG.Type;
BEGIN type := intType; (*
*)
IF (sym # ident) & (sym < array) THEN Mark("
?");
REPEAT Get(sym) UNTIL (sym = ident) OR (sym >= array)
END;
IF sym = ident THEN
find(obj); Get(sym);
IF obj.class = Typ THEN type := obj.type ELSE Mark("
?") END
ELSIF sym = array THEN
Get(sym); expression(x);
IF (sym = number) THEN n := val; Get(sym) ELSE Mark("
?"); n:=1 END;
IF sym = of THEN Get(sym) ELSE Mark("OF?") END ;
Type(tp); NEW(type); type.form := Array; type.base := tp;
type.len := n; type.size := type.len * tp.size
ELSIF sym = OSS.record THEN
Get(sym); NEW(type); type.form := Record; type.size := 0; OpenScope;
LOOP
IF sym = ident THEN
IdentList(Fld, first); Type(tp); obj := first;
WHILE obj # guard DO
obj.type := tp; obj.val := type.size; INC(type.size, obj.type.size);
obj := obj.next
END
Упражнения
73
END ;
type.fields := topScope.next; CloseScope;
IF sym = end THEN Get(sym) ELSE Mark("END?") END
ELSE OSS.Mark("
?")
END
END Type1;
Следуя давней традиции, адресам переменных присваиваются отрицательные
значения, то есть отрицательные смещения относительно общего базового адреса,
устанавливаемого во время выполнения программы. Вспомогательные процеду
ры OpenScope и CloseScope служат для того, чтобы список полей записи не пере
мешивался со списком переменных. Каждое объявление записи устанавливает
новую область видимости для идентификаторов полей, как того требует опреде
ление языка Оберон. Обратите внимание, что начало списка, в который добавля
ются новые записи, задается глобальной переменной topScope.
8.4. Упражнения
8.1. По определению область видимости идентификатора простирается от места
его объявления до конца процедуры, в которой оказалось это объявление. Что
нужно для того, чтобы эта область могла простираться от начала до конца про
цедуры?
8.2. Рассмотрим объявления указателей, как они определены в Обероне. В них
указывается тип, с которым связан объявляемый указатель и который может по
явиться в тексте позже. Что нужно для того, чтобы примирить это послабление
с общим правилом о том, что все объекты, на которые ссылаются, должны объяв
ляться до своего использования?
Глава 9
RISC архитектура как цель
9.1. Ресурсы и регистры ............ 76
76
RISC архитектура как цель
Стоит заметить, что наш компилятор вплоть до этого раздела мог быть разработан
независимо от целевого компьютера, для которого он должен генерировать код.
В самом деле, почему целевая структура машины должна влиять на синтаксический
анализ и обработку ошибок? Напротив, такого влияния необходимо сознательно
избегать. В результате, согласно принципу пошаговой разработки, генерация кода
для произвольного компьютера может быть добавлена к существующему машин
но независимому анализатору, который служит для нее каркасом. Прежде чем
взяться за эту задачу, должна быть выбрана целевая архитектура.
Чтобы сохранить и разумную простоту компилятора, и чистоту проекта от де
талей, которые касаются только определенной машины и ее особенностей, мы
обусловим архитектуру нашим собственным выбором. Таким образом мы полу
чим существенное преимущество в том, что она может быть приспособлена к нуж
дам исходного языка. Эта архитектура не существует как реальная машина,
поэтому она виртуальна. Но так как каждый компьютер выполняет команды со
гласно фиксированному алгоритму, он может быть легко описан программой.
А затем для выполнения этой программы может быть использован реальный ком
пьютер, который будет интерпретировать сгенерированный код. Такую програм
му называют интерпретатором, и она эмулирует виртуальную машину, которая,
если можно так сказать, имеет полуреальное существование.
Цель этой главы – не изложение мотивов выбора определенной виртуальной
архитектуры со всеми ее деталями. Она скорее предназначена служить описатель
ным руководством, состоящим из неформального введения и формального опре
деления компьютера в виде интерпретирующей программы. Эту формализацию
можно даже рассматривать в качестве примера точной спецификации процессора.
При определении этого компьютера мы намеренно следуем линии, близкой
RISC архитектуре. Аббревиатура RISC (Reduced Instruction Set Computer) означа
ет процессор с сокращенным набором команд, где слово «сокращенный» нужно
понимать как отношение к архитектурам с большими наборами сложных команд,
которые доминировали до 1980 года. Понятно, что тут не место ни для объясне
ния сущности RISC архитектуры, ни для изложения различных ее достоинств.
Очевидно, что здесь она привлекает своей простотой и ясностью понятий, кото
рые упрощают описание набора команд и выбор последовательностей команд,
соответствующих определенным языковым конструкциям. Выбранная здесь ар
хитектура почти идентична той, что представлена Хэнесси и Паттерсоном под на
званием DLX [5]. Небольшие отклонения обусловлены нашим стремлением к по
вышенной регулярности. Среди коммерческих продуктов ближе всех к нашей
виртуальной машине подходят MIPS и ARM архитектуры.
9.1. Ресурсы и регистры
С точек зрения программиста и проектировщика компилятора, компьютер состо
ит из арифметического устройства, блока управления и памяти. Арифметическое
устройство содержит 16 регистров R0–R15 по 32 бита каждый. Блок управления
состоит из регистра команд IR (instruction register), содержащего текущую вы
Ресурсы и регистры
77
полняемую команду, и счетчика команд PC (program counter), содержащего адрес
следующей команды (рис. 9.1).
Счетчик команд включен в состав регистров данных: PC = R15. Команды пере
хода к подпрограммам неявно используют регистр R14 для хранения адреса воз
врата. Память является байт адресуемой и состоит из 32 битовых слов, то есть
адреса слов кратны 4.
Имеются три типа команд и форматов команд. Регистровые команды работают
только с регистрами и получают данные от регистра сдвигов (shifter) и арифмети
ко логического устройства ALU. Команды памяти извлекают и запоминают дан
ные в памяти. Команды перехода влияют на счетчик команд.
1. Регистровые команды (форматы F0 и F1):
MOV a, c
MVN a, c
ADD a, b, c
SUB a, b, c
MUL a, b, c
DIV a, b, c
MOD a, b, c
CMP b, c
R.a := Shift(R.c, b)
R.a :=–Shift(R.c, b)
R.a := R.b + R.c
R.a := R.b – R.c
R.a := R.b * R.c
R.a := R.b DIV R.c
R.a := R.b MOD R.c
Z := R.b = R.c
N := R.b < R.c
MOVI a, im
MVNI a, im
ADDI a, b, im
SUBI a, b, im
MULI a, b, im
DIVI a, b, im
MODI a, b, im
CMPI b, im
Рис. 9.1. Блок схема RISC структуры
R.a := Shift(im, b)
R.a :=–Shift(im, b)
R.a := R.b + im
R.a := R.b – im
R.a := R.b * im
R.a := R.b DIV im
R.a := R.b MOD im
Z := R.b = im
N := R.b < im
78
RISC архитектура как цель
Рис. 9.2. Форматы команд
В случае регистровых команд имеются два варианта. Или второй операнд яв
ляется непосредственным значением (F1), а знак 18 битовой константы im рас
пространяется до 32 битов. Или второй операнд – это номер регистра (F0). Ко
манда сравнения CMP влияет на биты состояния Z и N (нуль и отрицательно).
2. Команды памяти (формат F2):
LDW a, b, im
LDB a, b, im
POP a, b, im
STW a, b, im
STB a, b, im
PSH a, b, im
R.a := Mem[R.b +disp]
R.a := Mem[R.b + disp] MOD 100H
R.b := R.b – disp; R.a := Mem[R.b]
Mem[R.b + disp] := R.a
Mem[R.b + disp] := …
Mem[R.b] := R.a; R.b := R.b + disp
3. Команды перехода (формат F3, адрес слова относительно PC):
BEQ disp
BNE disp
BLT disp
BGE disp
BLE disp
BGT disp
BR disp
BSR disp
RET disp
PC := PC + disp*4,
Z
PC := PC + disp*4,
~Z
PC := PC + disp*4,
N
PC := PC + disp*4,
~N
PC := PC + disp*4,
Z
N
PC := PC + disp*4,
~(Z
N)
PC := PC + disp*4
R14 := PC; PC := PC + disp*4 (
PC := R.c
PC)
Более подробно виртуальный компьютер определяется следующей програм
мой интерпретатором. Заметим, что регистр PC хранит адреса слов, а не байтов, и
что Z и N – биты состояния, устанавливаемые командами сравнения.
MODULE RISC; (*NW 27. 11. 05*)
IMPORT SYSTEM, Texts;
CONST MemSize* = 4096; ProgOrg = 2048; (*
*)
MOV = 0; MVN = 1; ADD = 2; SUB = 3; MUL = 4; Div = 5; Mod = 6; CMP = 7;
MOVI = 16; MVNI = 17; ADDI = 18; SUBI = 19; MULI = 20; DIVI = 21;
Ресурсы и регистры
79
MODI = 22; CMPI = 23; CHKI = 24;
LDW = 32; LDB = 33; POP = 34; STW = 36; STB = 37; PSH = 38;
RD = 40; WRD= 41; WRH = 42; WRL = 43;
BEQ = 48; BNE = 49; BLT = 50; BGE = 51; BLE = 52; BGT = 53; BR = 56;
BSR = 57; RET = 58;
VAR IR: LONGINT;
N, Z: BOOLEAN;
R*: ARRAY 16 OF LONGINT;
M*: ARRAY MemSize DIV 4 OF LONGINT;
W: Texts.Writer;
(* R[15]
PC, R[14]
BSR *)
PROCEDURE Execute*(start: LONGINT; VAR in: Texts.Scanner; out: Texts.Text);
VAR opc, a, b, c, nxt: LONGINT;
BEGIN R[14] := 0; R[15] := start + ProgOrg;
LOOP (*
*)
nxt := R[15] + 4; IR := M[R[15] DIV 4];
opc := IR DIV 4000000H MOD 40H;
a := IR DIV 400000H MOD 10H;
b := IR DIV 40000H MOD 10H;
c := IR MOD 40000H;
IF opc < MOVI THEN (*F0*) c := R[IR MOD 10H]
ELSIF opc < BEQ THEN
(*F1, F2*) c := IR MOD 40000H;
IF c >= 20000H THEN DEC(c, 40000H) END (*
*)
ELSE (*F3*) c := IR MOD 4000000H;
IF c >= 2000000H THEN DEC(c, 4000000H) END (*
*)
END ;
CASE opc OF
MOV, MOVI: R[a] := ASH(c, b) (*
*)
| MVN, MVNI: R[a] := –ASH(c, b)
| ADD, ADDI: R[a] := R[b] + c
| SUB, SUBI: R[a] := R[b] – c
| MUL, MULI: R[a] := R[b] * c
| Div, DIVI: R[a] := R[b] DIV c
| Mod, MODI: R[a] := R[b] MOD c
| CMP, CMPI: Z := R[b] = c; N := R[b] < c
| CHKI: IF (R[a] < 0) OR (R[a] >= c) THEN R[a] := 0 END
| LDW: R[a] := M[(R[b] + c) DIV 4]
| LDB: (*
*)
| POP: R[a] := M[(R[b]) DIV 4]; INC(R[b], c)
| STW: M[(R[b] + c) DIV 4] := R[a]
| STB: (*
*)
| PSH: DEC(R[b], c); M[(R[b]) DIV 4] := R[a]
| RD: Texts.Scan(in); R[a] := in.i
| WRD: Texts.Write(W, " "); Texts.WriteInt(W, R[c], 1)
| WRH: Texts.WriteHex(W, R[c])
| WRL: Texts.WriteLn(W); Texts.Append(out, W.buf)
80
RISC архитектура как цель
|
|
|
|
|
|
|
|
|
BEQ: IF Z THEN nxt := R[15] + c*4 END
BNE: IF ~Z THEN nxt := R[15] + c*4 END
BLT: IF N THEN nxt := R[15] + c*4 END
BGE: IF ~N THEN nxt := R[15] + c*4 END
BLE: IF Z OR N THEN nxt := R[15] + c*4 END
BGT: IF ~Z & ~N THEN nxt := R[15] + c*4 END
BR: nxt := R[15] + c*4
BSR: nxt := R[15] + c*4; R[14] := R[15] + 4
RET: nxt := R[c MOD 10H];
IF nxt = 0 THEN EXIT END
END ;
R[15] := nxt
END
END Execute;
PROCEDURE Load*(VAR code: ARRAY OF LONGINT; len: LONGINT);
VAR i: INTEGER;
BEGIN i := 0;
WHILE i < len DO M[i + ProgOrg DIV 4] := code[i]; INC(i) END
END Load;
BEGIN Texts.OpenWriter(W)
END RISC.
Дополнительные замечания:
1. Команды RD, WRD, WRH и WRL не типичны для компьютеров. Мы добавили
их для обеспечения простого и эффективного способа ввода вывода. Таким
образом, компилируемые и интерпретируемые программы могут тестиро
ваться и обретать реальность.
2. Команды LBD и STB сохраняют и читают один байт памяти. Без них не было
бы смысла говорить о байт ориентированном компьютере. Однако мы воз
держимся здесь от их определения, так как операторы программы должны
были бы точно воспроизвести их аппаратную реализацию.
3. Команды PSH и POP ведут себя так же, как STW и LDW, вследствие чего значе
ние базового регистра R.b увеличивается или уменьшается на значение c.
Они позволят удобным способом передавать параметры процедуры (см.
главу 12).
4. Команда CHKI просто обнуляет значение индекса, которое вышло за допус
тимые границы, потому что RISC не поддерживает прерываний.
Глава 10
Выражения и присваивания
10.1. Прямая генерация кода
по принципу стека .....................
10.2. Отсроченная генерация
кода ...........................................
10.3. Индексированные
переменные и поля записей ......
10.4. Упражнения .......................
82
84
89
94
82
Выражения и присваивания
10.1. Прямая генерация кода
по принципу стека
В третьем примере главы 5 показано, как преобразовать выражение из обыкно
венной инфиксной в эквивалентную постфиксную запись. Наш идеальный ком
пьютер мог бы напрямую интерпретировать постфиксную запись. Как уже было
показано, такой идеальный компьютер нуждается в стеке для хранения промежу
точных результатов. И подобная компьютерная архитектура называется стековой
архитектурой.
Компьютеры со стековой архитектурой не являются общепринятыми. Стеку
предпочитают наборы явно адресуемых регистров. Конечно, набор регистров мо
жет легко использоваться для эмуляции стека. На его верхний элемент указывает
глобальная переменная, представляющая указатель стека (SP) в компиляторе.
Это возможно, поскольку количество промежуточных результатов известно на
этапе компиляции, а использование глобальной переменной оправдано тем, что
стек является глобальным ресурсом.
Чтобы получить программу для генерации кода, соответствующего конкрет
ным конструкциям, мы должны сначала определить шаблоны требуемого кода.
Этот метод, помимо выражений и присваиваний, будет в дальнейшем с тем же ус
пехом применяться и для других конструкций. Пусть код для данной конструк
ции K задается следующей таблицей:
K
Ident
number
( exp )
fac0 * fac1
term0 + term1
ident := exp
code(K)
LDW i, 0, adr(ident)
MOVI i, 0, value
code(exp)
code(fac0)
code(fac1)
MUL i, i, i+1
code(term0)
code(term1)
ADD i, i, i+1
ADD i, i, i+1
code(exp)
STW i, adr(ident)
INC(SP)
INC(SP)
DEC(SP)
DEC(SP)
DEC(SP)
DEC(SP)
Для начала ограничимся операндами в виде простых переменных и пренебре
жем селекторами для структурных переменных. Сначала рассмотрим выражение
u := x*y + z*w :
Команда
Смысл команды
Стек
Указатель стека
LDW
LDW
MUL
LDW
LDW
R0
R1
R0
R1
R2
x
x, y
x*y
x*y, z
x*y, z, w
SP = 1
2
1
2
3
R0, base, x
R1, base, y
R0, R1, R2
R1, base, z
R2, base, w
:= x
:= y
:= R0*R1
:= z
:= w
Прямая генерация кода по принципу стека
MUL R1, R1, R2
ADD R0, R0, R1
STW R0, base, u
R1 := R1*R2
R1 := R1+R2
u := R1
x*y, z*w
x*y + z*w
–
83
2
1
0
Отсюда совершенно ясно, каким образом должны быть расширены соответ
ствующие процедуры синтаксического анализатора.
PROCEDURE factor;
VAR obj: Object;
BEGIN
IF sym = ident THEN find(obj); Get(sym); INC(RX); Put(LDW, RX, 0, –obj.val)
ELSIF sym = number THEN INC(RX); Put(MOVI, RX, 0, val); Get(sym)
ELSIF sym = lparen THEN
Get(sym); expression;
IF sym = rparen THEN Get(sym) ELSE Mark(" )
") END
ELSIF ...
END
END factor;
PROCEDURE term;
VAR op: INTEGER;
BEGIN factor;
WHILE (sym = times) OR (sym = div) DO
op := sym; Get(sym); factor; DEC(RX);
IF op = times THEN Put(MUL, RX, RX, RX+1)
ELSIF op = div THEN Put(DIV, RX, RX, RX+1)
END
END
END term;
PROCEDURE SimpleExpression;
VAR op: INTEGER;
BEGIN
IF sym = plus THEN Get(sym); term
ELSIF sym = minus THEN Get(sym); term; Put(SUB, RX, 0, RX)
ELSE term
END ;
WHILE (sym = plus) OR (sym = minus) DO
op := sym; Get(sym); term; DEC(RX);
IF op = plus THEN Put(ADD, RX, RX, RX+1)
ELSIF op = minus THEN Put(SUB, RX, RX, RX+1)
END
END
END SimpleExpression;
PROCEDURE Statement;
VAR obj: Object;
BEGIN
IF sym = ident THEN
find(obj); Get(sym);
84
Выражения и присваивания
IF sym = becomes THEN
Get(sym); expression; Put(STW, RX, 0, obj.val); DEC(RX)
ELSIF ...
END
ELSIF ...
END
END Statement;
Здесь мы ввели процедуру генератор Put. Ее можно считать двойником проце
дуры лексического анализатора Get. Мы предполагаем, что она размещает коман
ду в глобальном массиве, используя переменную pc в качестве индекса, указыва
ющего на следующую свободную позицию в массиве. В таких допущениях
процедура Put записывается в Обероне следующим образом, где LSH(x,n) – функ
ция, выдающая значение x, сдвинутое влево на n битов:
PROCEDURE Put(op, a, b, d: INTEGER);
BEGIN
code[pc] := LSH(LSH(LSH(op, 4) + a, 4) + b, 18) + (d MOD 40000H); INC(pc)
END Put
В качестве адресов переменных здесь используются просто их идентифика
торы. В действительности место идентификаторов должны занять значения ад
ресов, полученные из таблицы символов. Они представляют собой смещения
относительно базового адреса, вычисляемого во время выполнения, которые до
бавляются к базовому адресу для получения абсолютных адресов. Это относится
не только к нашему RISC процессору, но фактически и ко всем распространен
ным компьютерам. Мы принимаем этот факт во внимание, задавая адреса в виде
пар, состоящих из смещения a и базы (регистра) r.
10.2. Отсроченная генерация кода
Рассмотрим в качестве второго примера выражение x + 1. Согласно схеме из раз
дела 10.1, мы получаем соответствующий код:
LDW
MOVI
ADD
0, base, x
1, 0, 1
0, 0, 1
R0 := x
R1 := 1
R0 := R0 + R1
Мы видим, что полученный код правильный, но явно не оптимальный. Недо
статок заключается в том, что константа 1 загружается в регистр, хотя в этом нет
необходимости, так как наш компьютер имеет команду, позволяющую добавлять
константы непосредственно к регистру (режим непосредственной адресации).
Ясно, что некоторый код был выдан поспешно. Выход состоит в том, чтобы в опре
деленных случаях задержать выдачу кода до тех пор, пока не будет точно извест
но, что лучшего решения не существует. Каким же образом должна быть реализо
вана такая отсроченная генерация кода?
В общих чертах метод заключается в привязке к полученной синтаксической
конструкции информации, которая должна использоваться для выбора порож
Отсроченная генерация кода
85
даемого кода. Согласно принципам атрибутных грамматик, изложенным в главе
5, подобная информация хранится в виде атрибутов. Таким образом, генерация
кода зависит не только от синтаксически сворачиваемых символов, но и от значе
ний их атрибутов. Это концептуальное расширение выражается в том, что про
цедуры синтаксического анализа снабжаются выходным параметром, который
представляет эти атрибуты. Так как обычно атрибутов бывает несколько, то для
таких параметров используется тип RECORD; назовем этот тип Item [19].
Для нашего второго примера необходимо установить, хранится ли (во время
выполнения) значение множителя, слагаемого или выражения в регистре, как это
было до сих пор, или оно известная константа. Последнее, скорее всего, приведет
к команде с непосредственной адресацией. Теперь становится ясно, что атрибут
должен задавать режим (mode) для множителя, слагаемого или выражения, то
есть определять, где хранится его значение и как к нему обратиться. Подобный
атрибут режим соответствует режиму адресации команд компьютера, а диапазон
его возможных значений зависит от множества режимов адресации целевого ком
пьютера. Каждому режиму адресации соответствует значение атрибута режима
элемента. Атрибут режим также неявно вводится классами объектов. Классы
объектов и режимы элементов частично совпадают. В случае нашей RISC архи
тектуры существуют только три режима адресации:
Режим
элемента
Класс
объекта
Var
Const
Reg
Var
Const
–
Режим адресации
Дополнительные атрибуты
a
a
r
r
a
a
R[r]
Имея это в виду, мы объявляем тип данных Item как запись с полями mode,
type, a и r. Ясно, что атрибутом элемента также является его тип. Но ниже об этом
больше упоминаться не будет, потому что мы будем рассматривать только один
тип – Integer.
Теперь процедуры анализатора выглядят как функции с результатом типа
Item. Однако из соображений программирования вместо функций предполагает
ся использовать именно процедуры с выходным параметром.
Item = RECORD
mode: INTEGER;
type: Type;
a, r: LONGINT;
END
Давайте теперь вернемся к нашему примеру, чтобы продемонстрировать гене
рацию кода для выражения x + 1. Процесс изображен на рис. 10.1. Преобразова
ние Var Item в Reg Item сопровождается выдачей команды LDW, а преобразова
ние Reg Item и Const Item в Reg Item сопровождается выдачей команды ADDI.
86
Выражения и присваивания
Рис. 10.1. Генерация элементов и команд для выражения x + 1
Отметим подобие типов Item и Object. Оба они описывают объекты, но если
тип Object представляет объявленные, именованные объекты, видимость которых
выходит за пределы их объявлений, то тип Item описывает объекты, которые все
гда жестко связаны со своей синтаксической конструкцией. Поэтому настоятель
но рекомендуется не создавать объекты типа Item динамически (в куче), а объяв
лять их как локальные параметры и переменные.
PROCEDURE factor(VAR x: Item);
BEGIN
IF sym = ident THEN find(obj); Get(sym); x.mode := obj.class;
x.a := obj.adr; x.r := 0
ELSIF sym = number THEN x.mode := Const; x.a := val; Get(sym)
ELSIF sym = lparen THEN
Get(sym); expression(x);
IF sym = rparen THEN Get(sym) ELSE Mark(" )
") END
ELSIF ...
END
END factor;
PROCEDURE term(VAR x: Item);
VAR y: Item; op: INTEGER;
BEGIN factor(x);
WHILE (sym = times) OR (sym = div) DO
op := sym; Get(sym); factor(y); Op2(op, x, y)
END
END term;
PROCEDURE SimpleExpression(VAR x: Item);
VAR y: Item; op: INTEGER;
BEGIN
IF sym = plus THEN Get(sym); term(x)
ELSIF sym = minus THEN Get(sym); term(x); Op1(minus, x)
ELSE term(x)
END ;
WHILE (sym = plus) OR (sym = minus) DO
Отсроченная генерация кода
87
op := sym; Get(sym); term(y); Op2(op, x, y)
END
END SimpleExpression;
PROCEDURE Statement;
VAR obj: Object; x, y: Item;
BEGIN
IF sym = ident THEN
find(obj); Get(sym); x.mode := obj.class; x.a := obj.adr; x.r := 0;
IF sym = becomes THEN
Get(sym); expression(y);
IF y.mode # Reg THEN load(y) END ;
Put(STW, y.r, 0, x.a)
ELSIF ...
END
ELSIF ...
END
END Statement;
Генерирующие код операторы теперь собраны в двух процедурах Op1 и Op2.
Здесь также используется принцип отсроченной генерации кода, чтобы избежать
выдачи арифметических команд, когда компилятор сам может выполнить эту
операцию. Это тот случай, когда оба операнда являются константами. Такой ме
тод известен как свертка констант (constant folding).
PROCEDURE Op1(op: INTEGER; VAR x: Item); (* x := op x *)
VAR t: LONGINT; BEGIN
IF op = minus THEN
IF x.mode = Const THEN x.a := –x.a
ELSE
IF x.mode = Var THEN load(x) END ;
Put(MVN, x.r, 0, x.r)
END
...
END
END Op1;
PROCEDURE Op2(op: INTEGER; VAR x, y: Item); (* x := x op y *)
BEGIN
IF (x.mode = Const) & (y.mode = Const) THEN
IF op = plus THEN x.a := x.a + y.a
ELSIF op = minus THEN x.a := x.a – y.a
...
END
ELSE
IF op = plus THEN PutOp(ADD, x, y)
ELSIF op = minus THEN PutOp(SUB, x, y)
...
END
END
END Op2;
88
Выражения и присваивания
PROCEDURE PutOp(cd: LONGINT; VAR x, y: Item);
BEGIN
IF x.mode # Reg THEN load(x) END ;
IF y.mode = Const THEN Put(cd+MVI, x.r, x.r, y.a)
ELSE
IF y.mode # Reg THEN load(y) END ;
Put(cd, x.r, x.r, y.r); EXCL(regs, y.r)
END
END PutOp;
PROCEDURE load(VAR x: Item);
VAR r: INTEGER;
BEGIN (*x.mode # Reg*)
IF x.mode = Var THEN GetReg(r); Put(LDW, r, x.r, x.a); x.r := r
ELSIF x.mode = Const THEN
IF x.a = 0 THEN x.r := 0 ELSE GetReg(x.r); Put(MOVI, x.r, 0, x.a) END
END;
x.mode := Reg
END load;
Всякий раз, когда вычисляется арифметическое выражение, неизбежно возни
кает опасность переполнения. Поэтому такие вычисления должны быть надежно
защищены. В случае сложения защита может быть оформлена следующим обра
зом:
IF x.a >= 0 THEN
IF y.a <= MAX(INTEGER) – x.a THEN x.a := x.a + y.a ELSE Mark("
ELSE
IF y.a >= MIN(INTEGER) – x.a THEN x.a := x.a + y.a ELSE Mark("
END
") END
") END
Сущность отсроченной генерации кода состоит в том, что код не выдается,
пока не станет ясно, что не существует лучшего решения. Например, операнд не
загружается в регистр, пока не выяснится, что это неизбежно.
Мы даже отказываемся от распределения регистров согласно жесткому прин
ципу стека. Это выгодно в определенных случаях, которые будут объяснены поз
же. Процедура GetReg выдает и резервирует один из свободных регистров. Мно
жество свободных регистров удобно представить глобальной переменной regs.
Конечно, необходимо позаботиться о том, чтобы освобождать регистры, как толь
ко их значение становится ненужным.
PROCEDURE GetReg(VAR r: LONGINT);
VAR i: INTEGER;
BEGIN i := 1;
WHILE (i < 15) & (i IN regs) DO INC(i) END ;
INCL(regs, i); r := i
END GetReg;
Принцип отсроченной генерации кода полезен также во многих других случа
ях, но он становится необходимым, когда дело касается компьютеров со сложны
Индексированные переменные и поля записей
89
ми режимами адресации, для которых должен быть сгенерирован достаточно эф
фективный код за счет умелого использования доступных сложных режимов.
В качестве примера рассмотрим генерацию кода для CISC архитектуры. Обычно
в ней предполагаются команды с двумя операндами, один из которых становится
ее результатом. Рассмотрим выражение u := x + y*z и получим следующую после
довательность команд:
MOV
MUL
ADD
MOV
y, R0
z, R0
x, R0
R0, u
RO := y
RO := R0 * z
RO := R0 + x
u := R0
Она получается за счет отсрочки загрузки переменных до того момента, когда
они должны сливаться с другим операндом. Поскольку команда заносит резуль
тат во второй операнд, последний не может быть фактическим адресом перемен
ной, а может быть только временной переменной, обычно регистром. Команда ко
пирования не выдается до тех пор, пока не выяснится, что это неизбежно.
Побочный эффект такой меры состоит в том, что, например, простое присваива
ние x := вообще исключает пересылку в регистр, а производится непосредствен
но командой копирования, которая увеличивает эффективность и в то же время
уменьшает длину кода:
MOV
y, x
x :=
10.3. Индексированные переменные
и поля записей
До сих пор мы имели дело только с простыми переменными в выражениях и
присваиваниях. Обращение к элементам структурных переменных – массивов и
записей – требует выбора элемента согласно вычисленному индексу или иден
тификатору поля соответственно. Синтаксически идентификатор переменной
сопровождается одним или несколькими селекторами. Это отражается в син
таксическом анализаторе вызовом процедуры selector в процедурах factor и
StatSequence:
find(obj); Get(sym); x.mode := obj.class; x.a := obj.adr; x.r := 0; selector(x)
Процедура selector обрабатывает не один селектор, но, если нужно, всю цепоч
ку селекторов. Из ее текста видно, что здесь также используется атрибут type опе
ранда x.
PROCEDURE selector(VAR x: Item);
VAR y: Item; obj: Object;
BEGIN
WHILE (sym = lbrak) OR (sym = period) DO
IF sym = lbrak THEN
Get(sym); expression(y);
IF x.type.form = Array THEN Index(x, y) ELSE Mark("
") END ;
90
Выражения и присваивания
IF sym = rbrak THEN Get(sym) ELSE Mark("]?") END
ELSE Get(sym);
IF sym = ident THEN
IF x.type.form = Record THEN
FindField(ob), x.type.fields); Get(sym);
IF obj # guard THEN Field(x, obj)
ELSE Mark("
")
END
ELSE Mark("
")
END
ELSE Mark("
?")
END
END
END
END selector;
Адрес выбираемого элемента вычисляется по формулам, приведенным в раз
деле 8.3. В случае идентификатора поля адрес вычисляется компилятором и рав
няется сумме адреса переменной и смещений ее полей.
PROCEDURE Field(VAR x: Item; y: Object); (* x := x.y *)
BEGIN INC(x.a, y.val); x.type := y.type
END Field;
В случае индексированной переменной код выдается согласно формуле
adr(a[k]) = adr(a) + k * size(T)
Здесь a обозначает переменную массив, k – индекс и T – тип элементов масси
ва. Вычисление индекса требует двух команд: умноженный на размер индекс до
бавляется к регистровой составляющей адреса. Пусть индекс хранится в регистре
R.j, и пусть адрес массива хранится в регистре R.i.
MULI
ADD
j, j, size(T)
j, i, j
Процедура Index выдает приведенный выше код для индекса, проверяет, дей
ствительно ли индексированная переменная – массив, и, если индекс – константа,
вычисляет адрес элемента.
PROCEDURE Index(VAR x, y: Item); (* x := x[y] *)
VAR z: Item;
BEGIN
IF y.type # intType THEN Mark("
") END ;
IF y.mode = Const THEN
IF (y.a < 0) OR (y.a >= x.type.len) THEN Mark("
x.a := x.a + y.a * x.type.base.size
ELSE
IF y.mode # Reg THEN load(y) END ;
Put(MULI, y.r, y.r, x.type.base.size);
Put(ADD, y.r, x.r, y.r); EXCL(regs, x.r); x.r := y.r
") END;
Индексированные переменные и поля записей
91
END;
x.type := x.type.base
END Index;
Мы можем теперь привести код, получающийся для следующего программно
го фрагмента, который содержит одно и двумерные массивы:
PROCEDURE P1;
VAR i, j : INTEGER;
a: ARRAY 4 OF INTEGER;
b: ARRAY 3 OF ARRAY 5 OF INTEGER;
BEGIN i := a[j]; i := a[2]; i := a[i+j]; i := b[i][j];
i := b[2][4]; i := a[a[i]]
END P1.
LDW
MULI
ADD
LDW
STW
LDW
STW
LDW
LDW
ADD
MULI
ADD
LDW
STW
LDW
MULI
ADD
LDW
MULI
ADD
LDW
STW
LDW
STW
LDW
MULI
ADD
LDW
MULI
ADD
LDW
STW
0, base, –8
0,0,4
0, base, 0
1,0,–24
a
1, base, –4
0, base,–16
0, base, –4
0, base, –4
1,base, –8
0,0,1
0,0,4
0, base, 0
1,0,–24
1,base, –4
1.base, –4
0, 0, 20
0, base, 0
1, base,–8
1,1,4
1,0,1
0, 1,–84
0, base, –4
0, base, –28
0, base, –4
0, base, –4
0,0,4
0, base, 0
1,0,–24
1,1,4
1,base, 1
0,1,–24
0, base, –4
adr –4, –8
adr –24
adr –84
i := a[j]
i
i:=a[2]
i := a[i+j];
i+j
i
i:=b[i][j]
j
b
i
i:=b[2][4]
i := a[a[i]]
Заметим, что правильность индекса может быть проверена, только если индекс –
константа, то есть если он имеет уже известное значение. В противном случае ин
92
Выражения и присваивания
декс не может быть проверен до момента начала выполнения программы. Хотя
проверка, конечно, избыточна в правильных программах, опускать ее не рекомен
дуется. Она вполне оправдана для сохранения структуры массива. Однако разра
ботчик компилятора должен попытаться достичь ее предельной эффективности.
Проверка принимает форму оператора
IF (k < 0) OR (k >= n) THEN HALT END
где k – индекс и n – длина массива. Для нашего виртуального компьютера мы про
сто заводим соответствующую команду. В других же случаях должна быть найде
на подходящая последовательность команд. Нужно учесть следующее: так как
в Обероне нижняя граница массива всегда 0, достаточно одного сравнения, если
значение индекса считается беззнаковым целым числом. Это так, потому что от
рицательные значения в дополнительном коде содержат 1 в знаковом разряде, де
лая беззнаковое значение больше самого большого (знакового) целого значения.
В связи с этим процедура Index дополняется генерацией команды CHK, кото
рая приводит к завершению вычисления в случае недопустимого индекса.
IF y.mode # Reg THEN load(y) END ;
Put(CHKI, y.r, 0, x.type.base.len);
Put(MULI, y.r, y.r, x.type.base.size);
В заключение приводится пример программы с вложенными структурами дан
ных. Из него ясно видно, как специальная обработка констант в селекторах упро
щает код для вычисления адресов. Сравните полученный код для переменных с
индексом выражением и с индексом константой. Команды CHK ради краткости
опущены.
PROCEDURE P2;
TYPE R0 = RECORD x, y: INTEGER END;
Rl = RECORD u: INTEGER;
v: ARRAY 4 OF R0;
w: INTEGER
END ;
VAR i, j, k: INTEGER;
s: ARRAY 2 OF Rl;
BEGIN k := s[i].u; k := s[l].w;
k := s[i].v[j].x; k := s[l].v[2].y;
s[0].v[i].y := k
END P2.
LDW
MULI
ADD
LDW
STW
LDW
STW
LDW
0, base, –4
0, 0, 40
0, base, 0
1, 0, –92
l, base, –12
0, base, –16
0, base, –12
0, base, –4
i
s[i].u
k
s[l].w
i
offset 0
offset 4
offset 36
adr–4,–8,–12
adr–92
Индексированные переменные и поля записей
MULI
ADD
LDW
MULI
ADD
LDW
STW
LDW
STW
LDW
MULI
ADD
LDW
STW
0,
0,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
1,
1,
0, 40
base, 0
base, –8
1,8
0,1
1,–88
base, –12
base, –28
base, –12
base, –4
0, 8
base, 0
base, –12
0, –84
93
j
s[i].v[j].x
s[l].v[2].y
i
k
s[0].v[i].y
Желание держать машинно зависимые части компилятора отдельно от ма
шинно независимых частей наводит на мысль о том, чтобы собрать все генери
рующие код операторы в виде процедур в отдельном модуле. Мы назовем этот
модуль OSG и представим его интерфейс. Он содержит несколько процедур гене
ратора кодов, с которыми мы уже столкнулись. Остальные будут объясняться
в главах 11 и 12.
DEFINITION OSG;
IMPORT OSS, Texts, Fonts, Display;
CONST Head = 0; Var = 1; Par = 2; Const = 3; Fid = 4;
= 5; Proc = 6;
SProc = 7; Boolean = 0; Integer = 1; Array = 2; Record = 3;
TYPE Object = POINTER TO ObjDesc;
ObjDesc = RECORD
class, lev: INTEGER;
next, dsc: Object;
type: Type;
name: OSS.Ident;
val: LONGINT;
END;
Type = POINTER TO TypeDesc;
TypeDesc = RECORD
form: INTEGER;
fields: Object;
base: Type;
size, len: INTEGER; END;
Item = RECORD
mode, lev: INTEGER;
type: Type; a: LONGINT;
END;
VAR boolType, intType: Type;
curlev, pc: INTEGER;
PROCEDURE FixLink (L: LONGINT);
PROCEDURE IncLevel (n: INTEGER);
94
Выражения и присваивания
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
PROCEDURE
END OSG.
MakeConstltem (VAR x: Item; typ: Type; val: LONGINT);
Makeltem (VAR x: Item; y: Object);
Field (VAR x: Item; y: Object);
Index (VAR x, y: Item);
Opl (op: INTEGER; VAR x: Item);
Op2 (op: INTEGER; VAR x, y: Item);
Relation (op: INTEGER; VAR x, y: Item);
Store (VAR x, y: Item);
Parameter (VAR x: Item; ftyp: Type; class: INTEGER);
CJump (VAR x: Item);
BJump (L: LONGINT);
FJump (VAR L: LONGINT);
Call (VAR x: Item);
IOCall (VAR x, y: Item);
Header (size: LONGINT);
Enter (size: LONGINT);
Return (size: LONGINT);
Open;
Close (VAR S: Texts.Scanner; globals: LONGINT);
10.4. Упражнения
10.1. Усовершенствуйте компилятор Оберон 0 таким образом, чтобы команды
умножения и деления заменялись более быстрыми командами сдвига и маскиро
вания, когда множитель или делитель являются степенью 2.
10.2. Усовершенствуйте компилятор Оберон 0 таким образом, чтобы код обра
щения к элементам массива включал проверку значения индекса на соответствие
диапазону, заданному в объявлении массива.
10.3. Упростится ли компиляция присваиваний Оберона, если в них левую и
правую части поменять местами, например e =: v?
10.4. Рассмотрите в Обероне случай многократного присваивания e =: v0 =:
v1 =: ... =: vn. Реализуйте его. Создает ли определение его семантики какие нибудь
проблемы?
10.5. Замените определение выражений Оберона определением Алгола 60 (см.
упражнение 2.1) и реализуйте его. Обсудите достоинства и недостатки двух опре
делений.
Глава 11
Условные и циклические
операторы и логические
выражения
11.1. Сравнения и переходы ...... 96
11.2. Условные и циклические
операторы ................................. 97
11.3. Логические операции ...... 101
11.4. Присваивание
логическим переменным ......... 105
11.5. Упражнения ..................... 106
96
Условные и циклические операторы и логические выражения
11.1. Сравнения и переходы
Условные и циклические операторы реализуются с помощью команд перехода,
называемых также командами ветвления. В качестве первого примера рассмот
рим самую простую форму условного оператора:
IF x = y THEN StatSequence END
Его отображение в последовательность команд очень простое:
IF x = y
THEN StatSequence
END
L
EQL x, y
BF L
code(StatSequence)
...
Наши соображения опять основываются на стековой архитектуре. Команда
EQL проверяет два операнда на равенство и заменяет их в стеке логическим ре
зультатом. Следующая команда ветвления BF (переход, если ложь) переходит на
метку L, если этот результат есть FALSE, и удаляет его из стека. Подобно EQL опре
деляются команды условного перехода для отношений <=, <, >=, # и >.
Однако, к сожалению, такие дружественные компиляторам компьютеры не
имеют широкого распространения. Чаще всего это обычные компьютеры, чьи ко
манды перехода зависят от результата сравнения значения регистра с 0. Обозна
чим их как BNE (переход, если не равно), BLT (переход, если меньше), BGE (пере
ход, если больше или равно), BLE (переход, если меньше или равно) и BGT
(переход, если больше). Последовательность кодов, соответствующая приведен
ному выше примеру, становится такой:
IF x = y
THEN StatSequence
END
L
code(Ri := x – y)
BNE L
code(StatSequence)
...
Использование вычитания (x – >= 0 взамен x >= y) таит в себе скрытую
ловушку: вычитание может привести к переполнению, приводящему к останов
ке программы или к ошибочному результату. Поэтому вместо вычитания
используется специальная команда сравнения CMP, которая не приводит к пере
полнению, но при этом корректно определяет, равна ли разность нулю, положи
тельна она или отрицательна. Результат обычно сохраняется в специальном ре
гистре, называемом кодом условия, который состоит из двух битов N и Z,
указывающих, является ли разность отрицательной и нулевой соответственно.
В этом случае все команды условного перехода неявно обращаются к этому ре
гистру как к аргументу.
IF x = y
THEN StatSequence
END
L
CMP x, y
BNE L
code(StatSequence)
...
Условные и циклические операторы
97
11.2. Условные и циклические операторы
Теперь возникает вопрос: как логическое значение может быть представлено зна
чением типа Item? В случае стековой архитектуры ответ прост: так как результат
сравнения находится в стеке, как любой другой результат, никакого специального
значения Item.mode не требуется. Однако команда CMP требует дальнейших раз
мышлений. Сначала мы ограничимся рассмотрением простых вариантов сравне
ний без последующих логических операций.
В случае архитектуры с командой CMP в элементе, представляющем результат
сравнения, необходимо указать, какой регистр содержит вычисленную разность и
какое отношение соответствует этому сравнению. Для последнего требуется новый
атрибут; мы назовем новый режим Cond, а его новый атрибут (поле записи) – c.
Отображение отношений на значения поля задается следующим образом:
=
<
<=
0
2
4
#
>=
>
1
3
5
Конструкция со сравнениями – это выражение. Его синтаксис таков:
expression = SimpleExpression [("="|"#"|"<"|"<="|">"|">=") SimpleExpression].
Соответствующая ему процедура синтаксического анализа легко расширяется
следующим образом:
PROCEDURE expression(VAR x: Item);
VAR y: Item; op: INTEGER;
BEGIN SimpleExpression(x);
IF (sym >= eql) & (sym <= gtr) THEN
Op := sym; Get(sym); SimpleExpression(y); Relation(op,x,y)
END;
END expression ;
PROCEDURE Relation(op: INTEGER; VAR x,y: Item);
BEGIN
IF (x.type.form # Integer) OR (y.type.form # Integer) THEN Mark(“
”)
ELSE
IF (y.mode = Const) & (y.a = 0) THEN load(x) ELSE PutOp(CMP,x,y) END;
x.c = op – eql; EXCL(regs,x.r); EXCL(regs,y.r)
END;
x.mode := Cond; x.type := boolType
END Relation;
Шаблон кода, представленный в начале этой главы, приводит к соответствую
щему фрагменту программы синтаксического анализа для обработки конструк
ции IF в процедуре StatSequence:
ELSIF sym = if THEN
Get(sym); expression(x); CJump(x);
IF sym = then THEN Get(sym) ELSE Mark("THEN?") END;
98
Условные и циклические операторы и логические выражения
StatSequence; Fixup(x.a)
IF sym = end THEN Get(sym) ELSE Mark("END?") END;
Процедура CJump(x) генерирует необходимую команду перехода согласно ее
параметру x.c таким образом, что переход происходит, если указанное условие не
выполняется.
Здесь становится очевидной трудность, которая свойственна всем однопро
ходным компиляторам: значения адресов переходов еще неизвестны в момент
выдачи команд перехода. Эта проблема решается добавлением адреса команды
перехода в качестве атрибута генерируемого элемента типа Item. Данный атрибут
используется позже, когда адрес перехода становится известным и можно завер
шить его формирование с истинным адресом. Это называется закреплением (fixup)
адреса. Простое решение возможно, только если код размещается в глобальном
массиве, элементы которого всегда доступны. Но это невозможно, если выдавае
мый код сохраняется непосредственно на диске. Для представления адреса неза
вершенной команды перехода мы используем поле элемента a.
PROCEDURE CJump(VAR x,y: Item);
BEGIN
IF x.type.form = Boolean THEN
Put(BEQ + negated(x.c), x.r, 0, 0); EXCL(regs, x.r); x.a := pc–1;
ELSE OSS.Mark(“Boolean?”); x.a := pc
END;
END CJump;
PROCEDURE negated(cond: LONGINT: LONGINT);
BEGIN
IF ODD(cond) THEN RETURN cond–1 ELSE RETURN cond+1 END
END negated;
PROCEDURE Fixup(L: LONGINT);
BEGIN code[L] := code[L] DIV 10000H * 10000H + pc – L
END Fixup;
Процедура CJump выдает сообщение об ошибке, если x не имеет тип BOOLEAN.
Отметим, что в командах перехода используются адреса относительно адреса са
мой команды (относительно PC), поэтому используется значение pc – L.
Наконец, мы должны показать, как компилируется условный оператор самого
общего вида; его синтаксис таков:
"IF" expression "THEN" StatSequence
{"ELSIF" expression "THEN" StatSequence}
["ELSE" StatSequence]
"END"
а соответствующие ему шаблоны кода таковы:
IF expression THEN
code(expression)
Bcond L0
Условные и циклические операторы
StatSequence
ELSIF expression THEN
StatSequence
ELSIF expression THEN
StatSequence
...
ELSE StatSequence
END
99
code(StatSequence)
BR L
L0 code(expression)
Bcond L1
code(StatSequence)
BR L
L1 code(expression)
Bcond L2
code(StatSequence)
BR L
Ln code(StatSequence)
L ...
Отсюда операторы синтаксического анализатора могут быть получены как
часть процедуры StatSeqence. Несмотря на то что число конструкций ELSIF и, сле
довательно, меток L1, L2,...,Ln может оказаться произвольным, достаточно един
ственной переменной элемента кода x. Для каждого экземпляра ELSIF ей присваи
вается новое значение.
ELSIF sym = if THEN
Get(sym); expression(x); Cjump(x);
IF sym = then THEN Get(sym) ELSE Mark(“THEN?”) END ;
StatSequence; L := 0;
WHILE sym = elsif DO
Get(sym); FJump(L); Fixup(x,a); expression(x); Cjump(x);
IF sym = then THEN Get(sym) ELSE Mark(“THEN?”) END ;
StatSequence
END;
IF sym = else THEN Get(sym); Fjump(L); Fixup(x,a); StatSequence
ELSE Fixup(x,a)
END;
FixLink(L);
IF sym = end THEN Get(sym) ELSE Mark(“END?”) END
...
PROCEDURE FJump(VAR L: LONGINT);
BEGIN Put(BEQ, 0, 0, L); L := pc – 1
END FJump
Однако здесь возникает новая ситуация, когда на последнюю метку L ссылает
ся не один единственный переход, но целое множество, а именно столько, сколько
ветвлений IF и ELSIF в выражении. Проблема изящно решается, если хранить
ссылки списка незавершенных команд перехода непосредственно в самих коман
дах, а переменную L сделать началом этого списка. Ссылки устанавливаются па
раметром процедуры Put, вызываемой в FJump. Этого достаточно, чтобы заменить
процедуру Fixup на FixLink, в которой просматривается весь список команд для
закрепления в них адреса перехода. Существенно то, что переменная L объявляет
ся локально в процедуре StatSequence синтаксического анализатора, потому что
100
Условные и циклические операторы и логические выражения
операторы могут быть вложенными, что приводит к рекурсивной активации. В
этом случае одновременно существует несколько экземпляров переменной L,
представляющих разные списки.
PROCEDURE FixLink(VAR L: LONGINT);
VAR L1: LONGINT;
BEGIN
WHILE L # 0 DO
L1 := code[L] MOD 10000H; Fixup(L); L := L1
END
END FixLink;
Компиляция оператора WHILE очень похожа на компиляцию простого услов
ного оператора IF. В дополнение к первому условному переходу вперед здесь необ
ходим безусловный переход назад. Синтаксис и соответствующий шаблон кода
таковы:
WHILE expression DO
L0
StatSequence
END
L1
code(expression)
Bcond L1
code(StatSequence)
BR L0
...
Отсюда мы выводим соответствующую расширенную процедуру синтаксичес
кого анализа:
ELSIF sym = while THEN
Get(sym); L := pc; expression(x); Cjump(x);
IF sym = do THEN Get(sym) ELSE Mark("DO?") END ;
StatSequence; Bjump(L); Fixup(x,a);
IF sym = end THEN Get(sym) ELSE Mark("END?") END
PROCEDURE BJump(L: LONGINT);
BEGIN Put(BEQ, 0, 0, L – pc)
END BJump
В заключение приведем сгенерированный код для двух операторов с перемен
ными i и j:
IF i < j THEN i := j THEN i := 1 ELSE i := 2 END ;
WHILE i > 0 DO i := i – 1 END
4
8
12
16
20
24
28
32
36
LDW
LDW
CMP
BGE
STW
BEQ
LDW
LDW
CMP
0, base,
1, base,
0, 0, 1
3
0, base,
10
0, base,
1, base,
0, 0, 1
–4
–8
i
j
–4
(
i := 0
(
–4
–8
3
10
28)
64)
Логические операции
40
44
48
52
56
60
64
68
72
76
80
84
88
BNE
MOVI
STW
BEQ
MOVI
STW
LDW
BLE
LDW
SUBI
STW
BEQ
...
4
0, 0, 1
0, base,
3
0, 0, 2
0, base,
0, base,
5
0, base,
0, 0, 1
0, base,
–5
–4
–4
–4
101
(
4
56)
i := 1
(
3
64)
i := 2
(
5
88)
–4
–4
i := i – 1
(
5
64)
11.3. Логические операции
Конечно, было бы заманчиво обрабатывать логические выражения таким же спо
собом, как и арифметические. Но, к сожалению, это во многих случаях приводит
не только к неэффективному, но даже к неправильному коду. Причина кроется
в определении логических операций, а именно:
p OR q = if p then TRUE else q
p & q = if p then q else FALSE
Это определение предполагает, что не обязательно вычислять второй операнд
q, если результат однозначно определяется значением первого операнда p. Опре
деления языков программирования идут даже дальше, утверждая, что в таких
случаях второй операнд вообще не должен вычисляться. Подобное правило ус
танавливается для того, чтобы второй операнд можно было оставить неопреде
ленным, дабы в противном случае не вызвать аварийного завершения программы.
Самый распространенный пример использования указателя x:
(x # NIL) & (x^.size > 4)
Поэтому логические выражения с логическими операциями принимают вид
условных операторов (или, точнее, условных выражений) и, следовательно, к ним
применимы те же самые методы компиляции, что и для условных операторов. Как
показывает следующий пример, логические выражения и условные операторы
объединяются. Оператор
IF (x <= y) & (y < z) THEN S END
компилируется так же, как его эквивалент
IF x <= y THEN IF y < z THEN S END END
С целью получения подходящего шаблона кода рассмотрим сначала следую
щее выражение, содержащее три отношения, связанные операцией &. Мы опреде
ляем желаемый шаблон кода таким, как показано ниже, принимая во внимание
в данный момент только левый шаблон. Идентификаторы a, b, ... , f обозначают
102
Условные и циклические операторы и логические выражения
числовые значения. Метки T и F обозначают адреса назначения истинной и лож
ной ветвей соответственно.
(a < b) & (c < d) & (e < f)
CMP a, b
BGE F
CMP c, d
BGE F
CMP e, f
BGE F
(T)
CMP a, b
BGE F
CMP c, d
BGE F
CMP e, f
BLT T
(F)
Рис. 11.1. Шаблоны кода для логической операции &
В левом шаблоне команда условного перехода выдается для каждой операции
&. Переход выполняется, если предшествующее ему условие ложно (F переход).
Это происходит по команде BGE – для отношения <, BNE – для отношения = и т. д.
Если мы рассмотрим задачу генерации требуемого кода, можем увидеть, что
процедура синтаксического анализа term, которая, как известно, обрабатывает
арифметические слагаемые, должна быть слегка расширена. В частности, перед
обработкой второго операнда должна быть выдана команда перехода, а по ее окон
чании должен быть закреплен адрес перехода. Первая задача решается процеду
рой Op1, последняя – процедурой Op2.
PROCEDURE term(VAR x: Item);
VAR y: Item; op: INTEGER;
BEGIN factor(x);
WHILE (sym >= times) & (sym <= and) DO
op := sym; Get(sym);
IF op = and THEN Op1(op, x) END ;
factor(y); Op2(op, x, y)
END
END term;
PROCEDURE Op1(op: INTEGER; VAR x: Item); (* x := op x *)
VAR t: LONGINT;
BEGIN
IF op = minus THEN ...
ELSIF op = and THEN
IF x.mode # Cond THEN loadBool(x) END;
PutBR(BEQ + negated(x.c), x.a); EXCL(regs, x.r); x.a := pc–1
END
END Op1;
Логические операции
103
Если первый логический операнд представляется элементом x с режимом
Cond, то в текущем положении x есть TRUE, и, значит, следом должны идти коман
ды вычисления второго операнда. Однако если элемент x не находится в режиме
Cond, его нужно перевести в этот режим. Эта задача выполняется процедурой
loadBool. Мы предполагаем, что значение FALSE представляется как 0. Тогда, если
x есть 0, а значение атрибута = 1, активируется команда BEQ.
PROCEDURE loadBool(VAR x: Item);
BEGIN
IF x.type.form # Boolean THEN OSS.Mark("Boolean?") END ;
load(x); x.mode := Cond; x.c := 1
END loadBool;
Операция OR обрабатывается аналогично с той лишь разницей, что переходы
выполняются, когда соответствующие им условия истинны (T переход). Коман
ды приведены в двойном списке со ссылками на поле b элемента кода. Постусло
вием последовательности операндов, связанных операцией OR, будет FALSE. Сно
ва рассмотрим только левый столбец шаблона кода:
(a < b) OR (c < d) OR (e < f)
CMP a, b
BLT T
CMP c, d
BLT T
CMP e, f
BLT T
(F)
CMP a, b
BLT T
CMP c, d
BLT T
CMP e, f
BGE F
(T)
Рис. 11.2. Шаблоны кода для логической операции OR
Теперь рассмотрим реализацию операции отрицания. Оказывается, что в рам
ках представленной схемы здесь вообще не нужно выдавать никаких команд. Долж
но быть только инвертировано значение условия в поле элемента кода и перестав
лены местами списки F переходов и T переходов. Результат операции отрицания
для обоих выражений с операциями & и OR показан в правых колонках шаблонов
кода на рис. 11.1 и 11.2. Необходимые процедуры расширяются следующим обра
зом:
PROCEDURE SimpleExpression(VAR x: Item);
VAR y: Item; op: INTEGER;
BEGIN term(x);
WHILE (sym >= plus) & (sym <= or) DO
op := sym; Get(sym);
104
Условные и циклические операторы и логические выражения
IF op = or THEN Op1(op, x) END ;
term(y); Op2(op, x, y)
END
END SimpleExpression;
PROCEDURE Op1(op: INTEGER; VAR x: Item); (* x := op x *)
VAR t: LONGINT;
BEGIN
IF op = minus THEN ...
ELSIF op = not THEN
IF x.mode # Cond THEN loadBool(x) END ;
x.c := negated(x.c); t := x.a; x.a := x.b; x.b := t
ELSIF op = and THEN
IF x.mode # Cond THEN loadBool(x) END ;
PutBR(BEQ + negated(x.c), x.a); EXCL(regs, x.r);
x.a := pc–1; FixLink(x.b); x.b := 0
ELSIF op = or THEN
IF x.mode # Cond THEN loadBool(x) END ;
PutBR(BEQ + x.c, x.b); EXCL(regs, x.r);
x.b := pc–1; FixLink(x.a); x.a := 0
END
END Op1;
При компиляции выражений с операциями & и OR нужно следить за тем, чтобы
перед каждой операцией & выполнялось условие P, а перед каждой OR выполня
лось ~P. Соответствующие операциям списки команд перехода (T список для &, F
список для OR) должны быть просмотрены, а адреса переходов в них закреплены.
Это делается при вызовах процедуры FixLink в Op1. В качестве примеров рассмот
рим выражения
(a < b) & (c < d)) OR ((e < f) & (g < h)
(a < b) OR (c < d)) & ((e < f) OR (g < h)
и соответствующие им коды:
CMP a, b
BGE F0
CMP c, d
BLT T
F0 CMP e, f
BGE F
CMP g, h
BGE F
(T)
CMP a, b
BLT T0
CMP c, d
BGE F
T0 CMP e, f
BLT T
CMP g, h
BGE F
(T)
Рис. 11.3. Шаблоны кода
для логических операций & и OR
Присваивание логическим переменным
105
Может так случиться, что список для подвыражения соединяется со списком
для всего выражения (см. F ссылку в шаблоне для & на рис. 11.3). Это соединение
выполняется процедурой merged(a, b), выдающей в качестве результата конкате
нацию списков своих аргументов. Она вызывается в процедуре Op2.
PROCEDURE Op2(op: INTEGER; VAR x, y: Item); (* x := x op y *)
BEGIN
IF (x.type.form = Integer) & (y.type.form = Integer) THEN
...
ELSIF (x.type.form = Boolean) & (y.type.form = Boolean) THEN
IF y.mode # Cond THEN loadBool(y) END ;
IF op = or THEN x.a := y.a; x.b := merged(y.b, x.b); x.c := y.c
ELSIF op = and THEN x.a := merged(y.a, x.a); x.b := y.b; x.c := y.c
END
ELSE ...
END;
END Op2;
11.4. Присваивание логическим
переменным
Компиляция присваивания логической переменной q, конечно, сложнее, чем
обычно кажется. Причиной служит режим Cond, который должен быть преобра
зован в значение 0 или 1. Это достигается следующим шаблоном кода:
T ADDI
BEQ
F ADDI
L STW
0, 0, 1
L
0, 0, 0
0, q
Из за этого простое присваивание q := x < y превращается в ужасно длинную
последовательность команд. Однако следует отдавать себе отчет в том, что логи
ческие переменные (обычно называемые флагами) встречаются (должны встре
чаться) нечасто, хотя в действительности тип BOOLEAN относится к фундамен
тальным. Было бы неправильно добиваться оптимальной реализации редко
используемых операций ценой усложнения программы. В то же время очень
важно, чтобы часто встречающиеся случаи обрабатывались наилучшим спо
собом.
Тем не менее мы обрабатываем присваивания логическому элементу, не нахо
дящемуся в режиме Cond, как особый случай, а именно как обычное присваивание
без использования переходов. Следовательно, присваивание p := q дает в резуль
тате ожидаемую последовательность кодов
LDW 1, 0, q
STW 1, 0, p
106
Условные и циклические операторы и логические выражения
Поэтому процедура Store становится такой:
PROCEDURE Store(VAR x, y: Item); (* x := y *)
BEGIN ...
IF y.mode = Cond THEN
FixLink(y.b); GetReg(y.r); Put(MOVI, y.r, 0, 1); PutBR(BEQ, 2);
FixLink(y.a); Put(MOVI, y.r, 0, 0)
ELSIF y.mode # Reg THEN load(y)
END ;
IF x.mode = Var THEN Put(STW, y.r, x.r, x.a)
ELSE Mark("
")
END ;
EXCL(regs, x.r); EXCL(regs, y.r)
END Store;
11.5. Упражнения
11.1. Превратите язык Оберон 0 в его вариант Оберон D, переопределив услов
ный оператор и оператор цикла следующим образом:
statement = ...
"IF" guardedStatements {"|" guardedStatements} "FI" |
"DO" guardedStatements {"|" guardedStatements} "OD" .
guardedStatements = condition "." statement {";" statement} .
Новая форма оператора
IF B0.S0 | B1.S1 | ... | Bn.Sn FI
будет означать, что из всех истинных условий (логических выражений) Bi выбира
ется наугад одно, и выполняется соответствующая ему последовательность опе
раторов Si. Если ни одно из условий не истинно, выполнение программы прекра
щается. Любая последовательность операторов Si будет выполняться только в том
случае, если соответствующее условие Bi истинно. Поэтому говорят, что Bi являет
ся предохранителем Si.
Оператор
DO B0.S0 | B1.S1 | ... | Bn.Sn OD
будет означать, что пока среди условий Bi есть истинные, случайным образом вы
бирается одно из них, и выполняется соответствующая ему последовательность
операторов Si. Этот процесс завершается, как только все Bi оказываются ложными.
И здесь Bi играют роль предохранителя. DO OD – циклический недетерминиро
ванный оператор. Внесите в компилятор необходимые изменения.
11.2. Добавьте в Оберон 0 и в его компилятор оператор FOR:
statement = [assignment | ProcedureCall |
IfStatement | WhileStatement | ForStatement.
ForStatement = "FOR" identifier ":=" expression "TO" expression
["BY" expression] "DO" StatementSequence "END".
Упражнения
107
Выражение, предшествующее символу TO, задает начальное, а следующее за
ним – конечное значение переменной счетчика, обозначенной идентификатором.
Выражение после BY задает шаг счетчика. В случае его отсутствия значение шага
равно 1.
11.3. Обдумайте реализацию оператора CASE в Обероне (см. приложение A.2).
Его основное свойство состоит в том, что он использует таблицу адресов перехода
для вариантов и индексируемую команду перехода.
Глава 12
Процедуры и концепция
локализации
12.1. Организация памяти
во время выполнения ..............
12.2. Адресация переменных ...
12.3. Параметры ......................
12.4. Объявления и вызовы
процедур .................................
12.5. Стандартные процедуры .
12.6. Процедуры функции .......
12.7. Упражнения .....................
110
112
114
116
121
122
123
110
Процедуры и концепция локализации
12.1. Организация памяти
во время выполнения
Процедуры, которые также иногда называют подпрограммами, являются, воз
можно, самым важным инструментом для структурирования программ. Так как
они встречаются часто, необходимо добиться, чтобы их реализация была эффек
тивной. Реализация основана на команде перехода, которая сохраняет текущее
значение PC и, таким образом, точку возврата после завершения процедуры, когда
это значение восстанавливается в регистре PC.
Сразу возникает вопрос о том, где именно должен сохраняться адрес возврата.
Во многих компьютерах он хранится в регистре, и мы тоже приняли такое реше
ние в нашем RISC процессоре. Это гарантирует предельную эффективность, так
как исключаются дополнительные обращения к памяти. Но необходимость сохра
нять значение регистра в памяти перед следующим вызовом процедуры неизбеж
на, так как иначе старый адрес возврата будет затираться, вследствие чего адрес
возврата первого вызова будет утерян. В реализации компилятора это значение
регистра связи должно сохраняться в начале каждого вызова процедуры.
Очевидное решение для хранения связи – стек, по той причине, что активации
процедур оказываются вложенные, причем процедуры завершаются в порядке,
обратном их вызовам. Поэтому память для адресов возврата должна действовать
согласно принципу LIFO (Last In First Out: последним вошел – первым вышел),
что приводит к фиксированным кодовым последовательностям в начале и в конце
каждой процедуры. Их называют прологом и эпилогом процедуры. Здесь мы будем
использовать R13 в качестве указателя стека SP и R14 в качестве регистра связи
LNK. R15 определяется как счетчик команд PC.
P
BSR P
PSH LNK, SP, 4
POP LNK, SP, 4
RET LNK
LNK –
LNK –
Этот шаблон кода верен при условии, что команда BSR помещает адрес возвра
та в R14. Отметим, что это свойство аппаратуры (глава 9), тогда как использова
ние R13 в качестве указателя стека – лишь программное соглашение, установлен
ное проектом компилятора или базовой операционной системой. При каждом
запуске системы R14 должен получать значение указателя на область памяти, от
веденную под стек.
Алгол 60 ввел фундаментальное понятие локальных переменных. Оно означа
ло, что каждый объявленный идентификатор имел ограниченную область види
мости и существования. В Паскале (а также в Обероне) такой областью служит
тело процедуры. Строго говоря, переменные могут быть объявлены локальными
в процедуре таким образом, что они видимы и существуют только внутри этой
процедуры. Отсюда следует, что для таких локальных переменных память авто
матически выделяется при входе в процедуру и освобождается по завершении
Организация памяти во время выполнения
111
процедуры. Поэтому локальные переменные разных процедур могут разделять
одну и ту же область памяти, но, конечно, не одновременно.
На первый взгляд кажется, что эта схема наносит определенный ущерб эффек
тивности механизма вызова процедуры. Однако, к счастью, это не так, потому что
блоки памяти для наборов локальных переменных, как и для адресов возврата,
могут выделяться по принципу стека. В сущности, адрес возврата тоже можно
считать (скрытой) локальной переменной, и вполне естественно использовать
один и тот же стек для переменных и адресов возврата. Эти блоки памяти называ
ют записями активации процедуры (procedure activation records), или кадрами ак
тивации (activation frames). Освобождение блока по завершении процедуры дос
тигается просто установкой указателя стека в его значение до вызова процедуры.
Следовательно, выделение и освобождение локальной памяти оптимально эф
фективно.
Адреса локальных переменных, генерируемых компилятором, всегда назнача
ются относительно базового адреса соответствующего кадра активации. Так как
в программах большинство переменных – локальные, их адресация также должна
быть высокоэффективной. Это достигается за счет резервирования регистра для
хранения базового адреса и использования того факта, что исполнительный адрес
равен сумме значения регистра и поля адреса команды (режим относительной ре
гистровой адресации, register relative addressing mode). Зарезервированный ре
гистр называют указателем кадра (frame pointer, FP). Эти соображения учтены
в следующем прологе и эпилоге, где R12 исполняет роль указателя кадра:
P
PSH
PSH
MOV
SUBI
LNK, SP, 4
FP, SP, 4
FP, 0, SP
SP, SP, n
LNK –
(push link)
FP –
FP := SP
SP := SP – n (n =
MOV
POP
POP
RET
SP, 0, FP
FP, SP, 4
LNK, SP, 4
LNK
SP := FP
FP –
LNK –
)
(pop FP)
Кадры активации последовательно вызываемых проце
дур связываются в список своими базовыми адресами.
Список называется динамической связью (dynamic link), по
тому что он отражает динамическую последовательность
активаций процедур. Его начало находится в регистре ука
зателе кадра FP (см. рис. 12.1).
Состояние стека до и после вызова процедуры показано
на рис. 12.2. Отметим, что эпилог возвращает стек в его
первоначальное состояние, удаляя адрес возврата и начало
списка динамической связи.
Если мы внимательно рассмотрим необходимость двух
указателей SP и FP, то можем прийти к выводу, что FP на са
мом деле лишний, потому что смещения адресов перемен
Рис. 12.1. Список
кадров активации
в стеке
112
Процедуры и концепция локализации
Рис. 12.2. Состояние стека до и после вызова процедуры
ных могли бы вычисляться относительно SP вместо FP. Однако это предположение
верно, если размеры всех переменных известны во время компиляции. В случае от
крытых (динамических) массивов это не так, что станет видно позже. Но очевидно,
что сохранение второго указателя (FP) требует при каждом вызове процедуры и
возврате из нее дополнительных обращений к памяти, которые нежелательны.
Для повышения эффективности и, в частности, для уменьшения длины цепо
чек команд как пролога, так и эпилога компьютеры с более сложными командами
имеют специальные команды, соответствующие прологу и эпилогу. Подтвержде
нием тому – следующие два примера; второй имеет специальные регистры, пред
назначенные для указателей SP и FP. Однако количество необходимых команд ос
тается тем же.
Motorola 680x0
BSR P
LINK D14, n
UNLNK D14
RTD
National Semiconductor 32x32
BSR P
ENTER n
EXIT
RET
12.2. Адресация переменных
Напомним, что локальная переменная адресуется относительно базового адреса
кадра активации, содержащего данную локальную переменную, а базовый адрес
содержится в регистре FP. Последний, однако, предназначается только для после
днего активного кадра и, следовательно, только для тех переменных, которые от
носятся к процедуре, из которой к ним обращаются. Во многих языках програм
мирования объявления процедур могут быть вложенными, что позволяет
обращаться к переменным, которые локальны в некоторой процедуре, но не в той,
где к ним обращаются. Следующий пример демонстрирует ситуацию, когда про
цедура R локальна в Q, а Q и S локальны в P:
PROCEDURE P;
VAR x: INTEGER;
P
x
0
1
Адресация переменных
113
PROCEDURE Q;
VAR y: INTEGER;
Q
y
1
2
PROCEDURE R;
VAR z: INTEGER;
BEGIN x := y + z
END R;
BEGIN R
END Q;
R
z
2
3
PROCEDURE S;
BEGIN Q
END S;
S
1
BEGIN Q; S
END P;
Проследим цепочку вызовов P → Q → R. Заманчиво предположить, что при об
ращении к переменным x, y или z в R их базовый адрес может быть получен переме
щением по динамической цепочке. Число шагов при этом было бы разностью между
уровнями обращения и объявления переменной. Эта разность равна 2 для x, 1 для y,
0 для z. Но это предположение неверно. В процедуру R можно также попасть по
цепочке вызовов P → S → Q → R, как показано на рис. 12.3. И тогда обращение
к переменной x за два шага привело бы нас к кадру активации S вместо P.
Рис. 12.3. Динамические и статические связи в стеке
Очевидно, что необходим второй список записей активации, который отража
ет статический порядок вложенности, а не динамический порядок вызовов. Сле
довательно, при каждом вызове процедуры должна создаваться вторая ссылка.
Теперь в дополнение к адресу возврата и динамической связи так называемый
след процедуры (procedure mark) содержит элемент статической связи (static link).
Статическая ссылка процедуры P указывает на запись активации процедуры, со
держащей P, то есть процедуры, в которой P объявлена локально. Необходимо от
114
Процедуры и концепция локализации
метить, что этот указатель не нужен для глобальных процедур, потому что гло
бальные переменные адресуются непосредственно, то есть без использования ба
зового адреса. И хотя этот случай типичен и большинство процедур объявляются
глобально, дополнительная сложность, вызываемая наличием статической связи,
вполне приемлема. С некоторым допущением абсолютная адресация глобальных
переменных может считаться особым случаем адресации локальных переменных,
приводящей к повышению эффективности.
Наконец, отметим, что доступ к переменным посредством списка статических
связей (переменных промежуточного уровня) менее эффективен, чем доступ
к строго локальным переменным, поскольку каждый шаг по списку требует до
полнительных обращений к памяти. Для устранения потери эффективности было
предложено и реализовано несколько решений. В конечном счете все они предпо
лагают отображение статического списка в множество базовых регистров. Мы
считаем это оптимизацией «не в том месте». Во первых, регистры – это ограни
ченный ресурс, которым не следовало бы так легко разбрасываться. А во вторых,
затраты на копирование элементов связок в регистры при каждом вызове и воз
врате могут оказаться несколько дороже экономии, в частности потому, что обра
щения к переменным промежуточного уровня на практике встречаются весьма
редко. Поэтому такая оптимизация может обернуться полной своей противопо
ложностью.
В целях простоты и удобочитаемости компилятора Оберон 0, текст которого
приведен в приложении C, обработка локальных переменных промежуточного
уровня в нем не реализована.
Глобальные переменные имеют фиксированные адреса, которые тоже следова
ло бы рассматривать относительно базы кадра. Их абсолютные значения опреде
ляются на этапе загрузки кода, то есть после компиляции, но до выполнения про
граммы. Следовательно, выдаваемый при компиляции объектный код мог бы
сопровождаться списком адресов команд, которые обращаются к глобальным пе
ременным. Загрузчик должен добавить к этим адресам базовый адрес соответ
ствующего кадра глобальных переменных. Эта операция закрепления адресов
может быть опущена, если компьютер имеет в качестве счетчика команд регистр
адреса. Наш RISC процессор именно таков, предоставляя доступ к PC посред
ством R15. Кадр глобальных переменных размещается непосредственно перед
кадром кода. Следовательно, адреса глобальных переменных используют R15
в качестве базового адреса, и из смещения переменной должен вычитаться адрес
текущей команды.
12.3. Параметры
Параметры определяют интерфейс между вызывающей и вызываемой процеду
рами. Говорят, что параметры на вызывающей стороне фактические, а на вызыва
емой – формальные. Последние – в действительности только заглушки, вместо
которых подставляются фактические параметры. По существу, подстановка – это
присваивание фактического значения формальной переменной. Это значит, что
Параметры
115
каждый формальный параметр представлен переменной, связанной с процеду
рой, и каждый вызов процедуры сопровождается рядом присваиваний, называе
мых подстановкой параметров.
В большинстве языков программирования параметры подразделяются, по
крайней мере, на два вида. Первый – параметр значение (параметр, передавае
мый по значению), когда формальной переменной присваивается, согласно назва
нию, значение фактического параметра. Фактический параметр синтаксически
представлен выражением. Второй вид параметра – параметр ссылка (параметр,
передаваемый по ссылке), когда формальной переменной присваивается, также
согласно названию, ссылка на фактический параметр. Очевидно, что фактический
параметр в этом случае должен быть переменной, потому что разрешается присва
ивание формальному параметру, и это присваивание должно относиться к факти
ческой переменной. (Поэтому в Паскале, Модуле и Обероне параметр ссылка на
зывается параметром переменной.) Значение формальной переменной в этом
случае является скрытым указателем, то есть адресом.
Конечно, фактический параметр перед подстановкой должен быть вычислен.
В случае параметров переменных вычисление значения принимает форму иден
тификации переменной, означая, например, вычисление индекса для индексиро
ванных переменных. Но как определить адрес назначения такой подстановки?
Здесь вступает в игру стековая организация памяти. Фактические значения про
сто последовательно помещаются в вершину стека; никаких явных адресов назна
чения не требуется. Рисунок 12.4 показывает состояние стека после размещения
параметров, а также после вызова и пролога.
Рис. 12.4. Подстановка параметров
Теперь становится очевидным, что параметры могут адресоваться относи
тельно адреса кадра FP подобно локальным переменным. Но если локальные
переменные имеют отрицательные смещения, то параметры имеют смещения по
ложительные. Особенно ценно то, что вызванная процедура обращается за пара
метрами именно туда, куда они были помещены вызывающей процедурой. Выде
116
Процедуры и концепция локализации
ленное для параметров пространство освобождается в эпилоге просто увеличени
ем значения SP.
MOV
POP
POP
RET
SP, 0, FP
FP, SP, 4
LNK, SP, m + 4
LNK
SP := FP
FP –
LNK –
В случае CISC компьютеров с прологом и эпилогом, представленными специ
альными командами, необходимое увеличение SP выполняется командой возвра
та, имеющей в качестве своего аргумента размер блока параметров (RET m).
12.4. Объявления и вызовы процедур
Процедура для обработки объявлений процедур легко выводится из синтаксиса
с помощью правил построения синтаксического анализатора. Новая запись в таб
лице символов, сгенерированная для объявления процедуры, получает значение
класса Proc, а ее атрибут – текущее значение pc, адрес входа в пролог процедуры.
После чего в таблице символов открывается новая область действия так, чтобы
(1) новые записи для локальных объектов автоматически попадали в эту область
и (2) в конце процедуры локальные объекты легко удалялись, снова открывая
предыдущую область действия. Здесь две процедуры OpenScope и CloseScope
тоже воплощают принцип стека, а связь устанавливается в заголовочном элемен
те (класс Head, поле dsc). Объекты получают дополнительный атрибут lev, озна
чающий уровень вложенности объявленного объекта. Рассмотрим следующие
объявления:
CONST N = 10;
VAR x: T;
PROCEDURE P(x, y: INTEGER); …
Получающаяся таблица символов изображена на рис. 12.5. Указатель dsc ссы
лается на параметры x и y процедуры P.
PROCEDURE ProcedureDecl;
VAR proc, obj: Object;
procid: Ident;
locblksize, parblksize: LONGINT;
PROCEDURE FPSection;
VAR obj, first: Object; tp: Type; parsize: LONGINT;
BEGIN
IF sym = var THEN Get(sym); IdentList(Par, first)
ELSE IdentList(Var, first)
END ;
IF sym = ident THEN
find(obj); Get(sym);
IF obj.class = Typ THEN tp := obj.type
ELSE Mark("
?"); tp := intType
Объявления и вызовы процедур
Рис. 12.5. Таблица символов, представляющая две области действия
END
ELSE Mark("
?"); tp := intType
END ;
IF first.class = Var THEN parsize := tp.size ELSE parsize := 4 END ;
obj := first;
WHILE obj # guard DO
obj.type := tp; INC(parblksize, parsize); obj := obj.next
END
END FPSection;
BEGIN (* ProcedureDecl *) Get(sym);
IF sym = ident THEN
procid := id;
NewObj(proc, Proc); Get(sym); parblksize := 8;
INC(level); OpenScope; proc.val := –1;
IF sym = lparen THEN Get(sym);
IF sym = rparen THEN Get(sym)
ELSE FPSection;
WHILE sym = semicolon DO Get(sym); FPSection END ;
IF sym = rparen THEN Get(sym) ELSE Mark(")?") END
END
END ;
obj := topScope.next; locblksize := parblksize;
WHILE obj # guard DO
obj.lev := curlev;
IF obj.class = Par THEN
DEC(locblksize, 4) ELSE locblksize := locblksize – obj.type.size
END ;
obj.val := locblksize; obj := obj.next
117
118
Процедуры и концепция локализации
END ;
proc.dsc := topScope.next;
IF sym = semicolon THEN Get(sym) ELSE Mark(";?") END;
locblksize := 0; declarations(locblksize);
WHILE sym = procedure DO
ProcedureDecl;
IF sym = semicolon THEN Get(sym) ELSE Mark(";?") END
END ;
proc.val := pc; Enter(locblksize);
IF sym = begin THEN Get(sym); StatSequence END ;
IF sym = end THEN Get(sym) ELSE Mark("END?") END ;
IF sym = ident THEN
IF procid # id THEN Mark("
") END ;
Get(sym)
ELSE Mark("
?")
END ;
Return(parblksize – 8); CloseScope; DEC(level)
END
END ProcedureDecl;
Внутри тела процедуры параметры значения обрабатываются как локальные
переменные. Их записи в таблице идентификаторов имеют класс Var. Для пред
ставления параметров ссылок вводится новый класс Par. Адреса (смещения) фор
мальных параметров определяются по следующей формуле, где последний пара
метр pn получает наименьшее смещение, равное размеру следа процедуры (8).
Размер параметров ссылок всегда 4 и совпадает с размером адреса.
adr(pi) = size(pi+1) + ... + size(pn) + 8
К сожалению, это значит, что смещения не могут быть определены, пока не
будет распознан весь список параметров. Более того, в случае байт адресуемой
памяти всегда выгодно увеличивать или уменьшать указатель стека на величину,
кратную 4, чтобы параметры всегда выравнивались по границе слова. В случае
Оберона 0 нет необходимости обращать особое внимание на это правило, потому
что все типы данных имеют размер, кратный 4.
Локальные объявления обрабатываются процедурой синтаксического анали
затора declarations. Код для пролога выдается процедурой Enter после обработки
локальных объявлений. Выдача эпилога осуществляется процедурой Return в
конце ProcedureDecl.
PROCEDURE Enter(size: LONGINT);
BEGIN
Put(PSH, LNK, SP, 4);
Put(PSH, FP, SP, 4);
Put(MOV, FP, 0, SP);
Put(SUBI, SP, SP, size)
END Enter;
PROCEDURE Return(size: LONGINT);
BEGIN
Объявления и вызовы процедур
119
Put(MOV, SP, 0, FP);
Put(POP, FP, SP, 4);
Put(POP, LNK, SP, size+4);
PutBR(RET, LNK)
END Return;
Процедура Makeltem генерирует элемент типа Item, соответствующий данно
му объекту Object. Здесь нужно принять во внимание различие между адресацией
локальных и глобальных переменных. (Как уже упоминалось, обработка пере
менных промежуточного уровня здесь не выполняется.) Отметим, однако, что па
раметры ссылки (class = Par) требуют косвенной адресации. Так как архитектура
RISC не имеет явного режима косвенной адресации, значение формального пара
метра, то есть адрес фактического, загружается в регистр. Тогда к фактическому
параметру обращаются по этому регистру со смещением 0.
PROCEDURE MakeItem(VAR x: Item; y: Object);
VAR r: LONGINT;
BEGIN x.mode := y.class; x.type := y.type; x.a := y.val;
IF y.lev = 0 THEN x.r := PC ELSIF y.lev = curlev THEN x.r := FP
ELSE Mark("
!"); x.r := 0
END ;
IF y.class = Par THEN GetReg(r); Put(LDW, r, x.r, x.a); x.mode := Var;
x.r := r; x.a := 0
END
END MakeItem;
Вызовы процедур генерируются в уже встречавшейся процедуре StatSequence
с помощью вспомогательных процедур Parameter и Call:
IF sym = ident THEN
find(obj); Get(sym); MakeItem(x, obj); selector(x);
IF sym = becomes THEN ...
ELSIF x.mode = Proc THEN
par := obj.dsc;
IF sym = lparen THEN Get(sym);
IF sym = rparen THEN Get(sym)
ELSE
LOOP expression(y);
IF IsParam(par) THEN Parameter(y,par.type,par.class); par := par.next
ELSE Mark("
")
END ;
IF sym = comma THEN Get(sym)
ELSIF sym = rparen THEN Get(sym); EXIT
ELSIF sym >= semicolon THEN Mark(") ?"); EXIT
ELSE Mark(")
, ?")
END
END
END
END ;
IF obj.val < 0 THEN Mark("
")
120
Процедуры и концепция локализации
ELSIF ~IsParam(par) THEN Call(x)
ELSE Mark("
")
END
...
PROCEDURE Parameter(VAR x: Item; ftyp: Type; class: INTEGER);
VAR r: LONGINT;
BEGIN
IF x.type = ftyp THEN
IF class = Par THEN (*
*)
IF x.mode = Var THEN
IF x.a # 0 THEN GetReg(r); Put(ADDI, r, x.r, x.a) ELSE r := x.r END
ELSE Mark("
")
END ;
Put(PSH, r, SP, 4); EXCL(regs, r) (*push*)
ELSE (*
*)
IF x.mode # Reg THEN load(x) END ;
Put(PSH, x.r, SP, 4); EXCL(regs, x.r)
END
ELSE Mark("
")
END
END Parameter;
PROCEDURE IsParam(obj: Object): BOOLEAN;
BEGIN RETURN (obj.class = Par) OR (obj.class = Var) & (obj.val > 0)
END IsParam;
PROCEDURE Call(VAR x: Item);
BEGIN PutBR(BSR, x.a – pc)
END Call;
Здесь мы молчаливо полагаем, что адреса входов в процедуры уже известны,
когда компилируется вызов. Таким образом, мы заранее исключаем ссылки впе
ред, которые могут возникать, например, в случае взаимных рекурсивных обра
щений. Если снять это ограничение, то адреса вызовов (переходов) вперед долж
ны быть сохранены, чтобы закрепить адреса в командах переходов, когда их места
назначения станут известными. Этот случай подобен закреплению переходов впе
ред в условных и циклических операторах.
В заключение приведем код, сгенерированный для следующей простой проце
дуры:
PROCEDURE P(x: INTEGER; VAR y: INTEGER);
BEGIN x := y; y := x; P(x,y); P(y,x)
END P;
0
4
8
12
PSH LNK, SP, 4
PSH FP, SP, 4
MOV FP, 0, SP
SUB1 SP, SP, 0
Стандартные процедуры
16
20
24
LDW 0, FP, 8
LDW 1, 0, 0
STW 1, FP, 12
x := y
28
32
36
LDW 0, FP, 8
LDW 1, 0, 12
STW 1, FP, 12
y := x
40
44
48
52
56
LDW 0,
PSH 0,
LDW 0,
PSH 0,
BSR
FP, 12
SP, 4
FP, 8
SP, 4
–14
60
64
68
72
76
80
LDW 0,
LDW 1,
PSH 1,
ADDI
PSH 0,
BSR
FP, 8
0, 0
SP, 4
0, FP, 12
SP, 4
–20
84
88
92
96
MOVSP, 0, FP
POP FP, SP, 4
POP LNK, SP, 12
RET LNK
121
x
adr(y)
P(x,y)
y
adr(x)
P(y,x)
–
12.5. Стандартные процедуры
Большинство языков программирования содержат некоторые процедуры и функ
ции, не требующие объявления в программе. Они, как говорят, предопределены и
могут быть вызваны отовсюду, так как всюду видимы. Это такие хорошо извест
ные функции, как, например, абсолютная величина числа (ABS), преобразования
типа (ENTIER, ORD) или часто встречающиеся операторы, которые получили
аббревиатуры и доступны на многих компьютерах как особые команды (INC,
DEC). Общее свойство всех этих так называемых стандартных процедур состоит
в том, что им соответствует либо всего одна команда, либо короткая последова
тельность команд. Поэтому эти процедуры обрабатываются компиляторами со
всем по другому: вызов для них не генерируется, вместо этого необходимые ко
манды вставляются прямо в код. Такие процедуры называются встраиваемыми
(in line). Этот термин обретает смысл только тогда, когда становится понятным
лежащий в его основе способ реализации.
Следовательно, стандартные процедуры выгодно считать отдельным классом
объектов. Отсюда сразу становится очевидной необходимость специальной обра
ботки их вызовов. Для Оберона 0 мы определяем процедуры Read, Write, WriteHex и
WriteLn, которые, с одной стороны, вводят элементарные средства ввода вывода,
а с другой – служат для демонстрации предлагаемой обработки предопределенных
122
Процедуры и концепция локализации
процедур. В таком случае термин «стандартная» – скорее всего, заблуждение, тогда
как «предопределенная» и «встраиваемая» отражают суть дела. При инициализа
ции компилятора соответствующие встраиваемым процедурам записи заносятся
в таблицу символов, а именно в самую внешнюю область видимости, называемую
вселенной, которая всегда остается открытой (см. приложение C). Новый атрибут
класса обозначается SProc, а атрибут val ( для Items) задает нужную процедуру.
IF sym = ident THEN
find(obj); Get(sym); MakeItem(x, obj); selector(x);
IF sym = becomes THEN ...
ELSIF x.mode = Proc THEN ...
ELSIF x.mode = SProc THEN
IF obj.val <= 3 THEN param(y); TestInt(y) END ;
IOCall(x, y)
...
PROCEDURE IOCall(VAR x, y: Item);
VAR z: Item;
BEGIN (*x.mode = SProc*)
IF x.a = 1 THEN (*Read*)
GetReg(z.r); z.mode := Reg; z.type := intType; Put(RD, z.r, 0, 0);
Store(y, z)
ELSIF x.a = 2 THEN(*Write*) load(y); Put(WRD, 0, 0, y.r); EXCL(regs, y.r)
ELSIF x.a = 3 THEN(*WriteHex*) load(y); Put(WRH,0,0,y.r); EXCL(regs, y.r)
ELSE (*WriteLn*) Put(WRL, 0, 0, 0)
END
END IOCall;
В заключительном примере приводятся последовательность из трех операто
ров и итоговый код:
Read(x); Write(x); WriteLn
4
8
12
16
32
READ
STW
LDW
WRD
WRL
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
–4
–4
0
0
x
x
12.6. Процедуры функции
Процедура функция – это процедура, идентификатор которой обозначает одно
временно и алгоритм, и его результат. Она активируется не оператором вызова,
а операндом выражения. Поэтому вызов процедуры функции должен позабо
титься еще и о возврате результата. Таким образом, возникает вопрос, какие ре
сурсы следует использовать.
Если основная наша цель – генерация эффективного кода с минимальным чис
лом обращений к памяти, то регистр – первый кандидат для временного хранения
Упражнения
123
результата функции. Если принимается такое решение, мы должны отказаться от
возможности определения функций со структурным результатом, поскольку
структурные значения не могут храниться в регистре.
Если же такое ограничение считается неприемлемым, то в стеке должно отво
диться место для размещения структурного результата. Обычно оно добавляется
к области параметров кадра активации. Результат функции считается неявным
параметром переменной. В связи с этим SP увеличивается перед тем, как выдается
код для первого параметра.
Итак, все понятия, содержащиеся в языке Оберон 0 и реализованные в его
компиляторе, рассмотрены. Полный текст компилятора приводится в приложе
нии C.
12.7. Упражнения
12.1. Усовершенствуйте компилятор Оберон 0 таким образом, чтобы требование
строгой локальности переменных или их полной глобальности могло быть снято.
12.2. Добавьте в компилятор Оберон 0 стандартные функции, генерирующие
встраиваемый код. Рассмотрите ABS, INC, DEC.
12.3. Замените понятие VAR параметра понятием OUT параметра. OUT пара
метр представляет собой локальную переменную, значение которой присваивает
ся соответствующему ей фактическому параметру при выходе из процедуры. Он
является антиподом параметра значения, когда значение фактического парамет
ра присваивается формальной переменной при входе в процедуру.
Глава 13
Элементарные типы данных
13.1. Типы REAL и LONGREAL ...
13.2. Совместимость между
числовыми типами данных .......
13.3. Тип данных SET ...............
13.4. Упражнения .....................
126
127
129
130
126
Элементарные типы данных
13.1. Типы REAL и LONGREAL
Еще в 1957 году в Фортране целые и вещественные числа считались различными
типами данных. Так было не только потому, что для них нужны были различные
внутренние представления, но и потому, что по общему признанию программист
должен был осознавать, когда можно ждать точных расчетов (а именно для целых
чисел), а когда – только приближенных. То, что с помощью вещественных чисел
может быть получен лишь приближенный результат, может быть понято с учетом
того, что вещественные числа представлены масштабируемыми целыми числами
с фиксированным, конечным количеством цифр. Их тип называется REAL, а дей
ствительное значение x представляется парой целых чисел e и m, как определено
уравнением
x = Be–w × m,
где
1≤m<B
Это так называемое представление с плавающей точкой; e называют показате
лем степени, m – мантиссой. Основание B и смещение w являются фиксированны
ми для всех значений REAL и характеризуют выбранное представление числа. Два
стандарта IEEE представлений с плавающей точкой устанавливают следующие
значения B и w, а к компонентам e и m добавляется бит s для знака:
Тип
REAL
LONGREAL
B
2
2
w
127
1023
Число битов для e
8
11
Число битов для m Общее число
23
32
52
64
Точный вид этих двух типов, названных в Обероне REAL и LONGREAL, опре
деляется следующими формулами:
x = (–1)s × 2e–127 × 1.m
x = (–1)s × 2e–1023 × 1.m
В следующих примерах приведены представления с плавающей точкой для
нескольких чисел:
Десятичное
1.0
0.5
2.0
10.0
0.1
–1.5
s
0
0
0
0
0
1
e
127
126
128
130
123
127
1.m
1.0
1.0
1.0
1.25
1.6
1.5
Двоичное
0 01111111
0 01111110
0 10000000
0 10000010
0 01111011
1 01111111
00000000000000000000000
00000000000000000000000
00000000000000000000000
01000000000000000000000
10011001100110011001101
10000000000000000000000
Шестнадцатеричное
3F80 0000
3F00 0000
4000 0000
4120 0000
3DC CCCD
BFC0 0000
Два примера иллюстрируют случай LONGREAL:
1.0
0.1
0 1023 1.0
0 1019 1.6
0 01111111111 00000000 ... 00000000 3FF0 0000 0000 000
0 01111111011 10011001 ... 10011010 3FB9 9999 9999 999A
Эта логарифмическая форма по существу исключает значение для 0. Значение
0 считается особым случаем и представляется всеми битами, равными 0. С точки
зрения свойств чисел оно представляет собой особый случай разрыва. Кроме того,
стандарты IEEE устанавливают два дополнительных специальных значения:
Совместимость между числовыми типами данных
127
e = 0 (с m ≠ 0) и e = 255 (или e = 1023), которые считаются недопустимыми ре
зультатами и называются NaN (Not a Number – не число).
Обычно программист не должен беспокоиться об этих спецификациях, да и про
ектировщика компилятора они тоже не затрагивают. Типы REAL и LONGREAL
образуют абстрактные типы данных, обычно интегрированные в аппаратные сред
ства, которые предоставляют набор команд, применяемых к представлению с пла
вающей точкой. Если этот набор полный, то есть охватывает все основные чис
ловые операции, то представление можно рассматривать как скрытое, так как
никакие другие программируемые операции от него не зависят. Во многих компь
ютерах команды для операций с плавающей точкой используют специальный на
бор регистров. Причина в том, что часто для реализации всех команд с плавающей
точкой используются отдельные сопроцессоры, так называемые устройства
с плавающей точкой (floating point unit, FPU), которые и содержат этот набор реги
стров с плавающей точкой.
13.2. Совместимость между числовыми
типами данных
Значения всех переменных с числовым типом данных суть числа. Поэтому нет
видимой причины не объявить их всех совместимыми при присваиваниях. Но, как
уже было сказано, числа разных типов представляются в компьютерах разными
последовательностями битов. Следовательно, всякий раз, когда число типа T0 при
сваивается переменной типа T1, должно быть выполнено преобразование его
представления, которое занимает некоторое конечное время. Тогда возникает
вопрос, нужно ли, дабы не отвлекать внимания, скрывать его от программиста или
сделать его явным, так как оно влияет на эффективность программы. Последнее
достигается путем объявления различных типов как несовместимых и обеспече
ния явных предопределенных функций преобразования типа.
В любом случае, чтобы быть полным, набор команд компьютера должен также
содержать команды преобразования, которые превращают целые числа в числа
с плавающей точкой, и наоборот. То же самое сохраняется на уровне языка про
граммирования.
В Обероне различные типы данных могут использоваться в одном арифмети
ческом выражении, которое называется смешанным. В этом случае компилятор
должен вставить неявные команды преобразования, приводя представление опе
рандов к применяемой арифметической операции.
Определение Оберона обеспечивает не два, а целое множество числовых типов
данных. Они упорядочены в том смысле, что больший тип содержит все значения,
принадлежащие меньшему типу. Это понятие называют включением типов (type
inclusion):
SHORT ⊆ INTEGER ⊆ LONGINT ⊆ REAL ⊆ LONGREAL
128
Элементарные типы данных
По определению, тип результата выражения равен большему из типов операн
дов. Этим правилом определяются команды преобразования, которые вставляют
ся компилятором.
Несколько более мягкие правила совместимости типов устанавливаются для
присваивания, мягкие по отношению к строгому правилу о том, что тип перемен
ной приемника должен совпадать с типом выражения источника. Оберон опреде
ляет, что тип переменной должен включать тип выражения. Таким образом, допу
стимы следующие примеры:
VAR i, j: INTEGER; k: LONGINT; x, y: REAL; z: LONGREAL;
i := j;
k := i;
z := x;
x := i;
INTEGER ⊆ INTEGER
INTEGER ⊆ LONGINT
REAL ⊆ LONGREAL
INTEGER ⊆ REAL
(
)
(INTEGER LONGINT)
(REAL LONGREAL)
(INTEGER REAL)
Преобразования между целочисленными типами просты и эффективны, так
как они состоят только из сдвига знакового бита. Гораздо сложнее преобразова
ния из целого в представление с плавающей точкой. Они состоят из такой норма
лизации мантиссы, чтобы 1 ≤ m < 2, и упаковки знака, показателя степени и ман
тиссы в одно слово. Обычно эта задача решается соответствующей командой.
Однако если тип выражения больше типа переменной, то присваивание иногда
невозможно, так как присваиваемое значение может быть вне диапазона значе
ний, заданного типом переменной. Поэтому перед преобразованием необходим
контроль диапазона значений при выполнении. Такое преобразование должно
стать видимым в исходном языке посредством явной функции. Оберон предос
тавляет следующие функции:
SHORT
ENTIER
INTEGER SHORTINT
LONGINT INTEGER
LONGREAL REAL
REAL LONGINT
LONGREAL LONGINT
ENTIER(x) выдает наибольшее целое число, не превосходящее x.
В Обероне правила преобразования типа просты и понятны, но таят в себе ло
вушку. Иногда при перемножении двух операндов типа INTEGER(REAL) желатель
но получить результат типа LONGINT (LONGREAL). Однако в присваиваниях
k: = i * j + k
z: = x * y + z
согласно данным правилам, произведение имеет тип INTEGER (REAL), и оно пре
образуется к своему «длинному» типу только при последующем сложении. Чтобы
вычислить произведение с большей точностью, перед умножением требуется при
нудительное преобразование:
k := LONG(i) * LONG(j) + k
z := LONG(x) * LONG(y) + z
Простота правил совместимости – огромное достижение проектировщика
компилятора. Принцип обработки выражения ясно описан выше. Только про
Тип данных SET
129
цедуры компиляции для выражений и слагаемых, а также для присваивания дол
жны быть дополнены анализом вариантов. Чем больше разных числовых типов,
тем больше вариантов нужно различать.
13.3. Тип данных SET
Блоки памяти в компьютерах состоят из небольшого количества битов, которые
могут интерпретироваться по разному. Они могут представлять целые числа со
знаком или без знака, числа с плавающей точкой или логические данные. Вопрос
о способе введения в языки программирования высокого уровня логических по
следовательностей битов оставался спорным в течение долгого времени. Предло
жение использовать их в качестве множеств принадлежит Хоару [6].
Предложение привлекательно, потому что множество – математически убеди
тельная абстракция. Оно удачно представляется в компьютере своей характерис
тической функцией F. Если x – множество элементов из базового упорядоченного
множества М, то F(x) –последовательность логических величин bi со значениями
«i содержится в x». Если мы выберем слово (состоящее из N битов) для представ
ления значений типа SET, то базовое множество будет состоять из целых чисел 0, 1,
..., N 1. Число N обычно так мало, что диапазон значений для типа SET весьма
ограничен. Однако основные операции над множествами – пересечение, объеди
нение и разность – чрезвычайно эффективны. Примеры множеств, представлен
ных последовательностями битов слова длиной N:
x
N–1 ... 7 6 5 4 3 2 1 0
{0, 2, 4, 6,...}
{0, 3, 6,...}
{}
0
0
0
... 0 1 0 1 0 1 0 1
... 0 1 0 0 1 0 0 1
... 0 0 0 0 0 0 0 0
Операции для множеств в Обероне реализуются логическими командами, дос
тупными на каждом компьютере. Это возможно благодаря следующим свойствам
характеристической функции. Отметим, что мы используем нотацию Оберона
для множественных операций, то есть x+y для объединения и x*y для пересе
чения:
⊆i
⊆i
⊆i
⊆i
((i
((i
((i
((i
⊆
⊆
⊆
⊆
x+y ) : (i ⊆ x) OR (i ⊆ y))
x*y ) : (i ⊆ x) & (i ⊆ y))
x–y ) : (i ⊆ x) & ~(i ⊆ y))
x/y ) : (i ⊆ x) ≠‚ (i ⊆ y))
Следовательно, команда OR может использоваться для объединения мно
жеств, AND – для пересечения множеств и XOR – для симметрической разности.
В результате получается очень эффективная реализация, потому что операция
выполняется над всеми элементами (битами) одновременно (параллельно). При
мерами над базовым множеством {0, 1, 2, 3} являются:
{0, 1} + {0, 2} = {0, 1, 2}
{0, 1} * {0, 2} = {0}
0011 OR 0101 = 0111
0011 &
0101 = 0001
130
Элементарные типы данных
{0, 1} – {0, 2} = {1}
{0, 1} / {0, 2} = {1, 2}
0011 & ~ 0101 = 0010
0011 XOR 0101 = 0110
В завершение покажем код для множественного выражения (a+b)*(c+d):
LDW
LDW
OR
LDW
LDW
OR
AND
1,
2,
1,
2,
3,
2,
1,
0,
0,
1,
0,
0,
2,
1,
a
b
2
c
d
3
2
Проверка членства (включения) i IN x реализуется проверкой бита. Если такой
команды нет, цепочка битов должна быть определенным образом сдвинута с по
следующей проверкой знакового разряда.
Тип SET особенно полезен, если базовое множество состоит из порядковых
номеров набора символов (CHAR). Эффективность в этом случае несколько сни
жается, потому что для представления такого множества обычно требуется 256 би
тов (32 байта). Даже в 32 битовых компьютерах для выполнения операций над
таким множеством требуется 8 логических команд.
13.4. Упражнения
13.1. Расширьте язык Оберон 0 и его компилятор типом данных REAL (и/или
LONGREAL) с арифметическими операциями +, –, * и /. RISC архитектура долж
на быть соответственно расширена рядом команд с плавающей точкой и набором
регистров с плавающей точкой. Выберите одну из следующих альтернатив:
a. Тип результата операции всегда совпадает с типом операндов. Типы
INTEGER и REAL не могут смешиваться. Но при этом существуют две
функции преобразования ENTIER(x) и REAL(i).
b. Операнды типов INTEGER и REAL (и LONGREAL) могут смешиваться
в выражениях.
Сравните сложность компиляторов для этих двух вариантов.
13.2. Расширьте язык Оберон 0 и его компилятор типом данных SET с опера
циями + (объединение), * (пересечение), – (разность) и с отношением IN (член
ство). Кроме того, следующим дополнительным синтаксисом вводятся конструк
торы множеств. Как вариант,выражения в конструкторах множеств могут
ограничиваться константами.
factor
= number | set |... .
set
= "{" [element {"," element}] "}".
element = expression [".." expression].
13.3. Расширьте язык Оберон 0 и его компилятор типом данных CHAR с фун
кцией ORD(ch) (порядковый номер символа ch в множестве символов) и CHR(k)
(k й символ в множестве символов). Переменная типа CHAR занимает один байт
в памяти.
Глава 14
Открытые массивы,
указательный
и процедурный типы
14.1. Открытые массивы ..........
14.2. Динамические структуры
данных и указатели ..................
14.3. Процедурные типы ..........
14.4. Упражнения .....................
132
133
136
138
132
Открытые массивы, указательный и процедурный типы
14.1. Открытые массивы
Открытый массив – это параметр массив, длина которого неизвестна (открыта)
во время компиляции. Здесь мы впервые сталкиваемся с ситуацией, когда размер
необходимого блока памяти не задан. В случае передачи параметра по ссылке ре
шение достаточно простое, потому что никакого выделения памяти не требуется,
а вызываемой процедуре передается просто ссылка на фактический массив.
Однако для проверки границ индекса при обращении к элементам открытого
параметра массива все же нужно знать его длину. Поэтому в дополнение к адресу
массива процедуре передается также его длина. В случае многомерного открытого
массива длина тоже необходима для вычисления адресов элементов. Следова
тельно, длина должна предполагаться для массива любой размерности. Блок, со
стоящий из адреса массива и его длин, называется дескриптором массива. Рас
смотрим следующий пример:
VAR a: ARRAY 10 OF ARRAY 20 OF INTEGER;
PROCEDURE P (VAR x: ARRAY OF ARRAY OF INTEGER);
BEGIN k := x[i]
END P;
P(a)
Дескриптор с тремя входами, помещенный в стек как пара
метр процедуры P (рис. 14.1), и соответствующий ему код выг
лядят следующим образом:
MVI
PSH
ADDI
PSH
ADDI
PSH
BSR
1, 0, 20
1, 30, 4
1, 0, 10
1, 30, 4
1, 0, a
1, 30, 4
P call
R1 := 20
len –
, R14 = SP
R1 := 10
len –
R1 := adr(a)
adr –
Рис. 14.1.
Дескриптор
для открытого
массива
Если открытый параметр массив передается по значению, его значение долж
но быть скопировано в известное и заранее выделенное место в памяти, как и
в случае скалярного значения. Однако эта операция может потребовать значитель
ных затрат, если массив большой. В случае структурных параметров программис
ты должны всегда использовать опцию VAR, если копирование не обязательно.
Конечно, код для операции копирования лучше вставить после пролога про
цедуры, а не в точке вызова. Тогда шаблон кода для вызова становится одинако
вым и для параметров значений, и для параметров переменных, за исключением
того, что для первых операция копирования исключается из пролога.
Отведенная под формальный параметр память, очевидно, содержит не массив,
а дескриптор массива, размер которого известен. Место для копии выделяется на
верхушке стека, а указатель стека увеличивается (или уменьшается) на размер
Динамические структуры данных и указатели
133
массива. В случае многомерных массивов размер вычисляется (при выполнении)
как произведение отдельных длин на размер элемента.
Здесь SP изменяется при выполнении на величину, которая неизвестна при
компиляции. Поэтому в общем случае работать с одним индексным регистром
(SP) невозможно; становится необходим указатель кадра FP.
14.2. Динамические структуры данных
и указатели
Оберон предлагает два вида структур данных: массив (все элементы одного типа,
однородная структура) и запись (разнородная структура). Более сложные структу
ры должны программироваться особым образом, то есть они должны создаваться во
время выполнения программы, поэтому они называются динамическими. Таким об
разом, компоненты структуры создаются одна за другой; память для каждой из них
выделяется отдельно. Они необязательно располагаются в памяти последователь
но. Связи между компонентами задаются явно посредством указателей.
Для реализации такой концепции нужно обеспечить механизм выделения па
мяти во время выполнения. В Обероне он представляется стандартной процеду
рой NEW(x). Она выделяет память динамической переменной и присваивает адрес
выделенного блока переменной указателю x. Из этого следует, что указатели суть
адреса. Обращение к переменной, на которую ссылается указатель, обязательно
косвенное, как в случае с VAR параметрами. На самом деле VAR параметр пред
ставляет собой скрытый указатель. Рассмотрим следующие объявления:
TYPE T = POINTER TO TDesc;
TDesc = RECORD x, y : LONGINT END;
VAR a, b : T;
Код для присваивания a.x := b.y с обращениями по указателям a и b прини
мает вид
LDW
LDW
LDW
STW
1,
2,
3,
2,
FP, b
1, y
FP, a
3, x
R1 := b
R2 := b.y
R3 := a
a.x := R2
Переход от указателя записи к самой записи называется разыменованием.
В Обероне явный оператор разыменования обозначается символом ^. Очевидно,
что a.x – это сокращение более явной формы a^.x. Неявная операция разыменова
ния распознается, когда символу селектора (точке) предшествует не запись, а ука
затель.
Всякий, кто писал программы, которые перегружены действиями с указателя
ми, знает, как легко в них допустить ошибку с катастрофическими последствия
ми. Чтобы объяснить, почему это так, рассмотрим следующие объявления типов:
T0 = RECORD x, y : LONGINT END ;
T1 = RECORD x, y, z : LONGINT END;
134
Открытые массивы, указательный и процедурный типы
Пусть a и b – переменные указатели, и пусть a указывает на запись типа T0,
a b – на запись типа T1. Тогда обозначение a.z указывает на неопределенное зна
чение несуществующей переменной, а присваивание a.z: = b.x сохраняет значе
ние в некоторое неопределенное место, возможно, разрушая другую переменную,
расположенную по этому адресу.
Эта опасная ситуация элегантно устраняется закреплением указателя за опре
деленным типом данных. Это позволяет обеспечить правильность значений ука
зателей во время компиляции без потери эффективности выполнения. Эта блес
тящая идея принадлежит Хоару и впервые реализована в Алголе W [6]. Тип,
с которым связан указатель, называется его базовым типом.
P0 = POINTER TO T0;
P1 = POINTER TO T1;
Теперь компилятор может проверить и гарантировать, что переменной указа
телю p могут быть присвоены только такие значения, которые указывают на пере
менную ее базового типа. Считается, что значение NIL, не указывающее ни на ка
кую переменную вообще, относится ко всем типам указателей. В приведенном
примере обозначение a.z теперь становится неверным, потому что z не поле запи
си типа T0, с которой a связана. Если каждая переменная указатель инициализи
руется значением NIL, то этого достаточно, чтобы предварять каждое обращение
по указателю проверкой его на NIL. В этом случае указатель не указывает ни на
одну переменную, и любое выражение с ним должно быть ошибочным.
Такая проверка весьма проста, но из за частого ее применения производитель
ность падает. От необходимости иметь для нее явный шаблон кода можно увиль
нуть (зло)употреблением механизма защиты памяти, имеющегося на многих ком
пьютерах. В этом случае проверка сводится не к сравнению a = NIL, а, скорее,
к выяснению, будет ли a.z допустимым незащищенным адресом. Если, как обыч
но, NIL представляется адресом 0 и адреса 0...N–1 защищены, то ошибочные NIL
ссылки ловятся, как только их адресные смещения оказываются меньше N. Тем не
менее кажется, что этот метод должен быть вполне приемлемым на практике.
Введение указателей требует нового класса объектов в таблице символов, а так
же нового режима адресации элементов. Оба предполагают косвенную адресацию.
Поскольку VAR параметры также требуют косвенной адресации, а режим косвенно
сти уже существует, естественно воспользоваться им для доступа по ссылке. Од
нако имя Ind оказалось бы теперь более подходящим, чем Par.
Обозначение
x
x^
x^.y
Режим (mode)
Var
Ind
Ind
Следовательно, операция разыменования (обычно подразумеваемая) преобра
зует режим адресации элемента из Var в Ind. Подведем итоги.
1. Понятие указателя легко вписывается в нашу систему контроля совмести
мости типов. Каждый тип указателя связан с базовым типом, а именно,
с типом переменной, на которую он ссылается.
Динамические структуры данных и указатели
135
2. x^ обозначает разыменование, реализуемое с помощью косвенной адре
сации.
3. Указатели сохраняют тип, если они инициализируются значением NIL, а все
обращения по ним предваряются проверкой на NIL.
Память для переменных, к которым обращаются по указателю, выделяется
вызовом процедуры NEW(p). Мы предполагаем ее наличие в операционной систе
ме, поддерживающей среду выполнения программ. Размер блока, который дол
жен быть выделен, определяется базовым типом p.
До сих пор мы пренебрегали проблемой повторного использования памяти.
Для абстрактных программ она действительно не имеет значения, но для конкрет
ных имеет решающее значение, так как память, в сущности, конечна. Современ
ные операционные системы предлагают централизованное управление памятью
со сборкой мусора. Существуют различные схемы повторного использования па
мяти, но здесь мы не будем на них останавливаться. Мы ограничимся единствен
ным вопросом, имеющим значение для разработчика компиляторов: какие дан
ные нужно предоставить сборщику мусора, чтобы в любое время все свободные
блоки памяти могли быть благополучно обнаружены и восстановлены? Перемен
ная больше не нужна, когда на нее нет ссылок, исходящих от объявленных пере
менных указателей. Чтобы определить, существуют ли такие ссылки, сборщик
мусора требует следующие данные:
1) адреса всех объявленных переменных указателей,
2) смещения всех полей в динамически выделенной памяти и
3) размер каждой динамически размещенной переменной.
Эта информация доступна на этапе компиляции, и она должна быть подготов
лена так, чтобы быть доступной для сборщика мусора на этапе выполнения.
В этом смысле компилятор и система должны интегрироваться. Предполагается,
что система включает механизмы управления памятью, в частности распредели
тель памяти NEW и сборщик мусора.
Чтобы сделать эту информацию доступной на этапе выполнения, процедура
NEW не только выделяет блок памяти, но и снабжает его дескриптором типа той
переменной, которой выделена память. Естественно, такой дескриптор должен
быть создан только однажды, так как нет нужды дублировать его для каждого эк
земпляра переменной того же типа. Поэтому блок просто снабжается указателем
на дескриптор типа, который остается невидимым для программиста. Этот указа
тель называют тэгом типа (рис. 14.2).
Дескриптор типа, как легко видеть, сокращенная форма объекта, описывающе
го тип в таблице символов компилятора, ограниченная данными, необходимыми
для повторного использования памяти. Из такой концепции вытекает следующее:
1. Компилятор должен генерировать дескриптор для каждого типа (RECORD)
и добавлять его к объектному файлу.
2. Процедура NEW(p) вдобавок к адресу p получает дополнительный, скрытый
параметр, задающий адрес дескриптора базового типа для указателя p.
136
Открытые массивы, указательный и процедурный типы
Рис. 14.2. Указатель, разыменованная переменная и дескриптор типа
3. Загрузчик программы должен истолковать дополнительную информацию
объектного файла и сгенерировать дескрипторы типов.
Дескриптор типа задает размер переменной и смещение для всех динамичес
ких полей (рис. 14.3).
Рис. 14.3. Переменная с дескриптором типа
Однако всего этого еще недостаточно. Для того чтобы можно было обходить
структуры данных, должны быть известны их корни. Поэтому объектный файл
дополняется списком всех объявленных переменных указателей. Этот список ко
пируется при загрузке в память. Он должен включать скрытые указатели, ссыла
ющиеся на дескрипторы типов. Поскольку дескрипторы не должны генериро
ваться для всех типов данных, Оберон ограничивается только указателями на
записи. С учетом роли записей в динамических структурах данных это оправдано.
14.3. Процедурные типы
Если в языке процедуры могут передаваться как параметры или если они могут
оказываться значениями переменных, то становится необходимым введение про
цедурных типов. Каковы же характеристики таких типов, то есть значений, кото
рые могут принимать переменные такого типа?
Процедурные типы вошли в употребление с появлением Алгола 60. Там они
существовали только в неявном виде. Параметр в Алголе 60 мог быть процедурой
Процедурные типы
137
(формальной процедурой). Однако его тип не указывался; известно только, что
параметр обозначал некоторую процедуру или функцию. Спецификация типа
была недостаточно полной и образовала неудачную брешь в системе типов Алго
ла. В Паскале она была сохранена для совместимости с Алголом. Однако язык Мо
дула 2 уже нуждался в законченной и безопасной спецификации, в нем, помимо
параметров, допускались также переменные с процедурными значениями. Таким
образом, процедурные типы получили тот же статус, что и остальные типы дан
ных. В этом отношении Оберон следует той же концепции, что и Модула 2 [18].
Из чего же состоит эта безопасная спецификация, называемая сигнатурой про
цедуры? Она содержит все спецификации, необходимые для обеспечения совмес
тимости между фактическими и формальными параметрами, а именно их количе
ство, тип каждого параметра, их вид (значение или ссылка) и тип результата в
случае процедуры функции. Следующий пример иллюстрирует сказанное:
PROCEDURE F(x, y : REAL): REAL;
BEGIN
...
END F
PROCEDURE H(f: PROCEDURE(u, v : REAL): REAL);
VAR a, b: REAL;
BEGIN a := f(a + b, a – b)
END H
При компиляции объявления процедуры H проверяется совместимость типов
между (a + b) и u и, соответственно, между (a – b) и v, а также может ли результат f
быть присвоен переменной a. При вызове H(F) проверяется не только совмести
мость типов результатов F и f, но и совместимость типов их параметров, то есть
между u и x и между y и v. Отметим, что идентификаторы u и v должны встречать
ся в программе исключительно как имена формальных параметров формальной
процедуры f. Поэтому фактически они лишние, но могут оказаться полезными
комментариями для читателя, если им даны осмысленные имена.
Паскаль, Модула и Оберон предполагают согласование имен как основу для
установления совместимости типов. В случае процедур параметров было сделано
исключение: достаточно только структурной совместимости. Если бы требова
лось согласование имен, то типу (сигнатуре) каждой процедуры, используемой
в качестве фактического параметра, нужно было бы давать явное имя. При проек
тировании языка такой подход был признан слишком громоздким. Тем не менее
структурная совместимость требует, чтобы компилятор мог сравнивать два спис
ка параметров на соответствие типов.
Таким образом, процедура может быть присвоена переменной при условии со
ответствия друг другу двух списков параметров. Присвоенная процедура активи
руется при обращении к переменной. Вызов будет косвенным. Фактически это
базис объектно ориентированного программирования, где процедуры связаны
с полями переменных записей, называемых объектами. Такие связанные про
цедуры называют методами. В отличие от Оберона методы, однажды объявлен
138
Открытые массивы, указательный и процедурный типы
ные и связанные, не могут быть изменены. Все экземпляры класса обращаются
к одним и тем же методам.
Реализация процедурных типов и методов оказывается удивительно простой,
если пренебречь проблемой контроля совместимости типов. Значение перемен
ной или поля записи процедурного типа является просто адресом входа в присво
енную им процедуру. Это имеет силу только в том случае, если мы потребуем, что
бы присваивались только глобальные процедуры, то есть процедуры, которые не
встроены в некоторый контекст. Такое охотно принимаемое ограничение объяс
няется следующим примером, который нарушает это ограничение. При выполне
нии Q под именем v контекст, содержащий переменные a и b, уже отсутствует.
TYPE T = PROCEDURE (u: INTEGER);
VAR v: T; r: INTEGER;
PROCEDURE P;
VAR a, b: INTEGER;
PROCEDURE Q(VAR x: INTEGER);
BEGIN x := a + b END Q;
BEGIN v := Q
END P;
... P; v(r) ...
14.4. Упражнения
14.1. Расширьте язык Оберон 0 и его компилятор открытыми массивами:
a) для одномерных VAR параметров;
b) для многомерных VAR параметров;
c) для параметров значений.
14.2. Расширьте язык Оберон 0 и его компилятор процедурами функциями:
a) с результатом скалярного типа (INTEGER, REAL, SET);
b) с результатом любого типа.
14.3. Некоторый модуль управляет структурой данных, детали которой дол
жны быть скрыты. Несмотря на это, должна существовать возможность приме
нить любую заданную операцию P ко всем элементам структуры. С этой целью из
экспортируется процедура Enumerate, которая позволяет задавать P в качестве
своего параметра. Возьмем в качестве простого примера операции P подсчет всех
элементов структуры данных и приведем требуемое решение:
PROCEDURE Enumerate(P: PROCEDURE (e: Element));
PROCEDURE CountElements*;
VAR n: INTEGER;
PROCEDURE Cnt(e: Element); BEGIN n := n + 1 END Cnt;
BEGIN n := 0; M.Enumerate(Cnt); Texts.WriteInt(W, n, 6)
END CountElements;
Упражнения
139
К сожалению, это решение нарушает установленное в языке Оберон ограниче
ние, которое заключается в том, что процедуры, используемые как параметры,
должны объявляться глобально. Это вынуждает нас объявить Cnt, а вместе с ней и
переменную счетчик n за пределами CountElements, хотя обе функции опреде
ленно не должны быть глобальными.
Реализуйте процедурные типы так, чтобы упомянутое ограничение могло
быть снято, а предложенное решение стало бы приемлемым. Какова этому цена?
14.4. Наш RISC процессор (см. главу 9) имеет стековые команды «втолкнуть» и
«вытолкнуть» (PSH и POP). Они используются для реализации стековой парадиг
мы кадровой активации процедур. Если мы добавим альтернативную команду
«втолкнуть» PSHu, то вместе с существующей командой POP сможем использовать
ее для реализации FIFO буфера (очереди – Прим. перев.):
PSHu: M[R[b] div 4] := R[a]; INC(R[b], c)
Но каким образом понятие буфера и использование буферных команд могут
быть представлены в языке Оберон 0? Мы предлагаем понятие бегунка (rider).
Предположим, что бегунок r и буфер B объявляются как
VAR B: ARRAY N OF INTEGER;
r: RIDER ON B;
и что бегунки размещаются в регистрах. Тогда пусть для бегунка определяются
следующие операции:
SET(r, B, i)
r^
бегунок устанавливается на B[i]
означает элемент, на котором стоит r. После обращения
или присваивания r перемещается на следущий элемент буфера.
Напишите соответствующий модуль с буфером и процедурами для извлече
ния и сохранения его элементов. Исследуйте преимущества этого понятия, а так
же его проблемы. Реализуйте его как дополнение к компилятору.
14.5. Наш RISC процессор (см. главу 9) является, как говорят, байт ориенти
рованным. Но обмен данными с памятью всегда осуществляется группами по
4 байта, называемыми словами. Адреса слов всегда кратны 4. Младшие два бита
адреса игнорируются.
Давайте воспользуемся байтовой адресацией для распространения понятия
бегунка (см. 14.4) на буфер из байтов (литер). Расширим набор команд RISC двумя
командами PSHB и POPB. Они используют регистр в качестве 4 байтового буфера и
обмениваются очередным словом с памятью только после каждого четвертого об
ращения к буферу. Команда POPB определяется следующим образом:
POPB:
R[a]
8
;
IF R[b] MOD 4 = 0 THEN R[a] := M[R.b DIV 4] END;
R[b] := R[b] + 1
Состояния R[a] и R[b] до и после каждой команды POPB показаны ниже. После
каждой команды очередной байт становится младшим байтом регистра R[a]:
140
Открытые массивы, указательный и процедурный типы
R[a]
R[b]
DCBA
ADCB
BADC
CBAD
0
1
2
3
0
POPB
R[a]
R[b]
DCBA
B
ADCB
C
BADC
D
CBAD
...
1
2
3
0
1
Команда PSHB определяется аналогично:
PSHB:
R[a]
8
;
IF R[b] MOD 4 = 3 THEN M[R.b DIV 4] := R[a] END;
R[b] := R[b] + 1
Перед каждой командой PSHB очередной байт размещается в младшем байте
буферного регистра R[a]. Состояния R[a] и R[b] до и после каждой команды POPB
показаны ниже:
R[a]
R[b]
A
...A
B
A..B
C
BA.C
D
CBAD
0
1
2
3
PSHB
R[a]
R[b]
A...
BA..
CBA.
DCBA
1
2
3
0
Определите конструкцию байтового бегунка в языке Оберон 0 и реализуйте
его как расширение компилятора.
Глава 15
Модули
и раздельная компиляция
15.1. Принцип скрытия
информации ............................
15.2. Раздельная компиляция ..
15.3. Реализация символьных
файлов ....................................
15.4. Адресация внешних
объектов ..................................
15.5. Проверка
конфигурационной
совместимости ........................
15.6. Упражнения .....................
142
143
145
149
150
152
142
Модули и раздельная компиляция
15.1. Принцип скрытия информации
Алгол 60 ввел принципы текстуальной локальности идентификаторов и ограни
ченного времени жизни именованных объектов во время выполнения. Область
видимости идентификатора в тексте называют областью действия (scope) [12], и
она распространяется на весь блок, в котором идентификатор объявлен. Согласно
синтаксису, блоки могут быть вложенными, следовательно, правила о видимости
и областях действия должны уточняться. Алголом 60 устанавливается, что иден
тификаторы, объявленные в блоке B, видимы в самом и во всех блоках, содержа
щихся в B. Но они невидимы в окружении B.
Из этого правила разработчик заключает, что память для локальной в блоке B
переменной x должна быть выделена, как только управление передается B, и мо
жет быть освобождена, как только управление покидает B. Когда управление ока
зывается за пределами B, переменная x не только не видима, но и прекращает
существовать. Важное достоинство состоит в том, что память не остается выде
ленной всем переменным программы.
Однако в некоторых случаях весьма желательно более длительное существо
вание переменной в состоянии невидимости. Тогда при повторной передаче
управления блоку B оказывается, что переменная x появляется в нем с ее преды
дущим значением. Этот специальный случай был обеспечен в Алголе 60 посред
ством собственных (own) переменных. Но это решение, как вскоре обнаружилось,
было весьма неудовлетворительным, в частности в связи с рекурсивными проце
дурами.
Изящное и очень приемлемое решение проблемы собственности было найдено
примерно в 1972 году с введением модульной структуры. Оно было принято
в языках Модула [16], Mesa [11] и позже под названием «пакет» в Аде. Синтакси
чески модуль напоминает процедуру и состоит из локальных объявлений и следу
ющих за ними операторов. Однако, в отличие от процедуры, модуль не вызывает
ся, а его операторы выполняются только раз, а именно когда модуль загружен.
Локально объявленные в модуле объекты являются статическими и существуют,
пока модуль остается загруженным. Операторы в теле модуля служат только для
инициализации его переменных. Эти переменные не видимы вне модуля; на са
мом деле они скрыты. Термин «скрытие информации» был придуман Д. Л. Парна
сом (D. L. Parnas) и стал важным понятием в программной инженерии. Оберон
предоставляет возможность сделать отдельные идентификаторы, объявленные
в модуле, видимыми в его окружении. В этом случае говорят, что эти идентифика
торы экспортируются.
Собственная переменная x, объявленная внутри Алгол процедуры P, теперь
будет объявлятся, как и сама P, локально в модуле . Теперь экспортируется P, но
не x. Детали реализации P, как и сама переменная , скрыты в среде , но x при
этом существует и сохраняет значение между вызовами P.
Желание скрыть определенные объекты и детали особенно оправдано, если
система состоит из различных частей, чьи задачи довольно хорошо разделяются, и
если эти части сами по себе достаточно сложны. Это типично для организацион
Раздельная компиляция
143
ной единицы, которая владеет структурой данных. В этом случае структура дан
ных скрывается внутри модуля и доступна только через экспортируемые проце
дуры. Программист этого модуля устанавливает определенные инварианты, та
кие как условия непротиворечивости, которые управляют структурой данных.
Можно гарантировать, что эти инварианты сохраняются, потому что они не могут
быть нарушены частями системы вне модуля. Как следствие ответственность про
граммиста эффективно ограничивается процедурами внутри модуля. Эта инкап
суляция деталей, ответственная исключительно за указанные инварианты, явля
ется истинной целью скрытия информации и концепции модуля.
Типичные примеры модулей и скрытия информации – файловая система,
скрывающая структуру файлов и их каталогов, лексический анализатор компиля
тора, скрывающий исходный текст и его лексикографическую структуру, или ге
нератор кода компилятора, скрывающий сгенерированный код и структуру целе
вой архитектуры.
15.2. Раздельная компиляция
Было бы заманчиво потребовать, чтобы модули, подобно процедурам, вкладыва
лись друг в друга. Такая возможность предоставляется, например, языком Моду
ла 2. Однако на практике подобная гибкость едва ли полезна. Обычно вполне до
статочно «плоской» модульной структуры. Поэтому мы будем считать все модули
глобальными, а их окружением – весь мир.
Гораздо важнее вложенности модулей возможность их раздельной разработ
ки и компиляции. Очевидно, что последнее возможно, только если модули явля
ются глобальными, то есть невложенными. Причиной такого требования явля
ется тот простой факт, что программное обеспечение никогда не планируется, не
реализуется и не тестируется в строго заданном порядке, напротив, оно разраба
тывается пошагово, вбирая на каждом шаге некоторые дополнения и исправле
ния. Программное обеспечение не «пишется», а растет. В связи этим понятие
модуля имеет фундаментальное значение, потому что позволяет разрабатывать
отдельные модули раздельно в предположении о неизменности их интерфейсов
импорта. Множество экспортируемых объектов фактически образует интер
фейс модуля с его партнерами. Если интерфейс остается неизменным, реализа
ция модуля может быть улучшена (и откорректирована) без необходимости ис
правления и перекомпиляции модулей клиентов. Это реальное обоснование для
раздельной компиляции.
Преимущество такой концепции становится особенно явственным, если про
граммное обеспечение разрабатывается командой разработчиков. Как только дос
тигается соглашение о разделении системы на модули и об их интерфейсах, члены
команды могут независимо продолжить реализацию своих модулей. Даже если на
практике окажется, что последующие изменения в спецификации интерфейсов
вносились довольно редко, упрощение коллективной работы, благодаря раздель
ной компиляции модулей, трудно переоценить. Успешная разработка сложных
систем решительно зависит от концепции модуля и раздельной компиляции.
144
Модули и раздельная компиляция
Тут читатель может подумать, что все это не так ново, что независимое про
граммирование модулей и их связывание загрузчиком программ, как показано на
рис. 15.1, было в повсеместном использовании, начиная с эры ассемблеров и пер
вых компиляторов Фортрана.
Рис. 15.1. Независимая компиляция модулей A и B
Однако думать так – значит игнорировать тот факт, что языки программиро
вания более высокого уровня предлагают значительно более сильную защиту от
ошибок и несовместимостей благодаря понятию статического типа. Эта неоцени
мая, но слишком часто недооцениваемая, выгода пропадает, если контроль сов
местимости типов гарантируется только внутри модулей, а не между ними. Сле
довательно, информация о типах всех импортируемых объектов должна быть
доступна всякий раз, когда модуль компилируется. В отличие от независимой
компиляции (рис. 15.1), где эта информация недоступна, компиляцию с межмо
дульным контролем совместимости типов (рис. 15.2) называют раздельной ком
пиляцией.
Информация об импортируемых объектах – это, в сущности, фрагмент табли
цы символов, описанной в главе 8. Данный фрагмент таблицы символов, преобра
зованный в линейную форму, называется символьным файлом. Компиляция моду
ля A, который импортирует модули B1, ..., Bn (то есть их объекты), теперь требует,
в дополнение к исходному тексту A, символьные файлы B1,...,Bn. В дополнение
к объектному коду (A.obj) генерируется также символьный файл (A.sym).
Рис. 15.2. Раздельная компиляция модулей A и B
Реализация символьных файлов
145
15.3. Реализация символьных файлов
Из предыдущих обсуждений мы можем, во первых, заключить, что компиляция
списка импорта модуля требует чтения символьного файла каждого модуля
в списке. Таблица символов компилируемого модуля заполняется импортируе
мыми символьными файлами. Во вторых, из этого следует, что по окончании ком
пиляции выполняется обход обновленной таблицы символов, чтобы получить
символьный файл с записями, соответствующими каждому элементу таблицы
символов, помеченному для экспорта. Для примера на рис. 15.3 показан фрагмент
таблицы символов при компиляции модуля , импортирующего B. Внутри B иден
тификаторы T и f помечены звездочкой для экспорта.
Рис. 15.3. Таблица символов A с импортом B
Рассмотрим для начала процесс генерации символьного файла M.sym модуля
M. На первый взгляд задача состоит только в обходе таблицы и выдаче в надлежа
щем линеаризованном виде записей, соответствующих каждому помеченному
элементу. Таблица символов, в сущности, является списком объектов с указателя
ми на древовидные структуры типов. В таком случае наиболее подходящим спо
собом будет, пожалуй, линеаризация структур, использующая характерный пре
фикс для каждого элемента. Это иллюстрируется примером на рис. 15.4.
Проблема возникает из за того, что каждый объект содержит как минимум
указатель, ссылающийся на его тип. Запись значений указателей в файл пробле
матична, если не сказать больше. Наше решение заключается в том, чтобы при
просмотре таблицы символов заносить в файл описание типа в момент первого
146
Модули и раздельная компиляция
Рис. 15.4. Линеаризованная форма таблицы символов с двумя массивами
его появления. Таким образом, запись о типе помечается и получает уникальный
ссылочный номер. Номер хранится в дополнительном поле записи типа ObjectDesc.
Если позже на этот тип снова ссылаются, то вместо его структуры выводится его
ссылочный номер.
Такой способ не только позволяет избежать повторов при записи одних и тех
же описаний типа, но решает также проблему рекурсивных ссылок, как показано
на рис. 15.5.
Для ссылочных номеров используются положительные значения. Для указа
ния на то, что ссылочный номер используется впервые и, стало быть, вслед за ним
Рис. 15.5. Циклическая ссылка в типе
Реализация символьных файлов
147
идет описание типа, номер получает знак минус. При чтении символьного файла
таблица типов T строится со ссылками на соответствующие структуры типов.
Если читается положительный ссылочный номер r, то требуемым указателем бу
дет T[r]; если же r отрицательный, то читаются следующие за ним данные о типе,
а указателем, ссылающимся на вновь созданный дескриптор, становится T[–r].
В отличие от данных о других объектах, информация о типе может не только
импортироваться, но и реэкспортироваться. Поэтому необходимо указать модуль,
от которого происходит экспортируемый тип. Чтобы сделать это возможным, мы
используем так называемый якорь модуля. В заголовке каждого символьного
файла есть список объектов якорей, по одному на каждый импортируемый мо
дуль, являющийся реэкспортируемым, то есть содержащим тип, на который ссы
лается экспортируемый объект. Рисунок 15.6 иллюстрирует такую ситуацию; мо
дуль C импортирует модули A и B, в силу чего из B импортируется переменная x,
тип которой происходит из A. Контроль совместимости типов для присваивания
вроде y: = x основывается на предположении, что оба указателя на типы x и y ссы
лаются на один и тот же дескриптор типа. Если это не так, возникает ошибка.
Рис. 15.6. Реэкспорт типа A.T из модуля B
Отсюда мы заключаем, что при компиляции модуля нужны не только табли
цы символов явно импортируемых модулей, но и тех модулей, на типы которых
ссылаются либо прямо, либо косвенно. Это становится поводом для беспокой
ства, потому что компиляция любого модуля может потребовать чтения символь
ных файлов всей иерархии модулей. Таким образом можно опуститься до самых
глубин операционной среды, откуда не импортируются ни переменные, ни проце
дуры, а разве только один единственный тип. Результатом этого может стать не
только избыточная загрузка огромного количества данных, но и большой расход
памяти. Хотя наше беспокойство оправдано, оказывается, что последствия всего
этого менее драматичны, чем может показаться [3]. Дело в том, что большинство
148
Модули и раздельная компиляция
необходимых таблиц символов уже присутствуют по другим причинам. Поэтому
дополнительной работы остается совсем немного. Тем не менее стоит продумать
возможность сокращения такой чрезмерной работы. В первых компиляторах для
Модулы и Оберона применялся следующий метод.
Пусть модуль прямо или косвенно импортирует типы из модулей M0, M1 и т. д.
Решение состоит в том, чтобы включить в символьный файл модуля полные
описания импортируемых типов, избегая таким образом ссылок на вторичные
модули M0, M1 и т. д. Однако такое довольно очевидное решение вызывает неко
торые сложности. В примере на рис. 15.6 символьный файл модуля B, очевидно,
содержит полное описание типа T. Контроль совместимости для присваивания
y := B.x, будучи высокоэффективным, просто сравнивает два указателя типа. По
этому конфигурация, показанная справа на рис. 15.6, уже должна существовать
после загрузки. Ввиду того, что реэкспортируемые типы в символьных файлах
специфицируются не только собственным модулем, при загрузке символьного
файла необходимо проверить, существует ли уже читаемый тип. Это возможно
в случае, когда символьный файл модуля, определяющего тип, уже загружен или
когда тип уже был прочитан при загрузке других символьных файлов.
Упомянем здесь также еще одну связанную с типами небольшую сложность,
которая возникает потому, что типы могут появляться под разными именами
(псевдонимами). Хотя использование псевдонимов является редкостью, опреде
ление языка, к сожалению, допускает их. Они немного выразительнее, если сино
нимы происходят от разных модулей, как показано на рис. 15.7.
Рис. 15.7. Типы с псевдонимами
При загрузке символьного файла модуля B выясняется, что B.T1 и A.T0, оба
указывающие на объект описания типа, должны фактически указывать на один
и тот же дескриптор объекта. Чтобы определить, какой из двух дескрипторов
должен быть уничтожен, а какой оставлен, узлы типа (типа Structure) снабжа
ются обратным указателем на исходный объект типа (типа Object), здесь –
на T0.
Адресация внешних объектов
149
15.4. Адресация внешних объектов
Главное достоинство раздельной компиляции состоит в том, что изменение моду
ля не оказывает негативного влияния на клиентов , если интерфейс остает
ся неизменным. Напомним, что интерфейс состоит из полного набора экспорти
руемых объявлений. Изменения, которые не затрагивают интерфейса, могут
вноситься, так сказать, втайне, причем без ведома программистов, пишущих кли
ентские программы. Такие изменения даже не должны требовать перекомпиля
ции клиентских программ, использующих новые символьные файлы. Истины
ради спешим добавить, что экспортируемые процедуры не должны менять своей
семантики, так как компиляторы не могут наверняка обнаружить таких измене
ний. Следовательно, если мы говорим, что интерфейс остался неизменным, то яв
но относим это к объявлениям типов и переменных, а также к описаниям проце
дур, а к их семантике – только неявно.
Если в некотором модуле неэкспортируемые процедуры и переменные изме
няются, добавляются или удаляются, то их адреса, конечно, также меняются,
и, следовательно, то же самое произойдет с другими, возможно экспортируемыми,
переменными и процедурами. Это приведет к изменению таблицы символов и
в силу этого также к повреждению клиентских модулей. Но это, очевидно, проти
воречит требованиям раздельной компиляции.
Решение этой дилеммы заключается в том, чтобы не включать адреса в сим
вольный файл. Это значит, что адреса должны вычисляться при загрузке и связы
вании модуля. Поэтому вдобавок к своему адресу (для внутримодульного ис
пользования) экспортируемый объект получает уникальный номер. Этот номер
соответствует месту адреса в символьном файле. Как правило, эти номера назна
чаются строго последовательно.
Следовательно, при компиляции клиента ему доступны только специальные
номера модуля, но не адреса. Эти номера, как упомянуто ранее, должны превра
щаться в исполнительные адреса при загрузке. Для этого нужно располагать
знаниями о позициях таких незаполненных адресных полей. Вместо исполь
зования объектного файла со списком всех позиций таких адресов элементы
списка закрепления адресов вставляются в команды на истинные места еще не
известных адресов. Это аналог метода вычисления адресов перехода вперед
(см. главу 11). Если все такие заполняемые адреса собрать в единый список зак
репления, то это будет соответствовать рис. 15.8а. Каждый элемент задается па
рой, состоящей из номера модуля mno и номера записи eno. Проще снабдить
каждый модуль отдельным списком. Тогда в объектном файле потребуется не
один единственный корень списка закрепления, а по одному для каждого списка,
что соответствует рис. 15.8б. На рис. 15.8в показана другая крайность, когда для
каждого импортируемого объекта создается отдельный список закрепления. Ка
кое из трех представленных решений будет выбрано, зависит от того, сколько
информации можно поместить на место исполнительного адреса, который заме
нит ее в конечном счете.
150
a
Модули и раздельная компиляция
б
в
Рис. 15.8. Три вида списков адресных привязок в объектных файлах
15.5. Проверка конфигурационной
совместимости
Может показаться запоздалым задаваться теперь вопросом: «Зачем вообще введе
ны символьные файлы?» Допустим, нужно откомпилировать модуль , который
импортирует модули M0 и M1. Очевидным решением было бы непосредственно
перед компиляцией перекомпилировать модули M0 и M1 и объединить три по
лученные в результате таблицы символов. Компиляцию модулей M0 и M1 можно
легко запустить при компиляции , прочитав его список импорта.
Хотя повторная компиляция того же самого исходного текста – пустая трата
времени, такой метод применяется в различных коммерческих компиляторах для
(расширенного) Паскаля и Модулы. Однако серьезный недостаток этого метода
заключается не столько в необходимости дополнительных усилий, сколько в от
сутствии гарантии совместимости связываемых модулей. Предположим, что
должен быть перекомпилирован после некоторых изменений, сделанных в исход
ном тексте. Тогда вполне вероятно, что после первоначальной формулировки и
его компиляции в M0 и M1 тоже могут быть внесены изменения, которые повредят
. Может даже оказаться, что исходные версии M0 и M1, доступные в настоящее
время программисту клиента , больше не совместимы с актуальными объектны
ми файлами M0 и M1. Этот факт уже не может быть обнаружен при компиляции
и почти наверняка приводит к катастрофе, когда связываются и выполняются не
согласованные части.
Проверка конфигурационной совместимости
151
Однако символьные файлы не допускают изменений, как исходные файлы;
они закодированы и невидимы для текстового редактора. Они могут изменяться
только целиком. Для обеспечения совместимости каждый символьный файл
снабжается уникальным ключом. Таким образом, символьные файлы делают мо
дули доступными, не раскрывая при этом их исходного текста. Клиент может по
лагаться на предоставленное ему определение интерфейса, а благодаря ключу ему
гарантируется также совместимость этого определения с существующими реали
зациями. Пока такой гарантии нет, вся идея модулей и раздельной компиляции
хоть и заманчива, но едва ли полезна.
Так, например, рис. 15.9 показывает слева ситуацию при компиляции модуля B,
а справа – при компиляции модуля C. Между этими двумя компиляциями модуль
был изменен и перекомпилирован. В связи с этим файлы символов модулей B и
C содержат якори модуля с различающимися ключами, а именно 8325 в B и 8912
в C. Компилятор проверяет ключи, замечает их различие и выдает сообщение об
ошибке. Если же модуль A изменен после перекомпиляции C (с измененным ин
терфейсом), то несогласованность может и должна быть обнаружена при загрузке
и связывании этой конфигурации модулей. С этой целью те же ключи включают
ся и в соответствующие объектные файлы. Поэтому можно обнаружить несогла
сованность импорта в B и C до попытки выполнения. Это крайне важно.
Рис. 15.9. Несогласованность версий модуля
Пара «ключ – имя» используется в качестве характеристики каждого модуля,
и такая пара содержится в заголовке каждого символьного и объектного файлов.
Как уже упоминалось, имена модулей в списке импорта тоже дополняются их
ключом. Эти соображения приводят к структуре символьных файлов, определен
ных в приложении A.3.
Уникальные ключи модуля могут быть сгенерированы различными алгорит
мами. Простейшим, возможно, является использование текущих времени и даты,
которые, соответствующим образом закодированные, дают желаемый ключ. Не
достаток состоит в том, что этот метод не совсем надежен. Даже если разрешаю
щая способность часов – одна секунда, то одновременные компиляции на разных
152
Модули и раздельная компиляция
компьютерах могут сгенерировать одинаковый ключ. Гораздо важнее то, что две
компиляции одного и того же исходного текста должны всегда генерировать один
и тот же ключ; но они этого не сделают. Следовательно, если в модуль внесены
изменения, которые позже окажутся ошибочными, то перекомпиляция его перво
начальной версии даст в результате новый ключ, который сделает старые клиен
ты как бы испорченными.
Лучший метод генерировать ключ состоит в том, чтобы использовать в каче
стве параметра сам символьный файл, как при вычислении контрольной суммы.
Но этот метод тоже не во всем безопасен, потому что для разных файлов символов
могут получиться одинаковые ключи. Но зато его преимущество в том, что каж
дая перекомпиляция одного и того же текста генерирует один и тот же ключ. Клю
чи, вычисленные таким способом, называются отпечатками (fingerprints).
15.6. Упражнения
15.1. Включите раздельную компиляцию в ваш компилятор Оберон 0. Язык рас
ширяется включением списка импорта (см. приложение A.2) и маркером в объяв
лениях экспортируемых идентификаторов. Используйте технику символьных
файлов и введите правило, которое не позволяет извне присваивать значения экс
портируемым переменным, то есть в импортирующих модулях они считаются пе
ременными «только для чтения».
15.2. Реализуйте способ отпечатков для генерации ключей модуля.
Глава 16
Оптимизация и структура
пре/постпроцессора
16.1. Общие соображения .......
16.2. Простые оптимизации ....
16.3. Исключение повторных
вычислений .............................
16.4. Распределение
регистров ................................
16.5. Структура
пре/постпроцессорного
компилятора ............................
16.6. Упражнения .....................
154
155
156
157
158
162
154
Оптимизация и структура пре/постпроццессора
16.1. Общие соображения
Если мы проанализируем код, сгенерированный компилятором, разработанным
в предыдущих главах, то можем легко увидеть, что он правильный, но довольно
примитивный, и во многих случаях может быть усовершенствован. Причина зак
лючается прежде всего в прямолинейности выбранного алгоритма, который
транслирует языковые конструкции вне зависимости от их контекста в фиксиро
ванные последовательности команд. Он с трудом различает особые случаи и не
использует их выгод. Прямолинейность такой схемы приводит к результатам, ко
торые лишь отчасти отвечают требованиям экономии памяти и скорости выпол
нения. Это неудивительно, поскольку исходный и целевой языки попросту не со
гласуются друг с другом, в связи с чем мы наблюдаем семантический разрыв
между языком программирования, с одной стороны, и системой команд и архи
тектурой машины – с другой.
Чтобы генерировать код, который использует имеющиеся команды и ресурсы
машины, более эффективно, должны быть применены более изощренные схемы
трансляции. Они называются оптимизациями, а использующие их компилято
ры – оптимизирующими компиляторами. Следует обратить внимание, что этот
термин, хотя и широко распространен, в сущности является эвфемизмом (разно
видностью синонима – Прим. перев.). Вряд ли кто либо готов утверждать, что сге
нерированный им код оптимален во всех отношениях, то есть совсем не поддается
усовершенствованию. Так называемая оптимизация есть не что иное, как усовер
шенствование. Однако мы согласимся с общепринятой терминологией и тоже бу
дем использовать термин «оптимизация».
Совершенно очевидно, что чем изощреннее алгоритм, тем лучше получаемый
код. В общем, можно утверждать, что чем лучше сгенерированный код и чем быст
рее его выполнение, тем сложнее, больше и медленнее будет компилятор. В неко
торых случаях компиляторы построены так, чтобы дать возможность выбирать
уровень оптимизации – низкий, пока программа находится в разработке, и высо
кий – после ее завершения. Попутно отметим, что оптимизация может выбирать
ся с разными целями, такими как ускорение выполнения или сжатие кода. Эти два
критерия обычно требуют различных алгоритмов генерации кода и зачастую про
тиворечивы, явно указывая на то, что нет такого понятия, как точно определен
ный оптимум.
Неудивительно, что одни меры по улучшению кода могут дать значительные
выгоды при скромных усилиях, тогда как другие могут потребовать существенно
го наращивания сложности и размера компилятора, принося лишь умеренные
улучшения кода, просто потому, что они редко применяются. В действительности
существует огромная разница в соотношении между усилиями и выгодой. Преж
де чем проектировщику компилятора решиться на включение в него изощренных
средств оптимизации или кому то решиться на приобретение хорошо оптимизи
рующего, медленного и дорогого компилятора, стоит выяснить это соотношение и
решить, действительно ли нужны обещаемые улучшения.
Простые оптимизации
155
Кроме того, мы должны различать те оптимизации, эффект от которых может
быть получен за счет иного написания исходной программы, и те, где это невоз
можно. Первый вид оптимизации оказывает услугу главным образом бездарному
или неряшливому программисту, а всех остальных пользователей только обреме
няет увеличенными размерами и уменьшенной скоростью компилятора. В каче
стве противоположного примера рассмотрим случай компилятора, который ис
ключает умножение, если один множитель имеет значение 1. При вычислении
адреса элемента массива, когда индекс должен умножаться на размер его элемен
тов, ситуация складывается по разному. Здесь размер нередко равен 1, но умно
жение не может быть исключено ловким трюком в исходной программе.
Следующий критерий классификации средств оптимизации – зависимость их
от данной целевой архитектуры. Существуют приемы, которые можно выразить
непосредственно на исходном языке независимо от целевого компьютера. Приме
ры целенезависимой оптимизации приводятся в следующих известных тожде
ствах:
x+0=x
x*2=x+x
b & TRUE = b
b & ~b = FALSE
IF TRUE THEN A ELSE B END = A
IF FALSE THEN A ELSE B END = B
С другой стороны, существуют оптимизации, которые возможны только бла
годаря особым свойствам данной архитектуры. Например, существуют компью
теры, которые объединяют в одной команде умножение и сложение, сравнение и
условный переход. В таких случаях компилятор должен распознать шаблон кода,
который позволит воспользоваться такой специальной командой.
Наконец, обратим также внимание на то, что чем больше оптимизаций со зна
чительным эффектом может быть включено в компилятор, тем беднее должна
быть его первоначальная версия. В связи с этим громоздкая структура многих
коммерческих компиляторов, происхождение которой трудно понять, приводит
Эк удивительно слабым первоначальным рабочим характеристикам, которые де
лают оптимизационные средства абсолютно необходимыми.
16.2. Простые оптимизации
Сначала рассмотрим оптимизации, которые реализуются небольшими усилиями
и потому практически обязательны. Эта категория включает случаи, которые мо
гут быть выявлены просмотром ближайшего контекста. Наилучший пример – это
вычисление выражений с константами, которое называется сверткой констант и
уже заложено в представленный компилятор.
Другой пример – умножение на степень 2, которое может быть заменено про
стой, эффективной командой сдвига. Хотя такой случай может быть выявлен и
без просмотра контекста:
156
Оптимизация и структура пре/постпроццессора
IF (y.mode = Const) & (y.a # 0) THEN
n := y.a; k := 0;
WHILE ~ODD(n) DO n := n DIV 2; k := k+1 END ;
IF n = 1 THEN PutShift(x, k) ELSE PutOp(MUL, x, y) END
ELSE ...
END
Деление (целых чисел) обрабатывется таким же образом. Если делитель равен
2k для некоторого целого числа k, то делимое просто сдвигается на k битов вправо.
Для операции остатка от деления просто выделяются самые младшие k битов.
16.3. Исключение повторных вычислений
Наверное, самый известный случай целенезависимой оптимизации – устранение
общих подвыражений. На первый взгляд, этот случай может быть классифициро
ван как избирательная оптимизация, потому что перевычисление того же самого
подвыражения может быть достигнуто простым изменением исходной програм
мы. Например, присваивания
x: = (a+b)/c; y: = (a+b)/d
могут быть легко заменены тремя более простыми присваиваниями с использова
нием вспомогательной переменной u:
u: = a+b; x: = u/c; b: = u/d
Конечно, эта оптимизация касается количества арифметических операций, но
не касается количества присваиваний или ясности исходного текста. Поэтому во
прос, будет ли такое изменение улучшением вообще, остается открытым.
Более критичен случай, когда улучшения невозможно достичь изменением ис
ходного текста, как показано в следующем примере:
a[i, j] := a[i, j] + b[i, j]
Здесь одно и то же вычисление адреса выполняется трижды, и каждый раз оно
включает по крайней мере одно умножение и одно сложение. Общие подвыраже
ния неявны и не видны непосредственно в исходном тесте. Оптимизация может
быть выполнена только компилятором.
Устранение общих выражений имеет смысл, только если они вычисляются по
вторно. Хотя это возможно и в случае, когда выражение встречается в исходном
тексте лишь однажды:
WHILE i > 0 DO z := x + y; i := i – 1 END
Так как x и y остаются неизменными во время повторения, сумма должна быть
вычислена только однажды. Компилятор должен вынести присваивание перемен
ной z за пределы цикла.
Во всех перечисленных случаях код программы может быть улучшен за счет
избирательного анализа контекста. Но это именно то, что заметно прибавляет ра
Распределение регистров
157
боты при компиляции. Представленный здесь компилятор для Оберон 0 – не
слишком подходящая основа для такого вида оптимизации.
Техника упрощения выражений путем использования значений, вычисленных
на предыдущем шаге цикла, то есть с учетом рекуррентных соотношений, подобна
вынесению константных выражений за пределы циклов. Если, например, адрес
элемента массива adr(a[i]) = k*i + a0, то adr(a[i+1]) = adr(a[i]) + k. Этот случай
особенно част и потому уместен. Например, адреса индексных переменных в опе
раторе
FOR i := 0 TO N–1 DO a[i] := b[i] * c[i] END
могут быть вычислены простым прибавлением константы к их предыдущим зна
чениям. Такая оптимизация приводит к существенному сокращению времени вы
полнения. Тест со следующим примером матричного умножения показал удиви
тельные результаты:
FOR i := 0 TO 99 DO
FOR j := 0 TO 99 DO
FOR k := 0 TO 99 DO a[i, j] := a[i, j] + b[i, k] * c[k, j] END
END
END
Использование регистров вместо ячеек памяти для запоминания индекса и
суммы, а также устранение проверок границ индексов привели к увеличению ско
рости в 1,5 раза. Замена индексации арифметической прогрессией адресов, опи
санной выше, увеличила скорость в 2,75 раза. А дополнительное использование
комбинированной команды умножения и сложения для вычисления скалярных
произведений позволило увеличить этот показатель до 3,90.
К сожалению, в этом случае уже недостаточно учета ближайшей контекстной ин
формации. Требуется сложный анализ потоков управления и данных, как и для выяв
ления того факта, что на каждом шаге цикла индекс монотонно увеличивается на 1.
16.4. Распределение регистров
Доминирующей темой в оптимизации являются использование и распределение
регистров процессора. В представленном компиляторе Оберон 0 регистры исполь
зуются исключительно для хранения безымянных промежуточных результатов
во время вычисления выражений. Для этой цели обычно достаточно нескольких
регистров. Однако характерная черта современных процессоров – значительное
число регистров с временем доступа, существенно меньшим, чем доступ к основ
ной памяти. Использование их только для промежуточных результатов означало
бы нерациональное использование наиболее ценных ресурсов. Главная цель хоро
шей оптимизации кода – наиболее эффективное использование регистров с целью
сокращения числа обращений к относительно медленной основной памяти. Хоро
шая стратегия использования регистров дает больше преимуществ, чем любой
другой вид оптимизации.
158
Оптимизация и структура пре/постпроццессора
Широко распространенный метод – это распределение регистров с использо
ванием раскрашенного графа. Для каждого значения, возникающего в процессе
вычисления (то есть для каждого выражения), определяются точка его возникно
вения и точка его последнего использования. Они задают границы области его
существования. Очевидно, значения различных выражений могут быть сохране
ны в одном и том же регистре тогда и только тогда, когда их области существова
ния не перекрываются. Области представляются вершинами графа, в котором
дуга между двумя вершинами означает, что две области перекрываются. Тогда
под распределением N доступных регистров для возникающих значений можно
понимать раскраску графа в N цветов таким образом, что соседние вершины все
гда имеют разные цвета. Это значит, что значения с перекрывающимися областя
ми всегда размещаются в разных регистрах.
Более того, некоторые скалярные локальные переменные размещаются вовсе
не в памяти, а в выделенных им регистрах. Для достижения оптимального исполь
зования регистров применяются изощренные алгоритмы, способные определить,
к каким переменным обращаются чаще всего. Очевидно, что необходимая «бух
галтерия» обращений к переменным разрастается, в связи с чем страдает скорость
компиляции. К тому же необходимо позаботиться о том, чтобы сохранить регист
ровые значения в памяти перед вызовом процедуры и восстановить их при выходе
из процедуры. Скрытая опасность состоит в том, что затраты, необходимые для
решения этой задачи, превосходят получаемую экономию. Во многих компилято
рах локальные переменные размещаются в регистрах только в процедурах, кото
рые не содержат никаких вызовов самих себя (процедуры листья) и потому вызы
ваются наиболее часто, так как являются листьями в дереве, представляющем
иерархию вызовов процедур.
Детальная проработка всех этих проблем оптимизации находится за рамками
вводного курса о построении компиляторов. Поэтому сделанного выше наброска
будет достаточно. Во всяком случае, такие методы дают понять, что для генерации
кода, близкого к оптимальному, необходимо учитывать гораздо больше информа
ции о контексте, чем в нашем относительно простом компиляторе Оберон 0. Его
структура не вполне подходит для достижения высокой степени оптимизации. Но
он превосходно служит в качестве быстрого компилятора, производящего весьма
приемлемый, хотя и не оптимальный код, что присуще фазе разработки системы,
особенно для образовательных целей. Раздел 16.5 показывает другую, несколько
более сложную структуру компилятора, которая больше подходит для включения
в его состав алгоритмов оптимизации.
16.5. Структура пре/постпроцессорного
компилятора
Наиболее важной чертой компилятора, разработанного в главах 7–12, является
то, что исходный текст читается ровно один раз. В связи с этим код генерируется
«на лету». В любой момент информация об операндах ограничивается элемента
Структура пре/постпроцессорного компилятора
159
ми, обозначающими операнд, и таблицей
символов, представляющей объявления.
Так называемая пре/постпроцессорная (fron
tend/backend) структура компилятора, ко
торая была кратко упомянута в главе 1, су
щественно отличается в этом отношении.
Препроцессорная часть также читает исход
ный текст только однажды, но вместо того,
чтобы генерировать код, она формирует
структуру данных, представляющую про
грамму в удобной для дальнейшей обработ
ки форме. Вся информация, содержавшаяся
в операторах, отображается в эту структуру
данных. Ее называют синтаксическим дере
вом, потому что она также отражает синтакси
ческую структуру текста. Несколько упрощая,
можно сказать, что препроцессор компили
рует объявления в таблицу символов, а опе
раторы – в синтаксическое дерево. Эти две
Рис. 16.1. Компилятор, состоящий
структуры данных образуют интерфейс для из препроцессора и постпроцессора
постпроцессорной части, задача которой –
генерация кода. Синтаксическое дерево предоставляет быстрый доступ фактически
ко всем частям программы и представляет программу в предварительно обработан
ной (preprocessed) форме. Такой процесс компиляции показан на рис. 16.1.
В главе 1 мы указали на одно существенное преимущество такой структуры:
разделение компилятора на целенезависимый препроцессор и целезависимый по
стпроцессор. В дальнейшем мы сосредоточимся на интерфейсе между этими дву
мя частями, а именно на структуре синтаксического дерева. Более того, мы пока
жем, как генерируется подобное дерево.
Точно так, как в исходной программе операторы ссылаются на объявления,
синтаксическое дерево ссылается на записи в таблице символов. Это приводит
к понятному желанию объявить элементы таблицы символов (объекты) таким об
разом, чтобы можно было ссылаться на них из самой таблицы символов так же,
как из синтаксического дерева. В качестве основного введем тип Object, который
может видоизменяться для представления констант, переменных, типов и проце
дур. Общим для всех останется только атрибут type. Здесь и дальше мы восполь
зуемся возможностью Оберона, названной расширением типа [14].
Object
ObjDesc
ConstDesc
VarDesc
=
=
=
=
POINTER TO ObjDesc;
RECORD type: Type END ;
RECORD (ObjDesc) value: LONGINT END ;
RECORD (ObjDesc) adr, level: LONGINT END ;
Таблица символов состоит из списков элементов, по одному для каждой обла
сти действия (см. раздел 8.2). Элементы состоят из имени (идентификатора) и
ссылки на объект с таким именем.
160
Оптимизация и структура пре/постпроццессора
Ident
IdentDesc
Scope
ScopeDesc
= POINTER TO IdentDesc;
= RECORD name: ARRAY 32 OF CHAR;
obj: Object; next: Ident
= END ;
= POINTER TO ScopeDesc;
= RECORD first: Ident; dsc: Scope END ;
Синтаксическое дерево лучше представить как двоичное дерево. Назовем его
элемент Node (узел). Если синтаксическая конструкция имеет форму списка, она
представляется вырожденным деревом, в котором последний элемент имеет пус
тую ветвь.
Node
NodeDesc
= POINTER TO NodeDesc;
= RECORD (Object)
op: INTEGER;
left, right: Object
= END
Рассмотрим в качестве примера следующий фрагмент текста программы:
VAR x, y, z: INTEGER;
BEGIN z := x + y – 5; ...
Препроцессор анализирует исходный текст и формирует таблицу символов
и синтаксическое дерево, как показано на рис. 16.2. Представления типов данных
опущены.
Рис. 16.2. Таблица символов (внизу) и синтаксическое дерево (вверху)
Представления вызовов процедур, операторов IF и WHILE и последовательнос
ти операторов показаны на рис. 16.3–16.5.
Заключительные примеры демонстрируют, как генерируются описанные
структуры данных. Читатель должен сравнить эти фрагменты компилятора с со
Структура пре/постпроцессорного компилятора
161
Рис. 16.3. Вызов процедуры
Рис. 16.4. Операторы IF и WHILE
Рис. 16.5. Последовательность операторов
ответствующими процедурами компилятора Оберон 0, приведенного в приложе
нии C. Все последующие алгоритмы используют вспомогательную процедуру
New, которая генерирует новый узел.
PROCEDURE New(op: INTEGER; x, y: Object): Item;
VAR z: Item;
BEGIN New(z); z.op := op; z.left := x; z.right := y; RETURN z
END New;
PROCEDURE factor(): Object;
VAR x: Object; c: Constant;
BEGIN
IF sym = ident THEN x := This(name); Get(sym); x := selector(x)
162
Оптимизация и структура пре/постпроццессора
ELSIF sym = number THEN NEW(c); c.value := number; Get(sym); x := c
ELSIF sym = lparen THEN Get(sym); x := expression();
IF sym = rparen THEN Get(sym) ELSE Mark(22) END
ELSIF sym = not THEN Get(sym); x := New(not, NIL, factor())
ELSE ...
END ;
RETURN x
END factor;
PROCEDURE term(): Object;
VAR op: INTEGER; x: Object;
BEGIN x := factor();
WHILE (sym >= times) & (sym <= and) DO
op := sym; Get(sym); x := New(op, x, factor())
END ;
RETURN x
END term;
PROCEDURE statement(): Object;
VAR x: Object;
BEGIN
IF sym = ident THEN
x := This(name); Get(sym); x := selector(x);
IF sym = becomes THEN Get(sym); x := New(becomes, x, expression())
ELSIF ...
END
ELSIF sym = while THEN
Get(sym); x := expression();
IF sym = do THEN Get(sym) ELSE Mark(25) END ;
x := New(while, x, statseq());
IF sym = end THEN Get(sym) ELSE Mark(20) END
ELSIF ...
END ;
RETURN x
END statement
Эти фрагменты ясно показывают, что структура препроцессора предопределе
на синтаксическим анализатором. Программа стала даже несколько проще. Но
нужно иметь в виду, что для краткости в этих процедурах опущен контроль типов.
Тем не менее контроль типов как целенезависимая задача определенно относится
к препроцессору.
16.6. Упражнения
16.1. Улучшите генерацию кода компилятора Оберон 0 так, чтобы значения и ад
реса, однажды загруженные в регистр, могли бы повторно использоваться без пе
резагрузки. Так, для примера
z := (x – y) * (x + y); y := x
Упражнения
163
представленный компилятор сгенерирует последовательность команд
LDW
LDW
SUB
LDW
LDW
ADD
MUL
STW
LDW
STW
1,
2,
1,
2,
3,
2,
1,
1,
1,
1,
0,
0,
1,
0,
0,
2,
1,
0,
0,
0,
x
y
2
x
y
3
2
z
x
y
Улучшенная версия должна генерировать
LDW
LDW
SUB
ADD
MUL
STW
STW
1,
2,
3,
4,
5,
5,
1,
0,
0,
1,
1,
3,
0,
0,
x
y
2
2
4
z
y
Измерьте вручную выгоду на разумно большом количестве тестовых вариантов.
16.2. Какие дополнительные команды архитектуры RISC из главы 9 были бы
желательны для облегчения реализации предыдущих упражнений и генерации
более короткого и более эффективного кода?
16.3. Оптимизируйте компилятор Оберон 0 таким способом, чтобы скалярные
переменные по возможности размещались на регистрах вместо памяти. Измерьте
достигнутый выигрыш и сравните его с тем, что получен в упражнении 16.1. Как
обрабатываются переменные в качестве VAR параметров?
16.4. Создайте модуль OSGx, который заменяет OSG (см. текст в приложении
C) и генерирует код для CISC архитектуры x. Заданный интерфейс OSG должен
быть сохранен насколько это возможно, для того чтобы модули OSS и OSP оста
лись неизменными.
Приложение A. Синтаксис
A.1. Оберон 0
ident = letter {letter | digit}.
integer = digit {digit}.
selector = {"." ident | "[" expression "]"}.
factor = ident selector | integer | "(" expression ")" | "~" factor.
term
= factor {("*" | "DIV" | "MOD" | "&") factor}.
SimpleExpression = ["+"|"–"] term {("+"|"–" | "OR") term}.
expression = SimpleExpression [("=" | "#" | "<" | "<=" | ">" | ">=")
= SimpleExpression].
assignment = ident selector ":=" expression.
ActualParameters = "(" [expression {"," expression}] ")" .
ProcedureCall = ident [ActualParameters].
IfStatement = "IF" expression "THEN" StatementSequence {"ELSIF" expression
"THEN" StatementSequence} ["ELSE" StatementSequence] "END".
WhileStatement = "WHILE" expression "DO" StatementSequence "END".
statement
= [assignment | ProcedureCall | IfStatement | WhileStatement].
StatementSequence = statement {";" statement}.
IdentList
= ident {"," ident}.
ArrayType
= "ARRAY" expression "OF" type.
FieldList
= [IdentList ":" type].
RecordType
= "RECORD" FieldList {";" FieldList} "END".
type
= ident | ArrayType | RecordType.
FPSection
= ["VAR"] IdentList ":" type.
FormalParameters = "(" [FPSection {";" FPSection}] ")".
ProcedureHeading = "PROCEDURE" ident [FormalParameters].
ProcedureBody = declarations ["BEGIN" StatementSequence] "END".
ProcedureDeclaration = ProcedureHeading ";" ProcedureBody ident.
declarations = ["CONST" {ident "=" expression ";"}]
["TYPE" {ident "=" type ";"}] ["VAR" {IdentList ":" type ";"}]
{ProcedureDeclaration ";"}.
module = "MODULE" ident ";" declarations
["BEGIN" StatementSequence] "END" ident "." .
A.2. Оберон
ident
= letter {letter | digit}.
number = integer | real.
integer = digit {digit} | digit {hexDigit} "H".
real
= digit {digit} "." {digit} [ScaleFactor].
ScaleFactor = ("E" | "D") ["+" | "–"] digit {digit}.
hexDigit = digit | "A" | "B" | "C" | "D" | "E" | "F".
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9".
CharConstant = '"' character '"' | digit {hexDigit} "X".
Оберон
165
string = '"' {character} '"' .
identdef = ident ["*"].
qualident= [ident "."] ident.
ConstantDeclaration = identdef "=" ConstExpression.
ConstExpression
= expression.
TypeDeclaration
= identdef "=" type.
type
= qualident | ArrayType | RecordType | PointerType | ProcedureType.
ArrayType = ARRAY length {"," length} OF type.
length
= ConstExpression.
RecordType = RECORD ["(" BaseType ")"] FieldListSequence END.
BaseType
= qualident.
FieldListSequence = FieldList {";" FieldList}.
FieldList
= [IdentList ":" type].
IdentList
= identdef {"," identdef}.
PointerType
= POINTER TO type.
ProcedureType
= PROCEDURE [FormalParameters].
VariableDeclaration = IdentList ":" type.
designator = qualident {"." ident | "[" ExpList "]" | "(" qualident ")" | "^"}.
ExpList
= expression {"," expression}.
expression = SimpleExpression [relation SimpleExpression].
relation
= "=" | "#" | "<" | "<=" | ">" | ">=" | IN | IS.
SimpleExpression = ["+"|"–"] term {AddOperator term}.
AddOperator
= "+" | "–" | OR .
term
= factor {MulOperator factor}.
MulOperator
= "*" | "/" | DIV | MOD | "&" .
factor
= number | CharConstant | string | NIL | set |
designator [ActualParameters] | "(" expression ")" | "~" factor.
set
= "{" [element {"," element}] "}".
element = expression [".." expression].
ActualParameters = "(" [ExpList] ")" .
statement
= [assignment | ProcedureCall | IfStatement | CaseStatement |
WhileStatement | RepeatStatement | LoopStatement |
ForStatement | WithStatement | EXIT | RETURN [expression] ].
assignment
= designator ":=" expression.
ProcedureCall = designator [ActualParameters].
StatementSequence = statement {";" statement}.
IfStatement
= IF expression THEN StatementSequence {ELSIF expression THEN
StatementSequence} [ELSE StatementSequence] END.
CaseStatement = CASE expression OF case {"|" case}
[ELSE StatementSequence] END.
case
= [CaseLabelList ":" StatementSequence].
CaseLabelList = CaseLabels {"," CaseLabels}.
CaseLabels
= ConstExpression [".." ConstExpression].
WhileStatement = WHILE expression DO StatementSequence END.
RepeatStatement = REPEAT StatementSequence UNTIL expression.
LoopStatement = LOOP StatementSequence END.
ForStatement = FOR ident ":=" expression TO expression [BY ConstExpression]
DO StatementSequence END .
166
Приложение А. Синтаксис
WithStatement = WITH qualident ":" qualident DO StatementSequence END .
ProcedureDeclaration = ProcedureHeading ";" ProcedureBody ident.
ProcedureHeading
= PROCEDURE ["*"] identdef [FormalParameters].
ProcedureBody
= DeclarationSequence [BEGIN StatementSequence] END.
ForwardDeclaration = PROCEDURE "^" ident ["*"] [FormalParameters].
DeclarationSequence = {CONST {ConstantDeclaration ";"} |
TYPE {TypeDeclaration ";"} | VAR {VariableDeclaration ";"}}
{ProcedureDeclaration ";" | ForwardDeclaration ";"}.
FormalParameters = "(" [FPSection {";" FPSection}] ")" [":" qualident].
FPSection
= [VAR] ident {"," ident} ":" FormalType.
FormalType = {ARRAY OF} (qualident | ProcedureType).
ImportList = IMPORT import {"," import} ";" .
import = ident [":=" ident].
module = MODULE ident ";" [ImportList] DeclarationSequence
[BEGIN StatementSequence] END ident "." .
A.3. Символьные файлы
SymFile = BEGIN key {name key}
[ CONST {type name value} ] [ VAR {type name} ]
[ PROC {type name {[VAR] type name} END} ]
[ ALIAS {type name} ] [ NEWTYP {type} ] END .
type = basicType | [Module] OldType | NewType.
basicType = BOOL | CHAR | INTEGER | REAL | ...
NewType = ARRAY type name intval | DYNARRAY type name | POINTER type name
| RECORD type name {type name} END
| PROCTYP type name {[VAR] type name END .
Слова, состоящие из заглавных букв, обозначают терминальные символы.
В символьных файлах они кодируются целыми числами. OldType и Module обо
значают номера типа и модуля, то есть это ссылки на предварительно определен
ные объекты.
Приложение B
Набор символов ASCII
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
0
nul
soh
stx
etx
eot
enq
ack
bel
bs
ht
lf
vt
ff
cr
so
si
1
dle
dc1
dc2
dc3
dc4
nak
syn
etb
can
em
sub
esc
fs
gs
rs
us
2
!
"
#
$
%
&
'
(
)
*
+
,
–
.
/
3
0
1
2
3
4
5
6
7
8
9
:
;
<
=
>
?
4
@
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
5
P
Q
R
S
T
U
V
W
X
Y
Z
[
\
]
^
_
6
`
a
b
c
d
e
f
g
h
I
j
k
l
m
n
o
7
p
q
r
s
t
u
v
w
x
y
z
{
|
}
~
del
Приложение C
Компилятор Оберон 0
Компилятор разбит на три модуля, а именно сканер OSS, синтаксический анали
затор OSP и генератор кода OSG. Только OSG относится к целевой архитектуре
(см. главу 9), импортируя модуль RISC, который является интерпретатором. При
помощи этого интерпретатора скомпилированный код может быть выполнен сра
зу после компиляции.
В Обероне глобальные процедуры без параметров, как говорят, являются ко
мандами, и они могут быть активизированы пользователем через интерфейс Обе
рон. Команда Compile в основном модуле OSP начинает процесс с вызова проце
дуры Module, который соответствует начальному символу синтаксиса. Исходный
текст передается как параметр. В соответствии с соглашениями системы Оберон
это может быть определено несколькими способами:
OSP.Compile name исходный текст – название файла;
OSP.Compile *
исходный текст – текст в отмеченном просмотрщике;
OSP.Compile @
исходный текст начинается с последнего выбора.
Успешная компиляция немедленно сопровождается загрузкой скопилиро
ванного кода и выполнением скомпилированного тела модуля. Здесь загрузка
эмулируется копированием команд из массива кодов OSG в массив М модуля
RISC. Относительные адреса глобальных переменных превращаются в абсолют
ные добавлением базового значения, как это принято в загрузчиках программ.
Компилятор поставляет необходимые данные в виде таблицы, содержащей адреса
всех команд, которые готовы к выполению.
Команды заносятся компилятором в таблицы comname и comadr. Процедура
OSP.Exec name ищет имя в таблице comname и передает соответствующий ему
адрес из таблицы comadr интерпретатору в качестве отправной точки для выпол
нения.
Если откомпилированная процедура содержит вызовы Read, то числа, тексту
ально следующие за словами OSP.Exec name, прочитываются и подаются на вход
интерпретатора. Например, процедура
PROCEDURE Add;
VAR x, y, z: INTEGER;
BEGIN Read(x); Read(y); Read(z); Write(x+y+z); WriteLn
END Add
активированная командой Оберона OSP.Exec Add 3 5 7, выдаст в качестве резуль
тата 15.
Лексический анализатор
169
C.1. Лексический анализатор
MODULE OSS; (* NW 19.9.93 / 17.11.94*)
IMPORT Oberon, Texts;
CONST IdLen* = 16; KW = 34;
(*symbols*) null = 0;
times* = 1; div* = 3; mod* = 4; and* = 5; plus* = 6; minus* = 7; or* = 8;
eql* = 9; neq* = 10; lss* = 11; geq* = 12; leq* = 13; gtr* = 14;
period* = 18; comma* = 19; colon* = 20; rparen* = 22; rbrak* = 23;
of* = 25; then* = 26; do* = 27; lparen* =29;
lbrak* = 30; not* = 32; becomes* = 33; number* = 34; ident* = 37;
semicolon* = 38; end* = 40; else* = 41; elsif* = 42;
if* = 44; while* = 46;
array* = 54; record* = 55; const* = 57;
type* = 58; var* = 59; procedure* = 60; begin* = 61; module* = 63; eof = 64;
TYPE Ident* = ARRAY IdLen OF CHAR;
VAR val*
val*: LONGINT;
id*
id*: Ident;
error*
error*: BOOLEAN;
ch: CHAR;
nkw: INTEGER;
errpos: LONGINT;
R: Texts.Reader;
W: Texts.Writer;
keyTab: ARRAY KW OF
RECORD sym: INTEGER; id: ARRAY 12 OF CHAR END;
PROCEDURE Mark* (msg: ARRAY OF CHAR);
VAR p: LONGINT;
BEGIN p := Texts.Pos(R) – 1;
IF p > errpos THEN
Texts.WriteString(W, "
"); Texts.WriteInt(W, p, 1);
Texts.Write(W, " "); Texts.WriteString(W, msg);
Texts.WriteLn(W); Texts.Append(Oberon.Log, W.buf)
END;
errpos := p; error := TRUE
END Mark;
PROCEDURE Get* (VAR sym: INTEGER);
PROCEDURE Ident;
VAR i, k: INTEGER;
BEGIN i := 0;
REPEAT
170
Приложение С. Компилятор Оберон 0
IF i < IdLen THEN id[i] := ch; INC(i) END;
Texts.Read(R, ch)
UNTIL (ch < "0") OR (ch > "9") & (CAP(ch) < "A") OR (CAP(ch) > "Z");
id[i] := 0X; k := 0;
WHILE (k < nkw) & (id # keyTab[k].id) DO INC(k) END;
IF k < nkw THEN sym := keyTab[k].sym ELSE sym := ident END
END Ident;
PROCEDURE Number;
BEGIN val := 0; sym := number;
REPEAT
IF val <= (MAX(LONGINT) – ORD(ch) + ORD("0")) DIV 10 THEN
val := 10 * val + (ORD(ch) – ORD("0"))
ELSE Mark("
"); val := 0
END;
Texts.Read(R, ch)
UNTIL (ch < "0") OR (ch > "9")
END Number;
PROCEDURE comment;
BEGIN Texts.Read(R, ch);
LOOP
LOOP
WHILE ch = "(" DO Texts.Read(R, ch);
IF ch = "*" THEN comment END
END;
IF ch = "*" THEN Texts.Read(R, ch); EXIT END;
IF R.eot THEN EXIT END;
Texts.Read(R, ch)
END;
IF ch = ")" THEN Texts.Read(R, ch); EXIT END;
IF R.eot THEN Mark("
"); EXIT END
END
END comment;
BEGIN
WHILE ~R.eot & (ch <= " ") DO Texts.Read(R, ch) END;
IF R.eot THEN sym := eof
ELSE
CASE ch OF
| "&": Texts.Read(R, ch); sym := and
| "*": Texts.Read(R, ch); sym := times
| "+": Texts.Read(R, ch); sym := plus
| "–": Texts.Read(R, ch); sym := minus
| "=": Texts.Read(R, ch); sym := eql
| "#": Texts.Read(R, ch); sym := neq
| "<": Texts.Read(R, ch);
| IF ch = "=" THEN Texts.Read(R, ch); sym := leq ELSE sym := lss END
| ">": Texts.Read(R, ch);
Лексический анализатор
171
| IF ch = "=" THEN Texts.Read(R, ch); sym := geq ELSE sym := gtr END
| ";": Texts.Read(R, ch); sym := semicolon
| ",": Texts.Read(R, ch); sym := comma
| ":": Texts.Read(R, ch);
| IF ch = "=" THEN Texts.Read(R, ch); sym := becomes ELSE sym := colon END
| ".": Texts.Read(R, ch); sym := period
| "(": Texts.Read(R, ch);
| IF ch = "*" THEN comment; Get(sym) ELSE sym := lparen END
| ")": Texts.Read(R, ch); sym := rparen
| "[": Texts.Read(R, ch); sym := lbrak
| "]": Texts.Read(R, ch); sym := rbrak
| "0".."9": Number;
| "A" .. "Z", "a".."z": Ident
| "~": Texts.Read(R, ch); sym := not
ELSE Texts.Read(R, ch); sym := null
END
END
END Get;
PROCEDURE Init* (T: Texts.Text; pos: LONGINT);
BEGIN error := FALSE; errpos := pos; Texts.OpenReader(R, T, pos); Texts.Read(R, ch)
END Init;
PROCEDURE EnterKW (sym: INTEGER; name: ARRAY OF CHAR);
BEGIN keyTab[nkw].sym := sym; COPY(name, keyTab[nkw].id); INC(nkw)
END EnterKW;
BEGIN Texts.OpenWriter(W); error := TRUE; nkw := 0;
EnterKW(null, "BY");
EnterKW(do, "DO");
EnterKW(if, "IF");
EnterKW(null, "IN");
EnterKW(null, "IS");
EnterKW(of, "OF");
EnterKW(or, "OR");
EnterKW(null, "TO");
EnterKW(end, "END");
EnterKW(null, "FOR");
EnterKW(mod, "MOD");
EnterKW(null, "NIL");
EnterKW(var, "VAR");
EnterKW(null, "CASE");
EnterKW(else, "ELSE");
EnterKW(null, "EXIT");
EnterKW(then, "THEN");
EnterKW(type, "TYPE");
EnterKW(null, "WITH");
EnterKW(array, "ARRAY");
EnterKW(begin, "BEGIN");
172
Приложение С. Компилятор Оберон 0
EnterKW(const, "CONST");
EnterKW(elsif, "ELSIF");
EnterKW(null, "IMPORT");
EnterKW(null, "UNTIL");
EnterKW(while, "WHILE");
EnterKW(record, "RECORD");
EnterKW(null, "REPEAT");
EnterKW(null, "RETURN");
EnterKW(null, "POINTER");
EnterKW(procedure, "PROCEDURE");
EnterKW(div, "DIV");
EnterKW(null, "LOOP");
EnterKW(module, "MODULE");
END OSS.
C.2. Синтаксический анализатор
MODULE OSP; (* NW 23.9.93 / 9.2.95*)
IMPORT OSG, OSS, MenuViewers, Oberon, TextFrames, Texts, Viewers;
CONST WordSize = 4;
VAR sym: INTEGER; loaded: BOOLEAN;
topScope, universe: OSG.Object; (*
guard: OSG.Object;
W: Texts.Writer;
,
*)
PROCEDURE NewObj (VAR obj: OSG.Object; class: INTEGER);
VAR new, x: OSG.Object;
BEGIN x := topScope; guard.name := OSS.id;
WHILE x.next.name # OSS.id DO x := x.next END;
IF x.next = guard THEN
NEW(new); new.name := OSS.id; new.class := class; new.next := guard;
x.next := new; obj := new
ELSE obj := x.next; OSS.Mark("
")
END
END NewObj;
PROCEDURE find (VAR obj: OSG.Object);
VAR s, x: OSG.Object;
BEGIN s := topScope; guard.name := OSS.id;
LOOP x := s.next;
WHILE x.name # OSS.id DO x := x.next END;
IF x # guard THEN obj := x; EXIT END;
IF s = universe THEN obj := x; OSS.Mark("
s := s.dsc
END
END find;
"); EXIT END;
PROCEDURE FindField (VAR obj: OSG.Object; list: OSG.Object);
Синтаксический анализатор
173
BEGIN guard.name := OSS.id;
WHILE list.name # OSS.id DO list := list.next END;
obj := list
END FindField;
PROCEDURE IsParam (obj: OSG.Object): BOOLEAN;
BEGIN RETURN (obj.class = OSG.Par) OR (obj.class = OSG.Var) & (obj.val > 0)
END IsParam;
PROCEDURE OpenScope;
VAR s: OSG.Object;
BEGIN NEW(s); s.class := OSG.Head; s.dsc := topScope;
s.next := guard; topScope := s
END OpenScope;
PROCEDURE CloseScope;
BEGIN topScope := topScope.dsc
END CloseScope;
(* —————————— Parser ——————————*)
PROCEDURE^ expression (VAR x: OSG.Item);
PROCEDURE selector (VAR x: OSG.Item);
VAR y: OSG.Item; obj: OSG.Object;
BEGIN
WHILE (sym = OSS.lbrak) OR (sym = OSS.period) DO
IF sym = OSS.lbrak THEN
OSS.Get(sym); expression(y);
IF x.type.form = OSG.Array THEN
OSG.Index(x, y)
ELSE OSS.Mark("
") END;
IF sym = OSS.rbrak THEN OSS.Get(sym) ELSE OSS.Mark("]?") END
ELSE OSS.Get(sym);
IF sym = OSS.ident THEN
IF x.type.form = OSG.Record THEN
FindField(obj, x.type.fields); OSS.Get(sym);
IF obj # guard THEN OSG.Field(x, obj) ELSE OSS.Mark("
") END
ELSE OSS.Mark("
")
END
ELSE OSS.Mark("
?")
END
END
END
END selector;
PROCEDURE factor (VAR x: OSG.Item);
VAR obj: OSG.Object;
BEGIN (*sync*)
174
Приложение С. Компилятор Оберон 0
IF sym < OSS.lparen THEN OSS.Mark("
?");
REPEAT OSS.Get(sym) UNTIL sym >= OSS.lparen
END;
IF sym = OSS.ident THEN
find(obj); OSS.Get(sym); OSG.MakeItem(x, obj); selector(x)
ELSIF sym = OSS.number THEN
OSG.MakeConstItem(x, OSG.intType, OSS.val); OSS.Get(sym)
ELSIF sym = OSS.lparen THEN
OSS.Get(sym); expression(x);
IF sym = OSS.rparen THEN OSS.Get(sym) ELSE OSS.Mark(")?") END
ELSIF sym = OSS.not THEN OSS.Get(sym); factor(x); OSG.Op1(OSS.not, x)
ELSE OSS.Mark("
?"); OSG.MakeItem(x, guard)
END
END factor;
PROCEDURE term (VAR x: OSG.Item);
VAR y: OSG.Item; op: INTEGER;
BEGIN factor(x);
WHILE (sym >= OSS.times) & (sym <= OSS.and) DO
op := sym; OSS.Get(sym);
IF op = OSS.and THEN OSG.Op1(op, x) END;
factor(y); OSG.Op2(op, x, y)
END
END term;
PROCEDURE SimpleExpression (VAR x: OSG.Item);
VAR y: OSG.Item; op: INTEGER;
BEGIN
IF sym = OSS.plus THEN OSS.Get(sym); term(x)
ELSIF sym = OSS.minus THEN OSS.Get(sym); term(x); OSG.Op1(OSS.minus, x)
ELSE term(x)
END;
WHILE (sym >= OSS.plus) & (sym <= OSS.or) DO
op := sym; OSS.Get(sym);
IF op = OSS.or THEN OSG.Op1(op, x) END;
term(y); OSG.Op2(op, x, y)
END
END SimpleExpression;
PROCEDURE expression (VAR x: OSG.Item);
VAR y: OSG.Item; op: INTEGER;
BEGIN SimpleExpression(x);
IF (sym >= OSS.eql) & (sym <= OSS.gtr) THEN
op := sym; OSS.Get(sym); SimpleExpression(y); OSG.Relation(op, x, y)
END
END expression;
PROCEDURE parameter (VAR fp: OSG.Object);
VAR x: OSG.Item;
Синтаксический анализатор
175
BEGIN expression(x);
IF IsParam(fp) THEN OSG.Parameter(x, fp.type, fp.class); fp := fp.next
ELSE OSS.Mark("
")
END
END parameter;
PROCEDURE StatSequence;
VAR par, obj: OSG.Object; x, y: OSG.Item; L: LONGINT;
PROCEDURE param (VAR x: OSG.Item);
BEGIN
IF sym = OSS.lparen THEN OSS.Get(sym) ELSE OSS.Mark(")?") END;
expression(x);
IF sym = OSS.rparen THEN OSS.Get(sym) ELSE OSS.Mark(")?") END
END param;
BEGIN (* StatSequence *)
LOOP (*
*) obj := guard;
IF sym < OSS.ident THEN OSS.Mark("
?");
REPEAT OSS.Get(sym) UNTIL sym >= OSS.ident
END;
IF sym = OSS.ident THEN
find(obj); OSS.Get(sym); OSG.MakeItem(x, obj); selector(x);
IF sym = OSS.becomes THEN OSS.Get(sym); expression(y); OSG.Store(x, y)
ELSIF sym = OSS.eql THEN OSS.Mark(":= ?"); OSS.Get(sym); expression(y)
ELSIF x.mode = OSG.Proc THEN
par := obj.dsc;
IF sym = OSS.lparen THEN OSS.Get(sym);
IF sym = OSS.rparen THEN OSS.Get(sym)
ELSE
LOOP parameter(par);
IF sym = OSS.comma THEN OSS.Get(sym)
ELSIF sym = OSS.rparen THEN OSS.Get(sym); EXIT
ELSIF sym >= OSS.semicolon THEN EXIT
ELSE OSS.Mark(")
, ?")
END
END
END
END;
IF obj.val < 0 THEN OSS.Mark("
")
ELSIF ~IsParam(par) THEN OSG.Call(x)
ELSE OSS.Mark("
")
END
ELSIF x.mode = OSG.SProc THEN
IF obj.val <= 3 THEN param(y) END;
OSG.IOCall(x, y)
ELSIF obj.class = OSG.Typ THEN OSS.Mark("
?")
ELSE OSS.Mark("
?")
END
ELSIF sym = OSS.if THEN
OSS.Get(sym); expression(x); OSG.CJump(x);
176
Приложение С. Компилятор Оберон 0
IF sym = OSS.then THEN OSS.Get(sym) ELSE OSS.Mark("THEN?") END;
StatSequence; L := 0;
WHILE sym = OSS.elsif DO
OSS.Get(sym); OSG.FJump(L); OSG.FixLink(x.a); expression(x);
OSG.CJump(x);
IF sym = OSS.then THEN OSS.Get(sym) ELSE OSS.Mark("THEN?") END;
StatSequence
END;
IF sym = OSS.else THEN
OSS.Get(sym); OSG.FJump(L); OSG.FixLink(x.a); StatSequence
ELSE OSG.FixLink(x.a)
END;
OSG.FixLink(L);
IF sym = OSS.end THEN OSS.Get(sym) ELSE OSS.Mark("END?") END
ELSIF sym = OSS.while THEN
OSS.Get(sym); L := OSG.pc; expression(x); OSG.CJump(x);
IF sym = OSS.do THEN OSS.Get(sym) ELSE OSS.Mark("DO?") END;
StatSequence; OSG.BJump(L); OSG.FixLink(x.a);
IF sym = OSS.end THEN OSS.Get(sym) ELSE OSS.Mark("END?") END
END;
IF sym = OSS.semicolon THEN OSS.Get(sym)
ELSIF (sym >= OSS.semicolon) & (sym < OSS.if) OR (sym >= OSS.array) THEN
EXIT
ELSE OSS.Mark("; ?")
END
END
END StatSequence;
PROCEDURE IdentList (class: INTEGER; VAR first: OSG.Object);
VAR obj: OSG.Object;
BEGIN
IF sym = OSS.ident THEN
NewObj(first, class); OSS.Get(sym);
WHILE sym = OSS.comma DO
OSS.Get(sym);
IF sym = OSS.ident THEN NewObj(obj, class); OSS.Get(sym)
ELSE OSS.Mark("
?")
END
END;
IF sym = OSS.colon THEN OSS.Get(sym) ELSE OSS.Mark(":?") END
END
END IdentList;
PROCEDURE Type (VAR type: OSG.Type);
VAR obj, first: OSG.Object; x: OSG.Item; tp: OSG.Type;
BEGIN type := OSG.intType; (*
*)
IF (sym # OSS.ident) & (sym < OSS.array) THEN OSS.Mark("
?");
REPEAT OSS.Get(sym) UNTIL (sym = OSS.ident) OR (sym >= OSS.array)
END;
Синтаксический анализатор
177
IF sym = OSS.ident THEN
find(obj); OSS.Get(sym);
IF obj.class = OSG.Typ THEN type := obj.type ELSE OSS.Mark("
?") END
ELSIF sym = OSS.array THEN
OSS.Get(sym); expression(x);
IF (x.mode # OSG.Const) OR (x.a < 0) THEN OSS.Mark("
") END;
IF sym = OSS.of THEN OSS.Get(sym) ELSE OSS.Mark("OF?") END;
Type(tp); NEW(type); type.form := OSG.Array; type.base := tp;
type.len := SHORT(x.a); type.size := type.len * tp.size
ELSIF sym = OSS.record THEN
OSS.Get(sym); NEW(type); type.form := OSG.Record; type.size := 0;
OpenScope;
LOOP
IF sym = OSS.ident THEN
IdentList(OSG.Fld, first); Type(tp); obj := first;
WHILE obj # guard DO
obj.type := tp; obj.val := type.size; INC(type.size, obj.type.size);
obj := obj.next
END
END;
IF sym = OSS.semicolon THEN OSS.Get(sym)
ELSIF sym = OSS.ident THEN OSS.Mark("; ?")
ELSE EXIT
END
END;
type.fields := topScope.next; CloseScope;
IF sym = OSS.end THEN OSS.Get(sym) ELSE OSS.Mark("END?") END
ELSE OSS.Mark("
?")
END
END Type;
PROCEDURE declarations (VAR varsize: LONGINT);
VAR obj, first: OSG.Object;
x: OSG.Item; tp: OSG.Type; L: LONGINT;
BEGIN (*
*)
IF (sym < OSS.const) & (sym # OSS.end) THEN OSS.Mark("
?");
REPEAT OSS.Get(sym) UNTIL (sym >= OSS.const) OR (sym = OSS.end)
END;
LOOP
IF sym = OSS.const THEN
OSS.Get(sym);
WHILE sym = OSS.ident DO
NewObj(obj, OSG.Const); OSS.Get(sym);
IF sym = OSS.eql THEN OSS.Get(sym) ELSE OSS.Mark("=?") END;
expression(x);
IF x.mode = OSG.Const THEN obj.val := x.a; obj.type := x.type
ELSE OSS.Mark("
")
END;
IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.Mark(";?") END
178
Приложение С. Компилятор Оберон 0
END
END;
IF sym = OSS.type THEN
OSS.Get(sym);
WHILE sym = OSS.ident DO
NewObj(obj, OSG.Typ); OSS.Get(sym);
IF sym = OSS.eql THEN OSS.Get(sym) ELSE OSS.Mark("=?") END;
Type(obj.type);
IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.Mark(";?") END
END
END;
IF sym = OSS.var THEN
OSS.Get(sym);
WHILE sym = OSS.ident DO
IdentList(OSG.Var, first); Type(tp); obj := first;
WHILE obj # guard DO
obj.type := tp; obj.lev := OSG.curlev;
varsize := varsize + obj.type.size; obj.val := – varsize;
obj := obj.next
END;
IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.Mark("; ?") END
END
END;
IF(sym >= OSS.const) & (sym <= OSS.var) THEN
OSS.Mark("
?")
ELSE EXIT END
END
END declarations;
PROCEDURE ProcedureDecl;
CONST marksize = 8;
VAR proc, obj: OSG.Object;
procid: OSS.Ident;
locblksize, parblksize: LONGINT;
PROCEDURE FPSection;
VAR obj, first: OSG.Object; tp: OSG.Type; parsize: LONGINT;
BEGIN
IF sym = OSS.var THEN OSS.Get(sym); IdentList(OSG.Par, first)
ELSE IdentList(OSG.Var, first)
END;
IF sym = OSS.ident THEN
find(obj); OSS.Get(sym);
IF obj.class = OSG.Typ THEN tp := obj.type
ELSE OSS.Mark("
?"); tp := OSG.intType
END
ELSE OSS.Mark("
?"); tp := OSG.intType
END;
IF first.class = OSG.Var THEN
parsize := tp.size;
Синтаксический анализатор
IF tp.form >= OSG.Array THEN OSS.Mark("
ELSE parsize := WordSize
END;
obj := first;
WHILE obj # guard DO
obj.type := tp; INC(parblksize, parsize); obj := obj.next
END
END FPSection;
179
") END;
BEGIN (* ProcedureDecl *)
OSS.Get(sym);
IF sym = OSS.ident THEN
procid := OSS.id;
NewObj(proc, OSG.Proc); OSS.Get(sym); parblksize := marksize;
OSG.IncLevel(1); OpenScope; proc.val := – 1;
IF sym = OSS.lparen THEN
OSS.Get(sym);
IF sym = OSS.rparen THEN OSS.Get(sym)
ELSE FPSection;
WHILE sym = OSS.semicolon DO OSS.Get(sym); FPSection END;
IF sym = OSS.rparen THEN OSS.Get(sym) ELSE OSS.Mark(")?") END
END
ELSIF OSG.curlev = 1 THEN OSG.EnterCmd(procid)
END;
obj := topScope.next; locblksize := parblksize;
WHILE obj # guard DO
obj.lev := OSG.curlev;
IF obj.class = OSG.Par THEN DEC(locblksize, WordSize)
ELSE obj.val := locblksize; obj := obj.next
END
END;
proc.dsc := topScope.next;
IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.Mark(";?") END;
locblksize := 0; declarations(locblksize);
WHILE sym = OSS.procedure DO
ProcedureDecl;
IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.Mark(";?") END
END;
proc.val := OSG.pc; OSG.Enter(locblksize);
IF sym = OSS.begin THEN OSS.Get(sym); StatSequence END;
IF sym = OSS.end THEN OSS.Get(sym) ELSE OSS.Mark("END?") END;
IF sym = OSS.ident THEN
IF procid # OSS.id THEN OSS.Mark("
") END;
OSS.Get(sym)
END;
OSG.Return(parblksize – marksize); CloseScope; OSG.IncLevel( – 1)
END
END ProcedureDecl;
PROCEDURE Module (VAR S: Texts.Scanner);
180
Приложение С. Компилятор Оберон 0
VAR modid: OSS.Ident; varsize: LONGINT;
BEGIN Texts.WriteString(W, "
");
IF sym = OSS.module THEN
OSS.Get(sym); OSG.Open; OpenScope; varsize := 0;
IF sym = OSS.ident THEN
modid := OSS.id; OSS.Get(sym);
Texts.WriteString(W, modid); Texts.WriteLn(W);
Texts.Append(Oberon.Log, W.buf)
ELSE OSS.Mark("
?")
END;
IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.Mark(";?") END;
declarations(varsize);
WHILE sym = OSS.procedure DO
ProcedureDecl;
IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.Mark(";?") END
END;
OSG.Header(varsize);
IF sym = OSS.begin THEN OSS.Get(sym); StatSequence END;
IF sym = OSS.end THEN OSS.Get(sym) ELSE OSS.Mark("END?") END;
IF sym = OSS.ident THEN
IF modid # OSS.id THEN OSS.Mark("
") END;
OSS.Get(sym)
ELSE OSS.Mark("
?")
END;
IF sym # OSS.period THEN OSS.Mark(". ?") END;
CloseScope;
IF ~OSS.error THEN
COPY(modid, S.s); OSG.Close(S, varsize);
Texts.WriteString(W, "
");
Texts.WriteInt(W, OSG.pc, 6); Texts.WriteLn(W);
Texts.Append(Oberon.Log, W.buf)
END
ELSE OSS.Mark("MODULE?")
END
END Module;
PROCEDURE Compile*
Compile*;
VAR beg, end, time: LONGINT;
S: Texts.Scanner; T: Texts.Text; v: Viewers.Viewer;
BEGIN loaded := FALSE;
Texts.OpenScanner(S, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(S);
IF S.class = Texts.Char THEN
IF S.c = "*" THEN
v := Oberon.MarkedViewer();
IF (v.dsc # NIL) & (v.dsc.next IS TextFrames.Frame) THEN
OSS.Init(v.dsc.next(TextFrames.Frame).text, 0); OSS.Get(sym);
Module(S)
END
ELSIF S.c = "@" THEN
Синтаксический анализатор
181
Oberon.GetSelection(T, beg, end, time);
IF time >= 0 THEN OSS.Init(T, beg); OSS.Get(sym); Module(S) END
END
ELSIF S.class = Texts.Name THEN
NEW(T); Texts.Open(T, S.s); OSS.Init(T, 0); OSS.Get(sym); Module(S)
END
END Compile;
PROCEDURE Decode*
Decode*;
VAR V: MenuViewers.Viewer; T: Texts.Text;
X, Y: INTEGER;
BEGIN T := TextFrames.Text("");
Oberon.AllocateSystemViewer(Oberon.Par.frame.X, X, Y);
V := MenuViewers.New(TextFrames.NewMenu("Log.Text",
"System.Close System.Copy System.Grow Edit.Search Edit.Store"),
TextFrames.NewText(T, 0), TextFrames.menuH, X, Y);
OSG.Decode(T)
END Decode;
PROCEDURE Load*
Load*;
VAR S: Texts.Scanner;
BEGIN
IF ~OSS.error & ~loaded THEN
Texts.OpenScanner(S, Oberon.Par.text, Oberon.Par.pos); OSG.Load(S);
loaded := TRUE
END
END Load;
PROCEDURE Exec*
Exec*;
VAR S: Texts.Scanner;
BEGIN
IF loaded THEN
Texts.OpenScanner(S, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(S);
IF S.class = Texts.Name THEN OSG.Exec(S) END
END
END Exec;
PROCEDURE enter (cl: INTEGER; n: LONGINT; name: OSS.Ident; type: OSG.Type);
VAR obj: OSG.Object;
BEGIN NEW(obj);
obj.class := cl; obj.val := n; obj.name := name; obj.type := type;
obj.dsc := NIL;
obj.next := topScope.next; topScope.next := obj
END enter;
BEGIN Texts.OpenWriter(W); Texts.WriteString(W, "Oberon0 Compiler 9.2.95");
Texts.WriteLn(W); Texts.Append(Oberon.Log, W.buf);
NEW(guard); guard.class := OSG.Var; guard.type := OSG.intType; guard.val := 0;
topScope := NIL; OpenScope;
182
Приложение С. Компилятор Оберон 0
enter(OSG.Typ, 1, "BOOLEAN", OSG.boolType);
enter(OSG.Typ, 2, "INTEGER", OSG.intType);
enter(OSG.Const, 1, "TRUE", OSG.boolType);
enter(OSG.Const, 0, "FALSE", OSG.boolType);
enter(OSG.SProc, 1, "Read", NIL);
enter(OSG.SProc, 2, "Write", NIL);
enter(OSG.SProc, 3, "WriteHex", NIL);
enter(OSG.SProc, 4, "WriteLn", NIL);
universe := topScope
END OSP.
C.3. Генератор кода
MODULE OSG; (* NW 18.12.94 / 10.2.95 / 24.3.96 / 25.11.05*)
IMPORT OSS, RISC, Oberon, Texts;
CONST maxCode = 1000; maxRel = 200; NofCom = 16;
(* class / mode*) Head* = 0;
Var* = 1; Par* = 2; Const* = 3; Fld* = 4; Typ* = 5; Proc* = 6; SProc* = 7;
Reg = 10; Cond = 11;
(* form *) Boolean* = 0; Integer* = 1; Array* = 2; Record* = 3;
MOV = 0; MVN = 1; ADD = 2; SUB = 3; MUL = 4; Div = 5; Mod = 6; CMP = 7;
MOVI = 16; MVNI = 17; ADDI = 18; SUBI = 19; MULI = 20; DIVI = 21; MODI = 22;
CMPI = 23;
CHKI = 24;
LDW = 32; LDB = 33; POP = 34; STW = 36; STB = 37; PSH = 38;
RD = 40; WRD = 41; WRH = 42; WRL = 43; BEQ = 48;
BNE = 49; BLT = 50; BGE = 51; BLE = 52; BGT = 53; BR = 56; BSR = 57;
RET = 58; FP = 12; SP = 13; LNK = 14; PC = 15; (*reserved registers*)
TYPE Object* = POINTER TO ObjDesc;
Type* = POINTER TO TypeDesc;
Item* = RECORD
mode*
mode*, lev*
lev*: INTEGER;
type*
type*: Type;
a*
a*, b, c, r: LONGINT;
END;
ObjDesc* = RECORD
class*
class*, lev*
lev*: INTEGER;
next*
next*, dsc*
dsc*: Object;
type*
type*: Type;
name*
name*: OSS.Ident;
val*
val*: LONGINT
END;
TypeDesc* = RECORD
form*
form*: INTEGER;
Генератор кода
183
fields*
fields*: Object;
base*
base*: Type;
size*
size*, len*
len*: INTEGER
END;
VAR boolType*
boolType*, intType*
intType*: Type;
curlev*
curlev*, pc*
pc*: INTEGER;
cno: INTEGER;
entry, fixlist: LONGINT;
regs: SET; (*
*)
W: Texts.Writer;
code: ARRAY maxCode OF LONGINT;
comname: ARRAY NofCom OF OSS.Ident; (*
comadr: ARRAY NofCom OF LONGINT;
mnemo: ARRAY 64, 5 OF CHAR; (*
*)
*)
PROCEDURE GetReg (VAR r: LONGINT);
VAR i: INTEGER;
BEGIN i := 0;
WHILE (i < FP) & (i IN regs) DO INC(i) END;
INCL(regs, i); r := i
END GetReg;
PROCEDURE Put (op, a, b, c: LONGINT);
BEGIN (*
*)
IF op >= 32 THEN DEC(op, 64) END;
code[pc] := ASH(ASH(ASH(op, 4) + a, 4) + b, 18) + (c MOD 40000H);
INC(pc)
END Put;
PROCEDURE PutBR (op, disp: LONGINT);
BEGIN (*
*)
code[pc] := ASH(op – 40H, 26) + (disp MOD 4000000H); INC(pc)
END PutBR;
PROCEDURE TestRange (x: LONGINT);
BEGIN (*18–
*)
IF (x >= 20000H) OR (x < – 20000H) THEN OSS.Mark("
END TestRange;
PROCEDURE load (VAR x: Item);
VAR r: LONGINT;
124
BEGIN (*x.mode # Reg*)
IF x.mode = Var THEN
IF x.lev = 0 THEN x.a := x.a – pc * 4 END;
GetReg(r); Put(LDW, r, x.r, x.a); EXCL(regs, x.r); x.r := r
ELSIF x.mode = Const THEN
TestRange(x.a); GetReg(x.r); Put(MOVI, x.r, 0, x.a)
END;
") END
184
Приложение С. Компилятор Оберон 0
x.mode := Reg
END load;
PROCEDURE loadBool (VAR x: Item);
BEGIN
IF x.type.form # Boolean THEN OSS.Mark("Boolean?") END;
load(x); x.mode := Cond; x.a := 0; x.b := 0; x.c := 1
END loadBool;
PROCEDURE PutOp (cd: LONGINT; VAR x, y: Item);
BEGIN
IF x.mode # Reg THEN load(x) END;
IF y.mode = Const THEN TestRange(y.a); Put(cd + 16, x.r, x.r, y.a)
ELSE
IF y.mode # Reg THEN load(y) END;
Put(cd, x.r, x.r, y.r); EXCL(regs, y.r)
END
END PutOp;
PROCEDURE negated (cond: LONGINT): LONGINT;
BEGIN
IF ODD(cond) THEN RETURN cond – 1 ELSE RETURN cond + 1 END
END negated;
PROCEDURE merged (L0, L1: LONGINT): LONGINT;
VAR L2, L3: LONGINT;
BEGIN
IF L0 # 0 THEN
L2 := L0;
LOOP L3 := code[L2] MOD 40000H;
IF L3 = 0 THEN EXIT END;
L2 := L3
END;
code[L2] := code[L2] – L3 + L1; RETURN L0
ELSE RETURN L1
END
END merged;
PROCEDURE fix (at, with: LONGINT);
BEGIN code[at] := code[at] DIV 400000H * 400000H + (with MOD 400000H)
END fix;
PROCEDURE FixWith (L0, L1: LONGINT);
VAR L2: LONGINT;
BEGIN
WHILE L0 # 0 DO L2 := code[L0] MOD 40000H; fix(L0, L1 – L0); L0 := L2 END
END FixWith;
PROCEDURE FixLink* (L: LONGINT);
VAR L1: LONGINT;
BEGIN
WHILE L # 0 DO L1 := code[L] MOD 40000H; fix(L, pc – L); L := L1 END
Генератор кода
185
END FixLink;
(*———————————————————————*)
PROCEDURE IncLevel* (n: INTEGER);
BEGIN INC(curlev, n)
END IncLevel;
PROCEDURE MakeConstItem* (VAR x: Item; typ: Type; val: LONGINT);
BEGIN x.mode := Const; x.type := typ; x.a := val
END MakeConstItem;
PROCEDURE MakeItem* (VAR x: Item; y: Object);
VAR r: LONGINT;
BEGIN x.mode := y.class; x.type := y.type; x.lev := y.lev; x.a := y.val;
x.b := 0;
IF y.lev = 0 THEN x.r := PC
ELSIF y.lev = curlev THEN x.r := FP
ELSE OSS.Mark("
!"); x.r := 0
END;
IF y.class = Par THEN
GetReg(r); Put(LDW, r, x.r, x.a); x.mode := Var; x.r := r; x.a := 0
END
END MakeItem;
PROCEDURE Field* (VAR x: Item; y: Object); (* x := x.y *)
BEGIN INC(x.a, y.val); x.type := y.type
END Field;
PROCEDURE Index* (VAR x, y: Item); (* x := x[y] *)
BEGIN
IF y.type # intType THEN OSS.Mark("
") END;
IF y.mode = Const THEN
IF (y.a < 0) OR (y.a >= x.type.len) THEN OSS.Mark("
INC(x.a, y.a * x.type.base.size)
ELSE
IF y.mode # Reg THEN load(y) END;
Put(CHKI, y.r, 0, x.type.len);
Put(MULI, y.r, y.r, x.type.base.size);
Put(ADD, y.r, x.r, y.r); EXCL(regs, x.r); x.r := y.r
END;
x.type := x.type.base
END Index;
PROCEDURE Op1* (op: INTEGER; VAR x: Item); (* x := op x *)
VAR t: LONGINT;
BEGIN
IF op = OSS.minus THEN
IF x.type.form # Integer THEN OSS.Mark("
")
") END;
186
Приложение С. Компилятор Оберон 0
ELSIF x.mode = Const THEN x.a := – x.a
ELSE
IF x.mode = Var THEN load(x) END;
Put(MVN, x.r, 0, x.r)
END
ELSIF op = OSS.not THEN
IF x.mode # Cond THEN loadBool(x) END;
x.c := negated(x.c); t := x.a; x.a := x.b; x.b := t
ELSIF op = OSS.and THEN
IF x.mode # Cond THEN loadBool(x) END;
PutBR(BEQ + negated(x.c), x.a); EXCL(regs, x.r); x.a := pc – 1;
FixLink(x.b); x.b := 0
ELSIF op = OSS.or THEN
IF x.mode # Cond THEN loadBool(x) END;
PutBR(BEQ + x.c, x.b); EXCL(regs, x.r); x.b := pc – 1; FixLink(x.a);
x.a := 0
END
END Op1;
PROCEDURE Op2* (op: INTEGER; VAR x, y: Item); (* x := x op y *)
BEGIN
IF (x.type.form = Integer) & (y.type.form = Integer) THEN
IF (x.mode = Const) & (y.mode = Const) THEN
(*
*)
IF op = OSS.plus THEN INC(x.a, y.a)
ELSIF op = OSS.minus THEN DEC(x.a, y.a)
ELSIF op = OSS.times THEN x.a := x.a * y.a
ELSIF op = OSS.div THEN x.a := x.a DIV y.a
ELSIF op = OSS.mod THEN x.a := x.a MOD y.a
ELSE OSS.Mark("
")
END
ELSE
IF op = OSS.plus THEN PutOp(ADD, x, y)
ELSIF op = OSS.minus THEN PutOp(SUB, x, y)
ELSIF op = OSS.times THEN PutOp(MUL, x, y)
ELSIF op = OSS.div THEN PutOp(Div, x, y)
ELSIF op = OSS.mod THEN PutOp(Mod, x, y)
ELSE OSS.Mark("
")
END
END
ELSIF (x.type.form = Boolean) & (y.type.form = Boolean) THEN
IF y.mode # Cond THEN loadBool(y) END;
IF op = OSS.or THEN x.a := y.a; x.b := merged(y.b, x.b); x.c := y.c
ELSIF op = OSS.and THEN x.a := merged(y.a, x.a); x.b := y.b; x.c := y.c
END
ELSE OSS.Mark("
")
END;
END Op2;
PROCEDURE Relation* (op: INTEGER; VAR x, y: Item); (* x := x ? y *)
Генератор кода
187
BEGIN
IF (x.type.form # Integer) OR (y.type.form # Integer) THEN
OSS.Mark("
")
ELSE PutOp(CMP, x, y); x.c := op – OSS.eql; EXCL(regs, y.r)
END;
x.mode := Cond; x.type := boolType; x.a := 0; x.b := 0
END Relation;
PROCEDURE Store* (VAR x, y: Item); (* x := y *)
VAR r: LONGINT;
BEGIN
IF (x.type.form IN {Boolean, Integer}) & (x.type.form = y.type.form) THEN
IF y.mode = Cond THEN
Put(BEQ + negated(y.c), y.r, 0, y.a); EXCL(regs, y.r); y.a := pc – 1;
FixLink(y.b); GetReg(y.r); Put(MOVI, y.r, 0, 1); PutBR(BR, 2);
FixLink(y.a); Put(MOVI, y.r, 0, 0)
127
ELSIF y.mode # Reg THEN load(y)
END;
IF x.mode = Var THEN
IF x.lev = 0 THEN x.a := x.a – pc * 4 END;
Put(STW, y.r, x.r, x.a)
ELSE OSS.Mark("
")
END;
EXCL(regs, x.r); EXCL(regs, y.r)
ELSE OSS.Mark("
")
END
END Store;
PROCEDURE Parameter* (VAR x: Item; ftyp: Type; class: INTEGER);
VAR r: LONGINT;
BEGIN
IF x.type = ftyp THEN
IF class = Par THEN (*
*)
IF x.mode = Var THEN
IF x.a # 0 THEN GetReg(r); Put(ADDI, r, x.r, x.a)
ELSE r := x.r
END
ELSE OSS.Mark("
")
END;
Put(PSH, r, SP, 4); EXCL(regs, r)
ELSE (*value param*)
IF x.mode # Reg THEN load(x) END;
Put(PSH, x.r, SP, 4); EXCL(regs, x.r)
END
ELSE OSS.Mark("
")
END
END Parameter;
(*————————————————*)
188
Приложение С. Компилятор Оберон 0
PROCEDURE CJump* (VAR x: Item);
BEGIN
IF x.type.form = Boolean THEN
IF x.mode # Cond THEN loadBool(x) END;
PutBR(BEQ + negated(x.c), x.a); EXCL(regs, x.r); FixLink(x.b);
x.a := pc – 1
ELSE OSS.Mark("Boolean?"); x.a := pc
END
END CJump;
PROCEDURE BJump* (L: LONGINT);
BEGIN PutBR(BR, L – pc)
END BJump;
PROCEDURE FJump* (VAR L: LONGINT);
BEGIN PutBR(BR, L); L := pc – 1
END FJump;
PROCEDURE Call* (VAR x: Item);
BEGIN PutBR(BSR, x.a – pc)
END Call;
PROCEDURE IOCall* (VAR x, y: Item);
VAR z: Item;
BEGIN
IF x.a < 4 THEN
IF y.type.form # Integer THEN OSS.Mark("Integer?") END
END;
IF x.a = 1 THEN
GetReg(z.r); z.mode := Reg; z.type := intType; Put(RD, z.r, 0, 0);
Store(y, z)
ELSIF x.a = 2 THEN load(y); Put(WRD, 0, 0, y.r); EXCL(regs, y.r)
ELSIF x.a = 3 THEN load(y); Put(WRH, 0, 0, y.r); EXCL(regs, y.r)
ELSE Put(WRL, 0, 0, 0)
END
END IOCall;
PROCEDURE Header* (size: LONGINT);
BEGIN entry := pc; Put(MOVI, SP, 0, RISC.MemSize – size); (*
Put(PSH, LNK, SP, 4)
END Header;
PROCEDURE Enter* (size: LONGINT);
BEGIN
Put(PSH, LNK, SP, 4);
Put(PSH, FP, SP, 4);
Put(MOV, FP, 0, SP);
Put(SUBI, SP, SP, size)
END Enter;
SP*)
Генератор кода
189
PROCEDURE Return* (size: LONGINT);
BEGIN
Put(MOV, SP, 0, FP);
Put(POP, FP, SP, 4);
Put(POP, LNK, SP, size + 4);
PutBR(RET, LNK)
END Return;
PROCEDURE Open*
Open*;
BEGIN curlev := 0; pc := 0; cno := 0; regs := {}
END Open;
PROCEDURE Close* (VAR S: Texts.Scanner; globals: LONGINT);
BEGIN Put(POP, LNK, SP, 4); PutBR(RET, LNK);
END Close;
PROCEDURE EnterCmd* (VAR name: ARRAY OF CHAR);
BEGIN COPY(name, comname[cno]); comadr[cno] := pc * 4; INC(cno)
END EnterCmd;
(*—————————————————————*)
PROCEDURE Load* (VAR S: Texts.Scanner);
BEGIN RISC.Load(code, pc);
Texts.WriteString(W, "
"); Texts.WriteLn(W);
Texts.Append(Oberon.Log, W.buf);
RISC.Execute(entry * 4, S, Oberon.Log)
END Load;
PROCEDURE Exec* (VAR S: Texts.Scanner);
VAR i: INTEGER;
BEGIN i := 0;
WHILE (i < cno) & (S.s # comname[i]) DO INC(i) END;
IF i < cno THEN RISC.Execute(comadr[i], S, Oberon.Log) END
END Exec;
PROCEDURE Decode* (T: Texts.Text);
VAR i, w, op, a: LONGINT;
BEGIN Texts.WriteString(W, "entry"); Texts.WriteInt(W,
* 4, 6);
Texts.WriteLn(W);
i := 0;
WHILE i < pc DO
w := code[i]; op := w DIV 4000000H MOD 40H;
Texts.WriteInt(W, 4 * i, 4); Texts.Write(W, 9X);
Texts.WriteString(W, mnemo[op]);
IF op < BEQ THEN
a := w MOD 40000H;
IF a >= 20000H THEN DEC(a, 40000H) (*
*) END;
Texts.Write(W, 9X); Texts.WriteInt(W, w DIV 400000H MOD 10H, 4);
190
Приложение С. Компилятор Оберон 0
Texts.Write(W, ","); Texts.WriteInt(W, w DIV 40000H MOD 10H, 4);
Texts.Write(W, ",")
ELSE a := w MOD 4000000H;
IF a >= 2000000H THEN DEC(a, 4000000H) (*
*) END
END;
Texts.WriteInt(W, a, 6); Texts.WriteLn(W); INC(i)
END;
Texts.WriteLn(W); Texts.Append(T, W.buf)
END Decode;
BEGIN Texts.OpenWriter(W);
NEW(boolType); boolType.form := Boolean; boolType.size := 4;
NEW(intType); intType.form := Integer; intType.size := 4;
mnemo[MOV] := "MOV ";
mnemo[MVN] := "MVN ";
mnemo[ADD] := "ADD ";
mnemo[SUB] := "SUB ";
mnemo[MUL] := "MUL ";
mnemo[Div] := "DIV ";
mnemo[Mod] := "MOD ";
mnemo[CMP] := "CMP ";
mnemo[MOVI] := "MOVI";
mnemo[MVNI] := "MVNI";
mnemo[ADDI] := "ADDI";
mnemo[SUBI] := "SUBI";
mnemo[MULI] := "MULI";
mnemo[DIVI] := "DIVI";
mnemo[MODI] := "MODI";
mnemo[CMPI] := "CMPI";
mnemo[CHKI] := "CHKI";
mnemo[LDW] := "LDW ";
mnemo[LDB] := "LDB ";
mnemo[POP] := "POP ";
mnemo[STW] := "STW ";
mnemo[STB] := "STB ";
mnemo[PSH] := "PSH ";
mnemo[BEQ] := "BEQ ";
mnemo[BNE] := "BNE ";
mnemo[BLT] := "BLT ";
mnemo[BGE] := "BGE ";
mnemo[BLE] := "BLE ";
mnemo[BGT] := "BGT ";
mnemo[BR] := "BR ";
mnemo[BSR] := "BSR ";
mnemo[RET] := "RET ";
mnemo[RD] := "READ";
mnemo[WRD] := "WRD ";
mnemo[WRH] := "WRH ";
mnemo[WRL] := "WRL ";
END OSG.
Литература
1. Aho A. V., Ullman J. D. Principles of Compiler Design. Reading MA: Addison
Wesley, 1985.
2. DeRemer F. L. Simple LR(k) grammars. Comm. ACM, 14, 7 (July 1971), 453–460.
3. Franz M. The case for universal symbol files. Structured Programming 14 (1993),
136–147.
4. Graham S. L., Rhodes S. P. Practical syntax error recovery. Comm. ACM, 18, 11
(Nov. 1975), 639–650.
5. Hennessy J. L., Patterson D. A. Computer Architecture. A Quantitative Approach.
Morgan Kaufmann, 1990.
6. Hoare C. A. R. Notes on data structuring. In Structured Programming / Dahl O. J.,
Dijkstra E. W., Hoare C. A. R., Acad. Press, 1972. (Русский перевод: Хоор К.
О структурной организации данных. В сб. «Структурное программирование» /
Дал У., Декстра Э., Хоор К. – М.: Мир, 1975.)
7. Kastens U. Uebersetzerbau. Oldenbourg, 1990.
8. Knuth D. E. On the translation of languages from left to right. Information and
Control, 8, 6 (Dec. 1965), 607–639. (Русский перевод: Кнут Д. О переводе
(трансляции) языков слева направо. В сб. «Языки и автоматы». – М.: Мир,
1975. С. 9–42.)
9. Knuth D. E. Top down syntax analysis. Acta Informatica 1 (1971), 79–110.
10. LaLonde W. R., et al. An LALR(k) parser generator. Proc. IFIP Congress 71,
North Holland, 153–157.
11. Mitchell J. G., Maybury W., Sweet R. Mesa Language Manual. Xerox Palo Alto
Research Center, Technical Report CSL 78 3.
12. Naur (Ed) P. Report on the algorithmic language Algol 60. Comm. ACM, 3
(1960), 299–314, and Comm. ACM, 6, 1 (1963), 1–17. (Русский перевод: Алго
ритмический язык АЛГОЛ 60. – М.: Мир, 1965.)
13. Rechenberg P., Mo_ssenbo_ck H. Ein Compiler Generator fu_r Mikrocomputer.
C. Hanser, 1985.
14. Reiser M., Wirth N. Programming in Oberon. Wokingham: Addison Wesley, 1992.
15. Wirth N. The programming language Pascal. Acta Informatica 1 (1971).
16. Wirth N. Modula – A programming language for modular multiprogramming.
Software – Practice and Experience, 7 (1977), 3–35.
17. Wirth N. What can we do about the unnecessary diversity of notation for syntactic
definitions? Comm. ACM, 20 (1977), 11, 822–823.
18. Wirth N. Programming in Modula 2. Heidelberg: Springer Verlag, 1982. (Рус
ский перевод: Вирт Н. Программирование на языке Модула 2. – М.: Мир,
1987.)
19. Wirth N. and Gutknecht J. Project Oberon. Wokingham: Addison Wesley, 1992.
20. Ахо А., Сети Р., Ульман Дж. Компиляторы: принципы, технологии и инстру
менты. – М.: Издательский дом «Вильямс», 2001.
Книги издательства «ДМК Пресс» можно заказать в торгово издательском
холдинге «АЛЬЯНС КНИГА» наложенным платежом, выслав открытку или
письмо по почтовому адресу: 123242, Москва, а/я 20 или по электронному ад
ресу: orders@alians kniga.ru.
При оформлении заказа следует указать адрес (полностью), по которо
му должны быть высланы книги; фамилию, имя и отчество получателя.
Желательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в Internet магазине: www.alians kniga.ru.
Оптовые закупки: тел. (495) 258 91 94, 258 91 95; электронный адрес
books@alians kniga.ru.
Никлаус Вирт
Построение компиляторов
Главный редактор
Мовчан Д. А.
dm@dmk press.ru
Перевод
Корректор
Верстка
Дизайн обложки
Борисов Е. В., Чернышов Л. Н.
Синяева Г. И.
Чаннова А. А.
Мовчан А. Г.
Подписано в печать 16.10.2009. Формат 70×100 1/16 .
Гарнитура «Петербург». Печать офсетная.
Усл. печ. л. 36. Тираж 1000 экз.
№
Web сайт издательства: www.dmk press.ru
Документ
Категория
Информатика
Просмотров
91
Размер файла
735 Кб
Теги
1/--страниц
Пожаловаться на содержимое документа