Текст
                    Кмачкл
iijmlnMMulmlaHun
ие
Никлаус Вирт
Построек
компиляторов


Никлаус Вирт Построение компиляторов Москва, 2010
УДК 32.973.26-018.2 ББК 004.438 В52 Никлаус Вирт Построение компиляторов / Пер. с англ. Борисов Е. В.. Чернышов Л. Н. - М.: ДМК Пресс, 2010. - 192 с.: ил. ISBN 978-5-94074-585-3 Книга известного специалиста в области информатики Никлауса Вирта написана по материалам его лекций по вводному курсу проектирования компиляторов. На примере простого языка Оберон-О рассмотрены все эле- менты транслятора, включая оптимизацию и генерацию кода. Приведен полный текст компилятора на языке программирования Оберон. Для программистов, преподавателей и студентов, изучающих системное программирование и методы трансляции. Содержание компакт-диска: Базовая конфигурация системы Блэкбокс с коллекцией модулей, реализу- ющих оригинальный компилятор с языка Оберон-О и компилятор, адапти- рованный под Блэкбокс. Базовые инструкции по работе в системе Блэкбокс. Полный перевод документации системы Блэкбокс на русский язык. Конфигурация системы Блэкбокс для использования во вводных курсах программирования в университетах. Конфигурация системы Блэкбокс для использования в школах (полная ру- сификация меню, сообщений компилятора, с возможностью использования ключевых слов на русском и других национальных языках). Доклады участников проекта Информатика-21 по опыту использования системы Блэкбокс в обучении программированию. Оригинальные дистрибутивы системы Блэкбокс 1.5 (основной рабочий) и 1.6гс6. Инструкции по работе в Блэкбоксе под 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 (анг.) ©N. Wirth, 1985 (Oberon version: August 2004) © Перевод с английского Борисов E. В., Чернышов Л. Н., 2010 ISBN 978-5-94074-585-3 © Оформление, издание, ДМК Пресс, 2010
Краткое содержание ОТ АВТОРОВ ПЕРЕВОДА.................ю ВВЕДЕНИЕ............................12 ГЛАВА 1. ВВЕДЕНИЕ...................15 ГЛАВА 2. ЯЗЫК И СИНТАКСИС..........19 ГЛАВА 3. РЕГУЛЯРНЫЕ ЯЗЫКИ..........27 ГЛАВА 4. АНАЛИЗ КОНТЕКСТНО-СВОБОДНЫХ ЯЗЫКОВ.............................33 ГЛАВА 5. АТРИБУТНЫЕ ГРАММАТИКИ И СЕМАНТИКИ........................45 ГЛАВА 6. ЯЗЫК ПРОГРАММИРОВАНИЯ ОБЕРОН-О...........................51 ГЛАВА 7. СИНТАКСИЧЕСКИЙ АНАЛИЗАТОР ДЛЯ ОБЕРОНА-О......................55 ГЛАВА 8. УЧЕТ КОНТЕКСТА, ЗАДАННОГО ОБЪЯВЛЕНИЯМИ.......................65 ГЛАВА 9. RISC-АРХИТЕКТУРА КАК ЦЕЛЬ.75 ГЛАВА 10. ВЫРАЖЕНИЯ И ПРИСВАИВАНИЯ.81 ГЛАВА 11. УСЛОВНЫЕ И ЦИКЛИЧЕСКИЕ ОПЕРАТОРЫ И ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ...95
4 Содержание ГЛАВА 12. ПРОЦЕДУРЫ И КОНЦЕПЦИЯ ЛОКАЛИЗАЦИИ.........................Ю9 ГЛАВА 13. ЭЛЕМЕНТАРНЫЕ ТИПЫ ДАННЫХ 125 ГЛАВА 14. ОТКРЫТЫЕ МАССИВЫ, УКАЗАТЕЛЬНЫЙ И ПРОЦЕДУРНЫЙ ТИПЫ....131 ГЛАВА 15. МОДУЛИ И РАЗДЕЛЬНАЯ КОМПИЛЯЦИЯ.........................141 ГЛАВА 16. ОПТИМИЗАЦИЯ И СТРУКТУРА ПРЕ/ПОСТПРОЦЕССОРА.................153 ПРИЛОЖЕНИЕ А. СИНТАКСИС........... 164 ПРИЛОЖЕНИЕ В. НАБОР СИМВОЛОВ ASCII.167 ПРИЛОЖЕНИЕ С. КОМПИЛЯТОР ОБЕРОН-О..168 ЛИТЕРАТУРА.........................191
Содержание От авторов перевода...............................ю О книге........................................ ю О переводе.......................................Ю Введение.........................................12 Предисловие.....................................12 Благодарности..................................14 Глава 1. Введение................................15 Глава 2. Язык и синтаксис........................19 2.1. Упражнения............................... 24 Глава 3. Регулярные языки........................27 3.1. Упражнение.................................32 Глава 4. Анализ контекстно-свободных языков......зз 4.1. Метод рекурсивного спуска..................34 4.2. Таблично-управляемый нисходящий синтаксический анализ..........................................38 4.3. Восходящий синтаксический анализ.......... 40 4.4. Упражнения.................................42 Глава 5. Атрибутные грамматики и семантики .45 5.1. Правила типов..............................46 5.2. Правила вычислений.........................^7 5.3. Правила трансляции.........................^8 5.4. Упражнение................................. Глава 6. Язык программирования Оберон-О..........51 6.1. Упражнение.................................54
6 Содержание Глава 7. Синтаксический анализатор для Оберона-О..................................... 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. Процедуры и концепция локализации.......................................юэ 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 Приложение А. Синтаксис............................164 А1. Оберон-О......................................164 А2. Оберон........................................164 АЗ. Символьные файлы..............................166
8 Содержание Приложение В. Набор символов ASCII.............167 Приложение С. Компилятор Оберон-О..............168 С.1. Лексический анализатор...................169 С.2. Синтаксический анализатор................172 С.З. Генератор кода...........................182 Литература.....................................191
От авторов перевода О книге Давно известно, что лучший способ постичь секреты мастерства - это наблюдать за работой мастера. Эта небольшая, но насыщенная информацией книжка, по сути дела, представляет собой отчет о такой работе. Ну а то, что ее автор - настоящий мастер своего дела, сомнению не подлежит, потому что имя профессора Никлауса Вирта ни в каких дополнительных рекомендациях не нуждается. Эта книга - сво- его рода мастер-класс, который дает своим ученикам всемирно известный маэст- ро. Она не является ни «тяжелой» теоретической монографией, ни сборником на- ставлений и поучений увенчанного лаврами мэтра. Эта книжка - практическое пособие для всех тех любознательных людей, кто желает разобраться и понять, что такое компилятор и как он устроен. По мнению автора, без этого ни один про- граммист не может называть себя квалифицированным специалистом. В отличие от многочисленных книг, которые исчерпывающе описывают и тео- рию, и разнообразные методы синтаксического анализа, перевода и компиляции, эта книжка посвящена реализации одного-единственного компилятора современ- ного языка программирования для конкретного компьютера. Но это нисколько не умаляет ее достоинства. Если обычные книги после прочтения почти всегда ос- тавляют читателя наедине с вопросом «А что же дальше? Где же результат?» или с загадочными, полными опечаток текстами готовых программ, то эта небольшая книжка расставляет практически все точки над i, проводя читателя от самого на- чала до самого конца процесса разработки компилятора, попутно предупреждая его о неверных шагах и давая ему в руки богатый практический материал. Автор придерживается принципа «Делай со мной. Делай, как я. Делай лучше меня». Таким образом, книга Н. Вирта - безусловно, не только прекрасное дополне- ние к многочисленным и столь же прекрасным фундаментальным трудам по этой теме, но может и должна использоваться в качестве практического пособия по изучению компиляторов. Кроме того, простота и доступность преподнесения до- вольно сложного материала снимает с него покров таинственности и делает его доступным практически каждому любителю программирования. Остается только сожалеть о том, что эта книга не была своевременно переведена и издана у нас. Для практического использования текст компилятора Оберон-О, о котором идет речь в книге, адаптирован к системе БлэкБокс (BlackBox Component Builder - вариант системы Оберон). Оригинальные и адаптированные исходные тексты компилятора можно найти на сайте www.oberoncore.ru. О переводе Несколько слов о переводе. В силу того, что мы имеем дело не с развернутой монографией, а с конспектом лекций, каждая фраза, часто облекаемая в форму тезиса, до предела насыщена
10 От авторов перевода информацией. Поэтому наша основная задача при переводе состояла в том, чтобы сохранить лаконичность и информационную насыщенность авторского текста и при этом максимально точно довести его суть до читателя, не поддаваясь искуше- нию сдобрить его отсебятиной. Несмотря на царящие до сих пор «разброд и шатания» в терминологии по этой теме, мы при переводе, следуя за автором, отдавали предпочтение наиболее усто- явшимся, хотя и не всегда правильным и точным, терминам. В связи с этим нельзя не упомянуть о терминах «front-end» и «back-end». Они уже давно употребляются в разнообразной англоязычной технической литерату- ре, но тем не менее до сих пор не находят адекватных русскоязычных эквивален- тов. Чаще всего их перевод зависит от контекста. Применительно к компиляторам наиболее точными их русскими аналогами являются, пожалуй, «машинно-неза- висимая часть» и «машинно-зависимая часть» соответственно. Однако мы, те- перь уже следуя авторской лаконичности, предпочли им более абстрактные и ме- нее точные, но более короткие термины - «препроцессор» и «постпроцессор» соответственно. Кроме того, список литературы пронумерован, и именные ссылки на него в тексте заменены номерными. К списку литературы добавлено несколько более поздних публикаций. Авторы перевода выражают благодарность В. Н. Лукину за прочтение перево- да и сделанные замечания.
Введение Предисловие Эта книга появилась из моих конспектов лекций по вводному курсу проектирова- ния компиляторов в ЕТН (Федеральном технологическом институте) в Цюрихе. Несколько раз меня просили объяснить необходимость этого курса, так как про- ектирование компиляторов рассматривается как некий эзотерический предмет, применяемый только в нескольких узкоспециализированных программистских фирмах. Поскольку в наши дни все, что не приносит немедленной выгоды, долж- но быть оправдано, я должен попробовать объяснить, почему я вообще считаю этот предмет важным и уместным для студентов, изучающих информатику. Основой любого академического образования является то, что передается не только знание и, в случае инженерного образования, «ноу-хау», но и понимание сути явления и способность проникнуть в его суть. В частности, в информатике мало поверхностного знания системы, необходимо еще и понимание ее содер- жания. Каждый образованный программист должен знать возможности компью- тера, понимать способы и методы представления и интерпретации программ. Компилятор преобразует текст программы во внутренний код, это мост, соединя- ющий программное обеспечение и аппаратные средства. Однако кому-то может показаться, что нет необходимости знать методы транс- ляции для понимания связи между исполняемой программой и кодом и еще менее важно знать, как на самом деле пишется компилятор. Личный опыт преподавате- ля подсказывает мне, что глубокое понимание предмета лучше всего приходит при всестороннем проникновении как в общую идею системы, так и в детали ее реализации. В нашем случае таким проникновением будет написание реального компилятора. Конечно, мы должны сосредоточиться на основах. В конце концов, эта книга - вводный курс, а не справочник для специалистов. Наше первое ограничение каса- ется входного языка. Было бы неуместным рассматривать проектирование ком- пиляторов для больших языков. Язык должен быть небольшим, но тем не менре должен содержать все поистине фундаментальные элементы языков программи- рования. Для наших целей мы выбрали подмножество языка Оберон. Второе ограничение касается целевого компьютера. Он должен иметь обычную структу- ру и простой набор команд. Наиболее важна практичность обучающих понятий. Оберон - это общецелевой гибкий и мощный язык, а наш целевой компьютер иде- альным образом отражает удачную RISC-архитектуру. И наконец, третье огра- ничение состоит в отказе от изощренных методов оптимизации кода. При таких условиях можно объяснить весь компилятор в деталях и даже создать его в огра- ниченные рамками курса сроки. В главах 2 и 3 рассматриваются основы языка и синтаксиса. Глава 4 посвящена синтаксическому анализу, то есть методу разбора предложений и программ. Мы
12 Теория и методы построения компиляторов. Введение сосредоточили внимание на простом, но удивительно мощном методе рекурсив- ного спуска, который используется в нашем иллюстративном компиляторе. Мы рассматриваем синтаксический анализ как средство для достижения цели, но не как самоцель. Глава 5 готовит нас к переходу от синтаксического анализатора к компилятору, а выбранный метод ставится в зависимость от атрибутов синтак- сических конструкций. После знакомства в главе 6 с языком Оберон-О в главе 7 приводится разработ- ка его синтаксического анализатора методом рекурсивного спуска. Из практиче- ских соображений обсуждается также обработка синтаксических ошибок. В гла- ве 8 мы объясняем, почему языки, содержащие объявления и, следовательно, зависимость от контекста, могут тем не менее обрабатываться как контекстно- свободные. До этого момента не было необходимости в рассмотрении целевого компьюте- ра и набора его команд. Но поскольку последующие главы посвящены теме гене- рации кода, то становится неизбежной спецификация целевого компьютера (гла- ва 9). Это RISC-архитектура с небольшим набором команд и набором регистров. В связи с этим центральная тема разработки компилятора - генерация последова- тельностей команд - разнесена по трем главам: код для выражений и присваива- ний (глава 10), код для условных операторов и операторов цикла (глава И), код для объявлений процедур и обращений к ним (глава 12). Вместе они покрывают все конструкции языка Оберон-О. Последующие главы посвящены нескольким дополнительным важным конст- рукциям языков программирования общего назначения. Их трактовка поверхност- на и не затрагивает деталей, но подкреплена несколькими упражнениями в конце соответствующих глав. Рассматриваются следующие темы: элементарные типы данных (глава 13), открытые массивы, динамические структуры данных и проце- дурные типы, называемые методами в объектно-ориентированной терминологии (глава 14). Глава 15 касается модульного конструирования и принципов скрытия инфор- мации. Это приводит к теме разработки программного обеспечения в команде, ос- нованной на определении интерфейсов и последующей независимой реализации частей (модулей). Методика основана на раздельной компиляции модулей с пол- ным контролем совместимости типов всех компонентов интерфейса. Такая мето- дика имеет первостепенное значение для разработки программного обеспечения в целом и для современных языков программирования в частности. Наконец, глава 16 дает краткий обзор проблем оптимизации кода. Она необхо- дима, с одной стороны, из-за семантической пропасти между исходными языками и архитектурами компьютеров, а с другой - из-за нашего желания как можно пол- нее использовать все доступные ресурсы компьютеров.
Теория и методы построения компиляторов. Введение 13 Благодарности Я выражаю мои искренние благодарности всем, кто способствовал своими пред- ложениями и критикой этой книги, которая созрела за многие годы преподавания курса проектирования компиляторов в ЕТН в Цюрихе. В частности, я обязан Хан- спетеру Месенбоку и Михаэлю Францу, которые внимательно прочли рукопись и подвергли ее критическому разбору. Кроме того, я благодарю Штефана Геринга, Штефана Людвига и Джозефа Темпла за их ценные комментарии и сотрудниче- ство в курсе обучения. Никлаус Вирт Декабрь 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. Многопроходная компиляция Современные компьютеры с их практически неограниченными объемами па- мяти дают возможность исключить промежуточное хранение данных на диске. Вместе с этим можно отказаться как от сложного процесса линеаризации структу- ры данных на выходе, так и от воссоздания ее на входе. Поэтому однопроходные компиляторы могут увеличить скорость компиляции в несколько тысяч раз. Вме- сто того чтобы цепляться одна за другую в строго последовательном порядке, раз- личные части (задачи) чередуются. Например, генерация кода не ждет, пока за- вершатся все подготовительные задачи, а начинается сразу после распознавания первой сентенциальной структуры исходного текста. Разумным компромиссом является компилятор, состоящий из двух частей, называемых препроцессором (Jront end) и постпроцессором (back end). Первая часть включает лексический и синтаксический анализы с контролем типов и гене- рирует дерево, представляющее синтаксическую структуру исходного текста. Это дерево хранится в основной памяти и образует интерфейс для второй части, кото- рая выполняет генерацию кода. Основное преимущество такого решения заклю- чается в независимости препроцессора компилятора от целевого компьютера и его системы команд. Это преимущество неоценимо, когда нужны компиляторы одного и того же языка для разных компьютеров, потому что один и тот же пре- процессор служит им всем. Идея разделения исходного языка и целевой архитектуры привела также к со- зданию проектов с несколькими препроцессорами для различных языков, генери- рующими деревья для единственного постпроцессора. Если для трансляции т
18 Введение языков для п компьютеров раньше было необходимо т х п компиляторов, то те- перь достаточно т препроцессоров и п постпроцессоров (рис. 1.2). Это современное решение задачи переноса компилятора напоминает нам под- ход, который сыграл значительную роль в распространении Паскаля примерно в 1975 году [15]. Роль структурного дерева была возложена на линеаризованную форму - последовательность команд абстрактного компьютера. Постпроцессор состоял из программы-интерпретатора, реализация которой не вызывала боль- ших трудностей, а последовательность команд называлась P-кодом. Недостатком этого решения была потеря эффективности, свойственная интерпретаторам. Часто встречаются компиляторы, которые генерируют не двоичный код сразу, а сначала текст ассемблера. Для окончательной трансляции вслед за компилято- ром запускается еще и ассемблер, в силу чего неизбежно увеличивается время трансляции. Так как эта схема едва ли сулит какие-то преимущества, мы не реко- мендуем такой подход. Более того, языки высокого уровня все чаще используются для программиро- вания микроконтроллеров, применяемых в различных технических устройствах. Подобные устройства используются прежде всего для сбора данных и автомати- ческого управления машинами. В таких случаях объем рабочей памяти обычно небольшой и недостаточен для того, чтобы разместить в нем компилятор. И тогда программное обеспечение генерируется на других компьютерах, способных к компиляции. Компилятор, создающий код для компьютера, отличного от того, на котором выполняется компиляция, называется кросс-компилятором. Сгене- рированный код в этом случае передается, или загружается, по линии передачи данных. В следующих главах мы сосредоточимся на теоретических основах проектиро- вания компиляторов, а затем - на разработке настоящего однопроходного компи- лятора.
Глава 2 Язык и синтаксис
20 Язык и синтаксис Каждый язык обладает структурой, называемой грамматикой, или синтаксисом. Например, правйльное предложение (в английском языке - Прим, перев.) всегда состоит из подлежащего и следующего за ним сказуемого. Под правильным здесь понимается правильно составленное предложение. Это можно описать следующей формулой: предложение = подлежащее сказуемое. Если мы добавим к этой формуле еще две подлежащее = "Джон” | "Мария", сказуемое = "ест” | "говорит". то с их помощью получим ровно четыре возможных предложения, а именно: Джон ест Мария ест Джон говорит Мария говорит где символ | должен произноситься как или. Мы назовем эти формулы синтакси- ческими правилами, продукциями или просто синтаксическими уравнениями. Под- лежащее и сказуемое - это синтаксические классы. Краткая запись этих формул пренебрегает смыслом идентификаторов: S = АВ. L = {ас, ad, be, bd} А = "а" | "b". В = "с" | "d". Мы будем использовать такую сокращенную запись в последующих кратких примерах. Множество L предложений, которые могут быть сгенерированы этим способом, то есть повторяющейся заменой левых частей уравнений правыми, на- зывается языком. Приведенный выше пример, очевидно, определяет язык, состоящий только из четырех предложений. Обычно язык содержит бесконечно много предложений. Следующий пример показывает, что бесконечное множество может быть очень просто определено конечным числом уравнений. Символ 0 обозначает пустую последовательность. S = A. L = {0, а, аа, ааа, аааа,...} А - "а" А | 0. Метод, позволяющий выполнять подстановку (здесь "а"А вместо А) бесконеч- ное число раз, называется рекурсией. Наш третий пример опять основан на применении рекурсии. Но он генерирует не только предложения, состоящие из произвольной последовательности одного и того же символа, но и вложенные предложения: S = A. L = {b, abc, aabcc, aaabccc,...} А - "а" А "с" | "Ь". Понятно, что таким образом может быть выражена произвольно глубокая вло- женность (здесь - для А), что особенно важно в определении структурированных языков.
Язык и синтаксис 21 Наш четвертый, и последний, пример показывает структуру выражений. Сим- волы Е, Т, F и V обозначают выражение, слагаемое, множитель и переменную соот- ветственно. Е = Т | "+" Т. Т = F | Т F. F = V | "(” Е ")". V = "а” | "Ь" I "с” | "d". Из этого примера видно, что синтаксис не только определяет множество пред- ложений языка, нои наделяет их структурой. Синтаксис раскладывает предложе- ния на составляющие, как показано в примере на рис. 2.1. Графические представ- ления называются структурными деревьями, или синтаксическими деревьями. J а*Ь + с | | а + Ь*с | | (вН>)*(сЫ) | Рис. 2.1. Структура выражений Сформулируем представленные выше понятия более строго. Язык (порождающая язык грамматика - Прим, перев.) определяется следую- щим образом: 1. Множество терминальных символов. Это символы, которые появляются в предложениях. Говорят, что они терминальные, потому что не могут быть заменены никакими другими символами. Процесс подстановки заканчива- ется терминальными символами. В нашем первом примере это множество состоит из элементов а, Ь, с и d. Это множество также называется словарем. 2. Множество нетерминальных символов. Они обозначают синтаксические классы и могут замещаться в результате подстановок. В нашем первом при- мере это множество состоит из элементов S, А и В. 3. Множество синтаксических уравнений (также называемых продукциями). Они определяют возможные подстановки нетерминальных символов. Уравнение задается для каждого нетерминального символа.
22 Язык и синтаксис 4. Начальный символ. Это нетерминальный символ, обозначаемый в примерах как S. Таким образом, язык - это множество цепочек терминальных символов, кото- рые могут быть выведены из начального символа многократным применением синтаксических уравнений, то есть подстановок. Желательно также строго и точно определить нотацию, в которой записывают- ся синтаксические уравнения. Пусть нетерминальные символы будут идентифи- каторами, как в языках программирования, то есть последовательностями букв (и, возможно, цифр), например expression, term. Пусть терминальные символы будут последовательностями символов, заключенными в кавычки (строками), на- пример Т. Для определения структуры этих уравнений удобно воспользо- ваться тем же самым инструментом, который только что был определен: « production syntax | 0. = identifier "=" expression = term I expression T term. = factor | term factor. = identifier | string. = letter | identifier letter | identifier digit. = stringhead"”". = | stringhead character. = "A" | ... | "Z". = ”0" I ... I "9”. syntax production expression term factor identifier string stringhead letter digit Эта нотация почти в таком же виде была введена в 1960 году Дж. Бэкусом и П. Науром для формального описания синтаксиса языка Алгол 60 и поэтому полу- чила название формы Бэкуса-Наура (БНФ) [12]. Как показывает пример, ис- пользование рекурсии для простых повторений несколько мешает их восприя- тию. Поэтому мы расширим эту нотацию двумя конструкциями, выражающими повторение и необязательность. Кроме этого, разрешим выражения заключать в скобки. Таким образом, вводится расширение БНФ, называемое РБНФ [17], ко- торым мы снова воспользуемся для его же точного определения: syntax production expression term factor = { production }. = identifier "=" expression = term {”Г term }. = factor { factor}. = identifier | строка | ”(" expression ")" |"[" expression I"{" expression T- = letter {letter | digit}. = {character} = ’A" |... I "Z”. = “0" |... I “9”. identifier string letter digit Множитель вида {x} равнозначен произвольно длинной последовательности х, включая пустую последовательность. Продукция вида А = АВ | 0.
Язык и синтаксис 23 теперь записывается короче: А = {В}. Множитель вида [х] равнозначен «х, или нич- то», то есть выражает необязательность. Следовательно, потребность в специаль- ном символе 0 для пустой цепочки исчезает. Идея определять языки и их грамматику с математической точностью восхо- дит к Н. Хомскому (N. Chomsky). Однако стало ясно, что предложенная простая схема правил подстановки недостаточна для представления всей сложности раз- говорных языков. Положение не изменилось даже после того, как формализм был значительно расширен. Но зато эта работа оказалась чрезвычайно плодотворной для теории языков программирования и математических формализмов. С его по- мощью Алгол 60 стал первым языком программирования, который был определен точно и формально. Мимоходом подчеркнем, что эта точность относилась только к синтаксису, но не к семантике. Термин язык программирования также обязан формализму Хомского, по- скольку языки программирования, оказывается, обладают структурой, подобной структуре разговорных языков. Но мы уверены, что этот термин, в общем, доволь- но неудачен, поскольку на языке программирования нельзя разговаривать, и по- этому он не язык в прямом смысле слова. Более подходящими были бы термины формализм или формальная нотация. Некоторые удивляются, почему точному определению предложений языка должно придаваться такое большое значение; ведь в действительности это не так. Тем не менее очень важно, чтобы предложение было правильно составлено. Хотя и в этом случае предложение может потребовать уточнения. Но, в конце концов, структура предложения (правильно составленного) важна потому, что является инструментом понимания его смысла. Благодаря синтаксической структуре от- дельные составные части предложения и их смысл могут распознаваться незави- симо, а все вместе они придают смысл целому. Давайте проиллюстрируем этот момент, используя следующий простой пример выражения со сложением. Обозначим идентификатором Е выражение, a N - число: Е = N | Е "+" Е. N = "1“ | "2" | "3" | "4". Очевидно, ”4 + 2 + Г' - правильное выражение, которое можно получить не- сколькими способами, каждому из которых соответствует своя структура, как по- казано на рис. 2.2. Обе структуры могут быть представлены скобочными выражениями, а имен- но (4 + 2) + 1 и 4 + (2 + 1) соответственно. К счастью, благодаря ассоциативности сложения результат в обоих случаях один и тот же и равен 7. Однако это не всегда так. Если в нашем примере знак сложения заменить на знак вычитания, то две структуры дадут разные результаты: (4 - 2) - 1 = 1,4 - (2 - 1) = 3. Пример иллюстрирует два факта: 1. Интерпретация предложений всегда основывается на распознавании син- таксической структуры. 2. Каждое предложение должно иметь единственную структуру, для того что- бы не быть двусмысленным.
24 Язык и синтаксис Рис 2 2 Разные структурные деревья для рдного и того же выражения Если второе требование не выполняется, может возникнуть двусмысленное прехюжение. Этим можно обогатить разговорный язык; однако двусмысленные языки программирования просто бесполезны. Мы называем синтаксический класс неоднозначным, если ему можно соотнес- ти несколько структур. Язык неоднозначен, если в него входит по крайней мере один неоднозначный синтаксический класс (конструкция). 2.1. Упражнения - unsignedNumber | variable |"(” arithmeticExpression ")” |... = primary | factor "Т” primary. = factor | term ("*" |"/" | "+') factor. 2.1. Сообщение об Алголе 60 содержит следующий синтаксис (приведенный кРБНФ): primary factor term simpleArithmeticExpression = term | ("+’ |term | simpleArithmeticExpression ("+" |term. arithmeticExpression = simpleArithmeticExpression | "IF" BooleanExpression "THEN” simpleArithmeticExpression "ELSE" arithmeticExpression. relationalOperator = "=" | V| "<" | ”<" | ">” | ">". relation = arithmeticExpression relationalOperator arithmeticExpression. BooleanPrimary = logicalValue | variable | relation I "("BooleanExpression")" |... . “ “ BooleanPrimary. = BooleanSecondary | BooleanFactor "n" BooleanSecondary. = BooleanFactor | BooleanTerm "g” BooleanFactor. = BooleanTerm | implication "=>" BooleanTerm. = implication | simpleBoolean "a" implication. BooleanSecondary = BooleanPrimary | BooleanFactor BooleanTerm implication simpleBoolean BooleanExpression = simpleBoolean | "IF" BooleanExpression "THEN" simpleBoolean "ELSE" BooleanExpression. Определите синтаксические деревья следующих выражений, в которых буквы обозначают переменные:
Упражнения 25 X + у + z X * у + z X + у * z (х - у) * (х + у) -х + у а + Ь < с + d a+b<cVd#en -Т => g>h ? i *j = к T L V m-n + p < q 2.2. Следующие продукции также являются частью первоначального опреде- ния Алгола 60. Они содержат двусмысленности, которые были устранены в усмотренном сообщении. forListElement = arithmeticExpression I arithmeticExpression "STEP" arithmeticExpression "UNTIL” arithmeticExpression I arithmeticExpression "WHILE" BooleanExpression . forList = forListElement | forListforListElement. forClause = "FOR" variable ":=" forList "DO". forStatement = forClause statement. compoundTail = statement "END" | statementcompoundTail. compoundstatement = "BEGIN" compoundTail . unconditionalStatement = basicStatement | forStatement | compoundStatementl... ifStatement = "IF" BooleanExpression "THEN" unconditionalStatement. conditionalstatement = ifStatement | ifStatement "ELSE" statement. statement = unconditionalStatement | conditionalstatement. Найдите по крайней мере две различные структуры для следующих выраже- й и операторов. Пусть А и В обозначают «простые операторы». IF a THEN b ELSE с = d IF a THEN IF b THEN A ELSE В IF a THEN FOR ... DO IF b THEN A ELSE В Предложите альтернативный однозначный синтаксис. 2.3. Рассмотрите следующие конструкции и попытайтесь разобраться, какие них являются корректными для Алгола, а какие - для Оберона (см. приложе- 5 2): a+b=c+d а * -b a<b & c<d Вычислите следующие выражения: 5 * 13 DIV 4 = 13 DIV 5 *4 =

Глава 3 Регулярные языки
28 Регулярные языки Синтаксические уравнения в том виде, как они определены в РБНФ, генерируют контекстно-свободные языки. Термин «контекстно-свободный» принадлежит Хомскому и происходит из того факта, что замена символа слева от знака = це- почкой. порожденной выражением справа от знака возможна всегда, независи- мо от контекста этого символа внутри предложения. Оказывается, что эта свобода контекста (по Хомскому) очень подходит и даже желательна для языков програм- мирования. Но контекстная зависимость в инам смысле все-таки необходима. Мы вернемся к этой теме в главе 8. Здесь мы прежде всего хотим исследовать подкласс, а не контекстно-свобод- ные языки вообще. Этот подкласс, известный как регулярные языки, играет суще- ственную роль в области языков программирования. В сущности, это контекстно- свободные языки, синтаксис которых не содержит иной рекурсии, кроме спецификации повторения. Так как в РБНФ повторение определено непосред- ственно и без использования рекурсии, можно дать простое определение: Язык регулярен, если его синтаксис может быть выражен единствен- ным выражением РБНФ. Требование достаточности единственного уравнения подразумевает, что вы- ражение состоит только из терминальных символов. Такое выражение называют регулярным выражением. Видимо, достаточно двух кратких примеров регулярных языков. Первый оп- ределяет идентификаторы, поскольку они встречаются в большинстве языков, а второй - целые числа в десятичной записи. Для краткости мы используем нетер- минальные символы letter и digit. Их можно исключить подстановкой, в результа- те чего и identifier, и integer будут регулярными выражениями. identifier = letter {letter | digit}. integer = digit {digit}. letter = -A" | "B" |... | "Z" digit = 'O’ ГГ I '2‘ | ”3" | “4" | ”5" I ”6” | ”7" | ”8" | "9". Причина нашего интереса к регулярным языкам заключается в том, что про- граммы для распознавания регулярных выражений очень просты и эффективны. Под «распознаванием» мы подразумеваем определение структуры предложения и, следовательно, его правильность, то есть принадлежность языку. Распознава- ние выражения называется синтаксическим анализом. Для распознавания регулярных выражений необходимо и достаточно иметь конечный автомат, также называемый машиной состояний {state machine). На каждом шаге машина состояний читает следующий символ и меняет свое состоя- ние. Следующее состояние определяется исключительно предыдущим состояни- ем и очередным прочитанным символом. Если очередное состояние уникально, то машина состояний является детерминированной, иначе недетерминированной. Если машина состояний реализована программно, то состояние машины опреде- ляется текущей точкой выполнения программы. Анализирующая программа может быть выведена непосредственно из опреде- ления синтаксиса в РБНФ. Для каждой РБНФ-конструкции К существует прави-
Регулярные языки 29 ло перевода, которое порождает фрагмент программы Рг(К). Правила перевода из РБНФ в текст программы показаны ниже. В них sym - глобальная переменная, представляющая собой последний символ исходного текста, прочитанный проце- дурой next. Процедура error завершает выполнение программы, сигнализируя о том, что последовательность символов не принадлежит языку. К "х” (ехр) [ехр] {ехр} facO facl ... facN termOlterml |...|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(facO); Pr(facl);... Pr(facN); CASE sym OF first(termO): Pr(termO); I first(terml): Pr(terml); I first(termN): Pr(termN); END Множество first(K) содержит все символы, с которых могут начинаться пред- ложения, полученные из конструкции К. Это множество начальных символов кон- струкции К. Для двух примеров - идентификаторов и целых чисел - они таковы: first(integer) = digits = {"О”, "1", "2", “3", "4", "5", "б", "7”, "8", "9”} first(identifier) = letters = {"А", "В"."Z"} Однако применение этих простых правил перевода, генерирующих синтакси- ческий анализатор по заданному синтаксису, возможно лишь при условии детер- минированного синтаксиса. Это предварительное условие может быть сформули- ровано более конкретно следующим образом: К Cond(K) termO | terml Символы termO и terml не должны иметь никаких общих начальных символов facO facl Если facO содержит пустую последовательность, то fact) и facl не должны иметь никаких общих начальных символов [ехр] или {ехр} Множества начальных символов ехр и символов, которые могут следовать за К, не должны пересекаться Очевидно, эти условия выполняются в примерах для идентификаторов и це- лых чисел, и мы, таким образом, получаем следующие программы для их распоз- навания: IF s IN letters THEN next ELSE error END; WHILE sym IN letters + digits DO CASE sym OF "A" .. ”Z": next | "O’’ .. "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 записываются следующим образом: СА" <- sym) & (sym <= "Z") ГО" <= sym) & (sym <= "9") Важность регулярных языков для языков программирования следует из того, что последние обычно определяются в два этапа. На первом этапе их синтаксис определяется в терминах словаря абстрактных терминальных символов. На вто- ром эти абстрактные символы определяются как цепочки конкретных терминаль- ных символов, таких как ASCII-символы. Это второе определение обычно имеет регулярный синтаксис. Разделение на два этапа обладает тем преимуществом, что определение абстрактных символов и, таким образом, языка не зависит от конк- ретного представления, то есть от некоторого конкретного набора символов, ис- пользуемого на некотором конкретном оборудовании. Такое разделение оказывает влияние и на структуру компилятора. Процесс синтаксического анализа использует процедуру получения следующего абстракт- ного символа, а эта процедура, в свою очередь, распознает конкретные символы - цепочки из одной или более литер. Эту последнюю процедуру называют лекси- ческим анализатором, а синтаксический анализ на этом втором, более низком уровне - лексическим анализом. Определение символов, выражаемых литерами, обычно задается в форме регулярного языка, и поэтому лексический анализатор - типичная машина состояний. Суммируем различия между этими двумя уровнями следующим образом: Процесс Входной Алгоритм элемент Синтаксис Лексический анализ Литера Лексический анализатор Регулярный Синтаксический анализ Символ Синтаксический анализатор Контекстно-свободный В качестве примера приведем лексический анализатор для разбора РБНФ. Его терминальные символы и их определение в терминах литер таковы: symbol = (blank) (identifier | string |'(’ | ’)” I"[” |"]” I "Г I Т I Т I ”=' I “•’) identifier = letter (letter | digit). string = (character) Из этих определений мы выведем процедуру CetSym, которая при каждом вы- зове присваивает глобальной переменной sym числовое значение, представляю- щее следующий прочитанный символ. Если символ является идентификатором (identifier) или строкой (string), то конкретная цепочка литер присваивается до- полнительной глобальной переменной id. Также отметим, что лексический анали-
Регулярные языки 31 затор обычно подразумевает следующее правило о пробелах и концах строк: про- белы и концы строк разделяют цепочки литер и другого назначения не имеют. Процедура GetSym на Обероне использует следующие объявления: CONST IdLen = 32; ident = 0; literal = 2; Iparen = 3; Ibrak = 4; Ibrace = 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 I 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) I "=": sym := eql; Texts.ReadfR, ch) I "(" : sym := Iparen; Texts.Read(R, ch) I ”)" : sym := rparen; Texts.Read(R, ch) I "[" : sym := Ibrak; Texts.Read(R, ch) I ”]" : sym := rbrak; Texts.Read(R, ch) I "{" : sym := Ibrace; Texts.Read(R, ch) I: sym := rbrace; Texts.ReadfR, ch) I "I": sym := bar; Texts.Read(R, ch) I sym := period; Texts.Read(R, ch)
32 Регулярные языки ELSE sym := other; Texts.Read(R, ch) END END CetSym 3.1. Упражнение Предложения регулярных языков могут быть распознаны конечными автомата- ми. Они обычно описываются диаграммами переходов. Каждый узел представля- ет состояние, а каждая стрелка - переход из одного состояния в другое. Стрелка помечена символом, который прочитан непосредственно перед переходом. Рас- смотрите следующие диаграммы и опишите синтаксис соответствующих языков вРБНФ. о
Анализ контекстно-свободных языков i4-'( •*.Vn,'!>;<>’iiCi''‘i "-•<.1яе-с»4/ # a^wr-v? .. < 4 j ^р* Г » >'i>*i • f xj шя 42,В^гоЖдай - '’'ti ' 'r r‘_\ -.' -4, >' ,.J ’Л& 4,4 ЙЧ^ЖШ-Мй • ,•,./,4, v'4X. -•
34 Анализ контекстно-свободных языков 4.1. Метод рекурсивного спуска Регулярные языки ограничены тем, что не могут выразить вложенные структуры. Вложенные структуры могут быть выражены только при помощи рекурсии (см. главу 2). Поэтому конечного автомата недостаточно для распознавания предложений контекстно-свободных языков. Мы все-таки попытаемся получить программу синтаксического анализатора для третьего примера из главы 2, используя метод главы 3. Там. где этот метод будет терпеть неудачу (а он должен терпеть неудачу), лежит ключ для возможного обобщения. Просто удивительно, насколько малыми оказываются необходимые для этого дополнительные усилия по программирова- нию! Конструкция А « "а" А "с" | "Ь". приводит, после полезных упрощений и использования IF вместо CASE, к следую- щему фрагменту программы: IF sym = ’a' THEN next; IF sym = A THEN next ELSE error END; IF sym = "c" THEN next ELSE error END ELSIF sym = "b" THEN next ELSE error END Здесь мы вслепую обработали нетерминальный символ А тем же способом, что и терминальные символы. Это, конечно, неприемлемо. Цель третьей строки про- граммы заключается в анализе конструкции А, а не в чтении символа А. Ведь именно в этом и состоит задача нашей программы. Поэтому простое решение на- шей проблемы - дать программе имя, то есть придать ей вид процедуры, и заме- нить в ней третью строку вызовом этой процедуры. Процедура А рекурсивна так же, как сама конструкция А в синтаксисе: PROCEDURE А; BEGIN . IF sym = "a" THEN next; ' A; IF sym = "c" THEN next ELSE error END ELSIF sym = "b" THEN next ELSE error END END A Необходимое расширение набора правил перевода крайне простое. Един- ственное дополнительное правило:
Метод рекурсивного спуска 35 Алгоритм разбора выводится для каждого нетерминального символа и оформляется в виде процедуры, получающей имя этого символа. Появление такого символа в синтаксисе переводится в вызов соот- ветствующей процедуры. Внимание: это правило справедливо независимо от того, рекурсивна процеду- ра или нет. Важно только проверить, что соблюдены условия детерминированности алго- ритма. Это подразумевает среди прочего, что в выражении вида term0 | term, символы term0 и term, не должны иметь никаких общих начальных символов. Это требование исключает левую рекурсию. Если мы рассмотрим леворекурсивную продукцию А = А"а" | "Ь". то увидим, что требование нарушено только потому, что Ь - начальный символ А (b IN first(A)), и, следовательно, множества first(A”a") и firstC'b”) пересекаются. “Ь" является для них общим элементом. Простой выход: левая рекурсия может и должна быть заменена повторением. В примере выше А = А"а"Г’Ь" заменяется на А = "Ь"{"а'}. Другой взгляд на переход от машины состояний к ее обобщению состоит в том, чтобы представить ее как множество машин состояний, которые ссылаются друг на друга и на себя. В принципе, единственным новым условием является то, что состояние вызывающей машины должно восстанавливаться по завершении рабо- ты вызванной машины состояний. Следовательно, это состояние должно сохра- няться при вызове. Так как машины состояний вложенные, то стек является са- мым подходящим для этого типом памяти. В силу чего наше расширение машины состояний называется магазинным автоматом. Теоретически стек (магазинная память) должен быть бесконечно глубоким. В этом состоит существенное отличие конечной машины состояний от бесконечного магазинного автомата. Общий принцип, который предложен здесь, состоит в следующем: рассматри- вайте распознавание сентенциальной конструкции, которая начинается с началь- ного символа основного синтаксиса, как высшую цель. Если в процессе достиже- ния этой цели, то есть во время анализа продукции, встречается нетерминальный символ, распознавание соответствующей ему конструкции рассматривается как подчиненная цель, тогда как достижение вышестоящей цели временно приоста- навливается. Эту стратегию называют целенаправленным синтаксическим анали- зом. Если мы посмотрим на структурное дерево анализируемого предложения, то увидим, что сначала мы сталкиваемся с вышестоящей по дереву целью (сим- волом), а от нее идем к нижестоящим целям (символам). Этот метод называется нисходящим разбором [9, 1]. Представленная реализация такой стратегии, осно- ванная на рекурсивных процедурах, известна также как синтаксический анализ методом рекурсивного спуска.
36 Анализ контекстно-свободных языков В заключение повторим, что решение об очередном выполняемом шаге всегда принимается на основании только одного очередного входного символа. Синтак- сический анализатор смотрит вперед только на один символ. Предпросмотр нескольких символов значительно усложнил бы процесс принятия решения и, та- ким образом, замедлил бы его. По этой причине мы ограничимся языками, кото- рые можно анализировать с предпросмотром только одного символа. В качестве примера демонстрации метода рекурсивного спуска рассмотрим анализатор для РБНФ, синтаксис которого приведем здесь еще раз: syntax - {production} . production - identifier "=" expression expression - term {T term}. term - factor {factor}. factor - identifier | string |'(" expression I T expression "]" |"{" expression "}". Применяя данные правила перевода и последующее упрощение, получим сле- дующий анализатор. Он оформлен в виде модуля Оберона: MODULE EBNF; IMPORT Viewers, Texts, TextFrames, Oberon; CONST IdLen - 32; ident - 0; literal - 2; Iparen = 3; Ibrak » 4; Ibrace » 5; bar - 6; eql ~ 7; rparen = 8; rbrak - 9; rbrace - 10; period - 11 ;other - 12; ТУРЕ Identifier - ARRAY IdLen OF CHAR; VAR ch: CHAR; sym: INTEGER; lastpos: LONGINT; id: Identifier; R: Texts.Reader; W; Texts.Writer; PROCEDURE error(n: INTEGER); VAR pos: LONGINT; BEGIN pos :- Texts.Pos(R); IF pos > lastpos+4 THEN (‘избегаем ложных сообщений об ошибках*) Texts.WriteStringfW, " поз"); Texts.Writelnt(W, pos, 6); Texts.WriteString(W," ош'); Texts.Writelnt(W, n, 4); lastpos :- pos; Texts.WriteString(W," симв "); Texts.WritelntfW, 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 recordfTO, id, 1); GetSym ELSIF sym = litera) THEN record(TI, id, 0); GetSym ELSIF sym = Iparen THEN GetSym; expression; IF sym = rparen THEN GetSym ELSE error(2) END ELSIF sym = Ibrak THEN GetSym; expression; IF sym = rbrak THEN GetSym ELSE error(3) END ELSIF sym = Ibrace THEN GetSym; expression; IF sym = rbrace THEN GetSym ELSE error(4) END ELSE error(S) END END factor; BEGIN (*term*) factor; WHILE sym < bar DO factor END END term; BEGIN (‘expression*) term; WHILE 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 WHILE sym = ident DO prodnction END END syntax; PROCEDURE Compile*; 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 = POINTER ТО SymDesc; SymDesc = RECORD alt, next: Symbol END Теперь сформулируем этот абстрактный тип данных для терминальных и не- терминальных символов, используя механизм расширения типов Оберона [14]. Записи, обозначающие терминальные символы, определяют их посредством до- полнительного атрибута sym: Terminal = POINTER ТО TSDesc; TSDesc = RECORD (SymDesc) sym: INTEGER END Элементы, представляющие нетерминальные символы, содержат ссылку (ука- затель) на структуру данных, представляющую этот символ. Из практических со- ображений введем косвенную ссылку, когда указатель ссылается на дополнитель- ный элемент-заголовок, который, в свою очередь, ссылается на структуру данных. Заголовок содержит имя структуры, то есть имя нетерминального символа. Стро- го говоря, в этом дополнительном элементе нет необходимости; его полезность станет понятной позже. Nonterminal = POINTER ТО NTSDesc; NTSDesc = RECORD (SymDesc) this: Header END Header = POINTER TO HDesc; HDesc = RECORD sym: Symbol; name: ARRAY n OF CHAR END Для примера возьмем следующий синтаксис простых выражений. На рис. 4.1 изображена соответствующая структура данных в виде графа. Горизонтальные стрелки - next-указатели, вертикальные стрелки - alt-указатели.
Таблично-управляемый нисходящий синтаксический анализ 39 expression = term {(”+" |term}. term = factor {("*"17”) 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 I f, I ... I fn где все символы fk, кроме последнего, начинаются с разных терминальных символов. Только последний символ f„ может начинаться как терминаль- ным. так и нетерминальным символом. При таком условии можно переби- рать альтернативы, выполняя на каждом шаге только одно сравнение. 2. Структура данных может быть получена из синтаксиса (на РБНФ) автома- тически, то есть программой, которая компилирует синтаксис. 3. В приведенной выше процедуре имя каждого распознанного нетерминаль- ного символа является ее выходом. Именно этой цели служит элемент-за- головок. 4. Empty - это специальный терминальный символ и элемент, представ- ляющий пустую цепочку. Он служит для пометки выхода из повторений (циклов). 4.3. Восходящий синтаксический анализ И рекурсивный спуск, и таблично-управляемый разбор, представленные здесь, - это методы, основанные на принципах нисходящего синтаксического анализа. Их основная цель - показать, что анализируемый текст выводится из начального символа. Любые нетерминальные символы, встречающиеся при анализе, счита- ются подцелями. Процесс синтаксического анализа создает синтаксическое дере- во с начальным символом в корне, то есть в направлении сверху вниз. Однако согласно принципу дополнения можно пойти и в восходящем направ- лении. когда текст читается без преследования конкретной цели. После каждого шага происходит проверка, соответствует ли прочитанная подцепочка некоторой сентенциальной конструкции, то есть правой части продукции. Если это так, про- читанная подцепочка заменяется соответствующим нетерминальным символом. Процесс распознавания снова состоит из последовательности шагов двух различ- ных типов: 1) перенос входного символа в стек (шаг переноса); 2) свертывание цепочки символов в стеке в один нетерминальный символ со- гласно продукции (шаг свертки). Восходящий синтаксический анализ называют также разбором типа «перенос- свертка». Синтаксические конструкции сначала накапливаются в стеке, а затем сворачиваются; синтаксическое дерево растет от основания к вершине [8,1,7]. И опять мы продемонстрируем этот процесс на примере простых выражений. Пусть их краткий синтаксис имеет следующий вид: Е « Т | Е "+" Т. expression Т -= F | Т F. term F - id ГС Е ")". factor
Восходящий синтаксический анализ 41 и пусть распознается предложение х * (у + z). Для иллюстрации процесса разбора нераспознанную часть текста будем показывать справа, тогда как слева будет из- начально пустая цепочка распознанных конструкций. Еще левее символами S (пе- ренос) и R (свертка) обозначен тип применяемого шага: х * (у + z) S X * (У + Z) R F * (у + Z) R Т * (у + Z) S т* (У + Z) S Т*( У + Z) S Т*(у + Z) R T*(F + Z) R Т*(Т + Z) R Т*(Е + Z) S Т*(Е+ Z) S Т*(Е + z ) R Т*(Е + F ) R Т*(Е + Т ) R Т*(Е ) S Т*(Е) R T*F R Т R Е В итоге исходный текст сворачивается в начальный символ Е, который здесь лучше назвать конечным. Как упомянуто ранее, вспомогательный левый накопи- тель является стеком. Для сравнения приведем процесс нисходящего разбора того же предложения. Два типа шагов обозначены М (сопоставление) и Р (порождение, развертка). На- чальный символ - Е. Е x * (y + z) р Т x * (y + Z) р Т* F X * (y + z) р F*F X * (y + z) р id * F X * (y + z) м * F * (У + z) М F (y + Z) Р (E) (y+Z) М E) y + z) Р E + T) y + Z) Р T + T) y + z) Р F + T) y + z) Р id +T) y + z) М + T) + Z) М T) Z) Р F) z) Р id) Z) М ) ) М
42 Анализ контекстно-свободных языков Очевидно, при восходящем методе читаемая цепочка символов всегда свора- чивается с ее правой стороны, тогда как при нисходящем методе разворачивается всегда самый левый нетерминал. Согласно Д. Кнуту, восходящий метод по этой причине называют LR-анализом, а нисходящий - LL-анализом. Первая буква L значит, что текст читается слева направо. Обычно к этим обозначениям добавля- ют параметр k (LL(k), LR(k)). Он задает длину предпросмотра. Мы будем всегда считать к - 1, если не оговаривается иное. Но давайте все-таки вернемся к восходящему принципу. Здесь существует се- рьезная проблема определения, какой тип шага должен быть следующим, а в слу- чае свертки - сколько символов в стеке должно быть захвачено этим шагом. На этот вопрос дать ответ непросто. Скажем только: чтобы обеспечить эффектив- ность процесса разбора, информация, на основании которой принимаются реше- ния, должна быть заранее собрана и представлена в надлежащем виде. Восходя- щие синтаксические анализаторы всегда используют таблицы, то есть данные, структурно аналогичные данным таблично-управляемого нисходящего синтак- сического анализатора, представленного выше. Но, помимо синтаксиса в виде структуры данных, им нужны дополнительные таблицы для эффективного опре- деления следующего шага разбора. По этой причине восходящий синтаксический анализ в целом более запутанный и сложный, чем нисходящий. Существуют различные алгоритмы LR-анализа. Они налагают различные ог- раничения на обрабатываемый синтаксис. Чем слабее эти ограничения, тем слож- нее процесс синтаксического анализа. Упомянем здесь, не вдаваясь в их подроб- ности, методы SLR [2] и LALR [10]. 4.4. Упражнения 4.1. Алгол 60 содержит многократные присваивания вида vl := v2 := ... vn := е, которые определяются следующим синтаксисом: assignment = leftpartlist expression. leftpartlist = leftpart | leftpartlist leftpart. leftpart = variable expression = variable | expression ”+” variable. variable = idem I ident"(" expression "]”. Какой длины должен быть предпросмотр для выполнения грамматического разбора этого синтаксиса с помощью нисходящего принципа? Предложите аль- тернативный синтаксис для многократного присваивания, требующий предпрос- мотра только одного символа. 4.2. Определите множества символов FIRST и FOLLOW для конструкций РБНФ production, expression, term и factor. Используя эти множества, проверь- те, что РБНФ является детерминированной. syntax = {production}. production = id "=" expression expression = term {”!' term}.
Упражнения 43 term = factor {factor}. factor = id string |"(" expression ")""[" expression IT expression ''}". id = lette r {letter | digit}. string = {character} 4.3. Напишите синтаксический анализатор для РБНФ и расширьте его коман- дами генерации структуры данных (для таблично-управляемого синтаксического анализа), соответствующей читаемому синтаксису.

Глава 5 Атрибутные грамматики и семантики
46 Атрибутные грамматики и семантики В атрибутных грамматиках с отдельными конструкциями, то есть с нетерминаль- ными символами, связываются определенные атрибуты. Символы параметризу- ются, и представляют целые классы вариантов. Это служит упрощению синтак- сиса, а на практике способствует превращению синтаксического анализатора в реальный транслятор [13]. Процесс трансляции характеризуется сопоставлени- ем вывода (возможно, пустого) каждой распознанной сентенциальной конструк- ции. Каждое синтаксическое уравнение (продукция) сопровождается дополни- тельными правилами, определяющими отношение между значениями атрибутов символов правой части, нетерминала левой части и правила вывода. Предлагаем три применения атрибутов и атрибутных правил. 5.1. Правила типов В качестве простого примера рассмотрим язык, имеющий несколько типов дан- ных. Вместо определения отдельных синтаксических правил для выражений каж- дого типа (как было сделано в Алголе 60) мы определим выражение только один раз и свяжем с каждой входящей в него конструкцией атрибут типа данных Т. Например, если выражение типа Т обозначить ехр(Т), то есть ехр со значением атрибута Т, то правила совместимости типов можно рассматривать как дополне- ния к отдельным синтаксическим уравнениям. Требование о том, что оба операн- да и результат сложения и вычитания должны быть одного типа, может быть зада- но такими дополнительными атрибутными правилами: Синтаксис Атрибутное правило Контекстное условие ехр(Т0) = term(Tl) ТО := Т1 I ехр(Т1) Ч" term(T2) ТО := Т1 Т1 = Т2 I ехр(Т1) term(T2). ТО := Т1 Т1 = Т2 Если же в смешанных выражениях допускаются операнды типов INTEGER и REAL, правила смягчаются, но при этом становятся более сложными: ТО := if (Т1 = INTEGER) & (Т2 = INTEGER) then INTEGER else REAL, T1 = INTEGER or T1 = REAL T2 = INTEGER or T2 = REAL На самом деле правила совместимости типов тоже статические в том смысле, что они могут быть проверены без выполнения программы. Поэтому их отделение от чисто синтаксических правил оказывается совершенно необоснованным, зато их интеграция в синтаксис в виде атрибутных правил вполне оправдана. Однако заметим, что атрибутные грамматики получают новый аспект, если возможные значения атрибута (здесь - тип данных) и их количество заранее неизвестны. Если синтаксическое уравнение содержит повторение, то было бы неплохо выразить его в атрибутных правилах с помощью рекурсии. А при наличии вариан- тов каждый из них лучше выразить отдельно. Это показано в следующем примере, где два правила для выражений
Правила вычислений 47 ехр(ТО) = term(Tl) {"+" term(T2)}. ехр(Т0) = termCTI). разбиты на пары правил, а именно ехрСГО) = term(Tl) | ехр(ТО)= termCTI) | ехр(Т1) ”+" term(T2). term(Tl). Связанные с продукцией правила типов срабатывают всякий раз, когда соот- ветствующая продукции конструкция распознана. В случае синтаксического ана- лиза методом рекурсивного спуска эта связь реализуется очень просто: операто- ры, реализующие атрибутные правила, просто вставляются между операторами программы синтаксического анализа, а атрибуты передаются в качестве парамет- ров в процедуры синтаксических конструкций (нетерминальных символов) ана- лизатора. Первым примером демонстрации этого процесса может быть процедура распознавания выражений, которая берется за основу: PROCEDURE expression; BEGIN term; WHILE (sym = OR (sym = DO GetSym; term END END expression и расширяется включением в нее атрибутных правил типа; PROCEDURE expression(VAR typO: Type); VAR typl ,typ2: Type; BEGIN term(typl); WHILE (sym = "+") OR (sym = "-”) DO GetSym; term(typ2); typl := ResType(typl, typ2) END; typO := typl END expression 5.2. Правила вычислений В качестве второго примера мы рассмотрим язык, состоящий из выражений, в ко- торых операнды представлены числами. Это кратчайший путь к расширению син- таксического анализатора до программы, которая не только распознает выраже- ния, но одновременно вычисляет их. С каждой конструкцией свяжем ее значение в виде атрибута val. По аналогии с правилами совместимости типов из предыду- щего раздела теперь мы должны реализовать обработку правил вычисления в процессе разбора. Таким образом, мы неявно уже ввели нотацию для семантик. Синтаксис Атрибутное правило (семантика) exp(vO) = term(vl)| vO :- vl exp(vl)'+' term(v2) | vO :- vl + v2 exp(vl)term(v2). vO :- vl - v2
48 Атрибутные грамматики и семантики term(vO) - factorfvl) | term(vl) '*’ factor(v2)| term(vl) 'Г factor(v2). factor(vO) - number(vl)| T exp(vl) ”)". vO :» vl vO := vl * v2 vO := vl / v2 vO := vl vO := vl Здесь атрибут - это вычисленное числовое значение распознанной конструк- ции. Необходимое расширение соответствующей процедуры синтаксического анализа приводит к следующей процедуре для выражений: PROCEDURE expression(VAR valO: INTEGER); VAR vail, val2: INTEGER; op: CHAR, BEGIN term(vall); WHILE (sym = '+") OR (sym = DO op : = sym; GetSym; term(val2); IF op - '+' THEN vail : = vail + val2 ELSE vail := vail - val2 END END; valO:=vall END expression 5.3. Правила трансляции Третий пример применения атрибутных грамматик иллюстрирует основную функцию компилятора. Здесь дополнительные, связанные с продукцией правила уже не управляют атрибутами символов, а определяют выход (код), выдаваемый продукцией в процессе разбора. Генерация выхода может рассматриваться как побочный эффект синтаксического разбора. Как правило, выход - это последова- тельностью команд. В этом примере команды заменены абстрактными символа- ми, а их вывод осуществляется оператором put. Синтаксис Выходное правило (семантика) exp = term - 1 exp "+" term put("+") 1 expterm. put(”-") term = factor - | term ”*” factor put("*“) I term'/" factor. put(7") factor = number put(number) 1"(“ exp(vl) ")”• - Легко убедиться, что последовательность символов на выходе есть постфикс- ная запись анализируемого выражения. Синтаксический анализатор теперь рас- ширен до транслятора. Инфиксная запись Постфиксная запись 2 + 3 2 3 + 2*3 + 4 2 3*4 + 2 + 3*4 2 3 4 * + (5-4) * (3+2) 5 4 - 3 2 + *
Упражнение 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. Синтаксис Правила типов Семантики Программа I 1 “ — - ..J (Результат -----И Обобщенный компилятор I------------------► Рис. 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],[l ..n] OF BOOLEAN; PROCEDURE ancestor(VAR m: matrix; n: INTEGER); (* Первоначально m[ij] устанавливается в TRUE, если i - родитель). По завершению m[ij] будет TRUE, если i - предок j *) 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 Можно допустить, что количество терминальных и нетерминальных символе» анализируемых языков не превышает заданный предел, например, 32.
Глава 6 Язык программирования Оберон-О
52 Язык программирования Оберон-О Чтобы избежать потерь в общности и абстрактных теориях, мы построим вполне конкретный компилятор и объясним различные проблемы, которые возникают при его проектировании. Для этого мы должны предложить определенный исход- ный язык. Чтобы остаться в рамках вводного учебного курса, мы, с одной стороны, долж- ны придерживаться достаточной простоты как компилятора, так и самого языка. Но. с другой - мы хотим охватить как можно больше фундаментальных конст- рукций языков и способов компиляции. Из этих соображений вытекает условие выбора языка: он должен быть простым, но достаточно выразительным. Мы вы- брали подмножество языка Оберон [14], который вобрал в себя основные черты своих предков Модула-2 [18] и Паскаль [15]. Можно сказать, что Оберон - это последний отпрыск в традициях Алгола 60 [12]. Наше подмножество называется Оберон-О. и этот выбор достаточно обоснован для освоения основ теории и прак- тики современных методов программирования. Оберон-О довольно хорошо проработан в отношении программной структуры. Элементарный оператор - это присваивание. Составные операторы охватывают понятия последовательности операторов, условного и повторного выполнения, представленных в виде традиционных операторов IF и WHILE. Оберон-О также содержит важное понятие подпрограммы, представленное объявлением процеду- ры и вызовом процедуры. Его мощь опирается в основном на возможности пара- метризованных процедур. Мы различаем в Обероне параметры-значения и пара- метры-переменные. Однако в отношении типов данных Оберон-О довольно скромен. Единствен- ные элементарные типы данных - целые числа и логические значения, обозначае- мые INTEGER и BOOLEAN. Таким образом, можно объявлять целочисленные константы и переменные, а также строить выражения с арифметическими опера- циями. Сравнения выражений дают логические значения, которые могут быть подвергнуты логическим операциям. Доступные структуры данных - массив и запись. Они могут быть произвольно вложенными. Однако указатели исключены. Процедуры представляют функциональные блоки операторов. Поэтому нуж- но увязать понятие локализации имен с описанием процедуры. Оберон-О предос- тавляет возможность объявлять идентификаторы в процедуре так, что они дос- тупны (видимы) только в пределах процедуры. Этот очень краткий обзор Оберона-О должен прежде всего предоставить чита- телю сведения, необходимые для понимания следующего синтаксиса, определен- ного в терминах РБНФ. ident = letter {letter | digit}, integer = digit {digit}. selector = {"." ident | "[" expression "]”}. number = integer. factor = ident selector | number | "(" expression ")" |factor. term = factor {("*" | "DIV" I "MOD" I "&") factor}.
Язык программирования Оберон-О 53 SimpleExpression expression assignment Actualparameters ProcedureCall IfStatement WhileStatement statement Statementsequence IdentList ArrayType FieldList RecordType type FPSection FormalParameters ProcedureHeading ProcedureBody ProcedureDeclaration declarations module = [',+"Г'-"] term {("+"|"-" | "OR") term). = SimpleExpression [("=" | "#" | "<" | "<=" | ">" | ">=") SimpleExpression], = 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"] IdentListtype. = "(" [FPSection FPSection}]")". = "PROCEDURE” ident [FormalParameters]. = declarations ["BEGIN" Statementsequence] "END" ident. = ProcedureHeading ProcedureBody. = ["CONST" {ident "=” expression ";”}] ["TYPE” {ident "=” type ";"}] ["VAR" {IdentListtype ";”}] {ProcedureDeclaration ";"}. = "MODULE" ident ”;" declarations ["BEGIN” Statementsequence] "END” ident".". Следующий пример модуля поможет читателю почувствовать характер языка. Модуль содержит различные, хорошо известные учебные процедуры, названия которых очевидны. MODULE Sample; PROCEDURE Multiply; VAR x, у, z: INTEGER; BEGIN Read(x); Read(y); z := 0; WHILE x > 0 DO IF x MOD 2 = 1 THEN z := z + у END ; у := 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 Язык программирования Оберон-О WHILE w <= г DO w ;= 2*w END ; WHILE w > у 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); WriteQ); Write(a[j]); WriteLn END BinSearch; END Sample. 6.1. Упражнение 6.1. Для компьютера, описанного в главе 9, определите машинный код, генерируе- мый программой, приведенной в конце этой же главы.
Глава' Синтаксический анализатор для Оберона-О анадагшсл-. '.‘58 •<-' ЙимтаЧси*<еский ’ ' < вчш^Латор ........ . ‘Э? ,! '7'&.'Устранен^!1 ;i' n vjuijrjrtK....5«И ' ••> ..............’Е>4; '
56 Синтаксический анализатор для Оберона-О 7.1. Лексический анализатор Прежде чем начать разработку синтаксического анализатора, мы остановимся на проектировании его лексического анализатора. Лексический анализатор должен распознавать терминальные символы в исходном тексте. Сначала приведем его словарь: * DIV MOD & + OR = # < <= > >= : ) OF THEN DO ( [ ~ = END ELSE ELSIF IF WHILE ARRAY RECORD CONST TYPE 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; Iss = 11; geq = 12; leq = 1 3; gtr = 14; period = 18; comma = 19; colon = 20; rparen = 22; rbrak = 23; of = 25; then = 26; do = 27; Iparen = 29; Ibrak = 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 InitCT: Texts.Text; pos: LONGINT); END OSS.
Синтаксический анализатор 57 Символы отображаются в целые числа. Отображение задается множеством определений констант. Процедура Mark служит для вывода диагностики ошибок, обнаруженных в исходном тексте. Обычно в журнал ошибок вместе с позицией обнаруженной ошибки заносится краткое пояснение к ней. Процедура Get пред- ставляет собственно лексический анализатор. При каждом ее вызове она выдает следующий распознанный символ. Процедура выполняет следующие действия (полный ее листинг приведен в приложении С). 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) • I* (~ integer ident ( - integer ident + - ( - integer ident + - (~ integer ident ident ident ident IF WHILE * ident IF WHILE * ident * ident ARRAY RECORD ident VAR
58 Синтаксический анализатор для Оберона-О FormalParameters ProcedureHeading ProcedureBody ProcedureDeclaration declarations module S selector factor term SimpleExpression expression assignment ProcedureCall statement Statementsequence FieldList type FPSection FormalParameters ProcedureHeading ProcedureBody ProcedureDeclaration declarations ( PROCEDURE END CONST TYPE VAR PROCEDURE BEGIN PROCEDURE CONST TYPE VAR PROCEDURE * MODULE 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 Дальнейшая проверка правил на детерминированность показывает, что син- таксис Оберона-О действительно может быть обработан методом рекурсивного спуска с предпросмотром одного символа. Каждая процедура соответствует каж- дому нетерминальному символу. Прежде чем писать процедуры, полезно посмот- реть, как они зависят друг от друга. Для этой цели нарисуем граф зависимостей (рис. 7.1). Каждая процедура в нем представлена узлом, а дуги от него проводятся ко всем узлам, от которых эта процедура зависит, то есть вызывает их прямо или косвенно. Обратите внимание, что некоторых нетерминальных символов не оказалось в этом графе, так как они тривиальным образом включены в другие символы. Например, АггауТуре и RecordType содержатся только в type, поэтому явно не показаны. Кроме этого, напомним, что символы ident и integer являются терминальными символами, потому что так они определяются лексическим анализатором. Каждая петля в диаграмме соответствует рекурсии. Отсюда ясно, что анализа- тор должен записываться на языке, который допускает рекурсивные процедуры. Кроме того, диаграмма показывает, каким образом процедуры могут быть вложе- ны. Module - единственная процедура, которую не вызывает ни одна другая. Ди- аграмма отражает структуру программы. Полный текст программы приводится
Устранение синтаксических ошибок 59 Рис. 7.1. Диаграмма зависимости процедур синтаксического анализатора в приложении С. Синтаксический анализатор, подобно лексическому анализато- ру, также оформлен в виде модуля. 7.3. Устранение синтаксических ошибок До сих пор рассматривалась довольно простая задача проверки соответствия ис- ходного текста лежащему в его основе синтаксису. В качестве побочного эффекта синтаксический анализатор распознавал также структуру прочитанного текста. Но как только появлялся недопустимый символ, задача анализатора считалась выполненной, и процесс синтаксического анализа завершался. Однако для прак- тических применений такое положение дел недопустимо. Настоящий компиля- тор должен в этом случае выдать диагностическое сообщение об ошибке и затем продолжить анализ. Тогда, вполне возможно, будут обнаружены и последующие ошибки. Однако продолжение анализа после обнаружения ошибки возможно только при допущении определенных гипотез относительно природы ошибки. В зависимости от этих допущений часть последующего текста должна быть про- пущена или же в нее должны быть вставлены определенные символы. Такие меры необходимы, даже когда уже нет надежды на исправление или выполнение оши- бочной исходной программы. Без верной, хотя бы отчасти, гипотезы продолжать процесс разбора бесполезно [4, 13]. Методы выбора хороших гипотез сложны. Они в конечном счете опираются на эвристики, поскольку проблема все еще не поддается формализации. Основная причина заключается в том, что формальный синтаксис игнорирует факторы, ко-
60 Синтаксический анализатор для Оберона-О торые существенны для восприятия предложения человеком. Например, пропуск знака пунктуации - частая ошибка, причем не только в текстах программ, а вот знак операции в арифметическом выражении пропускается довольно редко. И если для синтаксического анализатора оба этих символа - равнозначные син- таксические единицы, то для программиста точка с запятой кажется почти не- нужной, а знак плюс - суть выражения. Это различие нужно иметь в виду, если ошибки должны обрабатываться осмысленно. Подводя итог, мы утверждаем сле- дующие качественные критерии обработки ошибок. 1. За один просмотр текста должно быть выявлено как можно больше ошибок. 2. Должно быть сделано как можно меньше дополнительных предположений о языке. 3. Средства обработки ошибок не должны существенно замедлять работу ана- лизатора. 4. Программа анализатора не должна значительно увеличиваться в размере. Можно сделать вывод, что обработка ошибок сильно зависит от конкретной ситуации и описывается общими правилами только с ограниченным успехом. Тем не менее есть несколько эвристических правил, которые, кажется, должны быть полезны и за рамками нашего конкретного языка Оберон. В основном они касаются влияния проекта языка на методы обработки ошибок. Несомненно, про- стая структура языка значительно упрощает диагностику ошибок, или, иначе, сложный синтаксис безусловно усложняет обработку ошибок. Будем различать два варианта неправильного текста. Первый - когда предпо- лагаемый символ отсутствует. Его обработать относительно легко. Синтаксичес- кий анализатор, выясняя ситуацию, продолжает обращаться к лексическому ана- лизатору один или более раз. Примером может служить оператор, где в конце выражения ожидается закрывающая круглая скобка. Если она отсутствует, син- таксический анализатор возобновляет свою работу после выдачи сообщения об ошибке: IF sym = rparen THEN Get(sym) ELSE Mark(') отсутствует') END Фактически не прерывают процесс разбора только пропуски слабых символов, символов, которые имеют исключительно синтаксическую природу, таких как за- пятая, точка с запятой и завершающие символы. Случай неправильного использо- вания знака равенства вместо оператора присваивания обрабатывается так же легко. Второй вариант - когда появляется недопустимый символ. Тут не остается ничего иного, как пропустить его и продолжить разбор со следующей позиции текста. Для облегчения продолжения разбора в Обероне предусмотрены опреде- ленные конструкции, начинающиеся с характерных символов, которые в силу своей сути редко используются неправильно. Например, последовательность объявлений всегда начинается с символов CONST, TYPE, VAR или PROCEDURE, а структурные операторы всегда начинаются с IF, WHILE, REPEAT, CASE и т. д. Поэто- му такие сильные символы никогда не пропускаются. Они служат в тексте точка-
Устранение синтаксических ошибок 61 ми синхронизации, в которых процесс разбора может быть продолжен с высокой вероятностью успеха. В синтаксисе Оберона мы зафиксируем четыре точки синх- ронизации, а именно factor, StatSequence, declarations и Туре. В начале соответ- ствующей им процедуры разбора выполняется пропуск символов. А когда прочи- тан либо правильный начальный символ, либо сильный символ, процесс разбора возобновляется. PROCEDURE factor; BEGIN (‘синхронизация*) IF sym < Iparen THEN Магк("идентификатор?"); REPEAT Get(sym) UNTIL sym >= Iparen END; END factor; PROCEDURE StatSequence; BEGIN (‘синхронизация*) IF sym < ident THEN МагкС'оператор?"); REPEAT Get(sym) UNTIL sym >= ident END; END StatSequence; PROCEDURE Type; ВЕС1Н(*синхронизация*) IF (sym#ident) & (sym>=const) THEN Магк("тип?"); REPEAT Get(sym) UNTIL (sym=ident) OR (sym>=array) END; END Type; PROCEDURE declarations; ВЕС1Н(*синхронизация‘) IF symcconst THEN МагкС'объявление?"); REPEAT Get(sym) UNTIL sym>=const END; END declarations; Очевидно, что здесь предполагается определенный порядок символов. Этот порядок был выбран таким образом, чтобы определенной группировкой символов обеспечить простые и эффективные проверки их диапазонов. Как видно из описа- ния интерфейса лексического анализатора, сильным символам, которые нельзя пропускать, назначен высокий приоритет (порядковый номер). В общем, правило гласит, что программа-анализатор порождается синтакси- сом согласно методу рекурсивного спуска и правилам перевода. Если прочитан- ный символ не отвечает ожиданиям, то посредством вызова процедуры Mark сооб- щается об ошибке и анализ продолжается со следующей точки синхронизации. Зачастую обнаруживаются наведенные ошибки, о которых можно не сообщать, потому что они являются просто следствием выявленных ранее ошибок. Дей-
62 Синтаксический анализатор для Оберона-О ствия, которые должны выполняться в каждой точке синхронизации, в общем мо- гут быть описаны следующим образом: IF ~(sym IN follow(SYNC)) THEN Mark(msg); REPEAT Cet(sym) UNTIL sym IN follow(SYNC) END где follow(SYNC) обозначает множество символов, которые можно ожидать в этой точке. В определенных случаях бывает выгодно отступить от утверждений, вытекаю- щих из метода. Примером может служить конструкция StatSequence (последова- тельность операторов). Вместо Statement; WHILE sym = semicolon DO Cet(sym); Statement END запишем LOOP (‘синхронизация*) IF sym < ident THEN Магк("идентификатор?"); ... END; Statement; IF sym = semicolon THEN Cet(sym) ELSIF sym IN followfStatSequence) THEN EXIT ELSE МагкСточка с запятой?”) END END Это заменяет два вызова Statement одним вызовом, который здесь может быть заменен телом процедуры, что избавляет от необходимости объявлять явную про- цедуру. Две проверки после Statement соответствуют двум допустимым вариан- там. когда после прочтения точки с запятой либо анализируется следующий опе- ратор, либо последовательность операторов заканчивается. Вместо условия sym IN follow (StatSequence) мы используем логическое выражение, которое снова по- зволяет воспользоваться специально выбранным порядком символов: (sym >= semicolon) & (sym < if) OR (sym >= array) Приведенная конструкция - пример общего случая, когда последователь- ность, возможно, пустая, однотипных элементов (здесь - операторов) разделяет- ся слабым символом (здесь - точкой с запятой). Второй подобный пример имеет место в списке параметров вызова процедуры. Действие IF sym = Iparen THEN Cet(sym); expression; WHILE sym = comma DO Cet(sym); expression END; IF sym = rparen THEN Cet(sym) ELSE Markf')?”) END END заменяется на IF sym = Iparen THEN Cet(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 Cet(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 МагкС'неверная последовательность объявлений") ELSE EXIT END END Причина отступления от заданного метода состоит в том, что неправильная последовательность объявлений (скажем, переменных перед константами) долж- на вызывать сообщение об ошибке, в то время как каждое отдельное объявление может быть разобрано без проблем. Еще один похожий пример можно найти в объявлении типов Туре. Во всех таких случаях абсолютно необходимо гаранти- ровать, чтобы синтаксический анализатор не мог быть втянут в бесконечный цикл. Простейший способ добиться этого - убедиться в том, что при каждом по- вторе читается по крайней мере один символ, то есть каждый путь содержит по меньшей мере один вызов Get. Таким образом, даже в худшем случае синтаксиче- ский анализатор достигает конца исходного текста программы и останавливается. За дальнейшими деталями можно обратиться к листингу в приложении С. Теперь должно стать ясно, что не существует идеальной стратегии обработки ошибок, которая с большой эффективностью транслировала бы все корректные предложения и в то же время точно диагностировала все ошибки в неправильных исходных текстах. Любая стратегия обработает некоторые непонятные предложе- ния таким способом, который окажется неожиданным для ее авторов. Существен- ными характеристиками хорошего компилятора, не вдаваясь в детали, являются: (1) ни одна из последовательностей символов не приводит к нарушению работы компилятора и (2) часто встречающиеся ошибки диагностируются корректно и, следовательно, не генерируют вообще (или совсем немного) ложных сообщений. Представленная здесь стратегия работает удовлетворительно, хотя может быть усовершенствована. Она интересна тем, что обработчик ошибок получается не- посредственно из синтаксического анализатора применением нескольких про-
64 Синтаксический анализатор для Оберона-О стых правил. Эти правила пополняются благодаря разумному выбору небольшо- го числа параметров, которые определяются на основе достаточного опыта ис- пользования языка. 7.4. Упражнения 7.1. Для того чтобы определить, является ли последовательность букв ключевым словом, приведенный в приложении С лексический анализатор компилятора ис- пользует линейный поиск по массиву Key Tab. Так как этот поиск выполняется очень часто, то усовершенствованный метод поиска мог бы, несомненно, привести к повышению производительности. Замените линейный поиск в массиве на: 1) двоичный поиск в упорядоченном массиве; 2) поиск в двоичном дереве; 3) поиск в хэш-таблице. Выберите хэш-функцию так, чтобы потребовалось не более двух сравнений для определения, является ли последовательность букв ключевым словом. Определите общий прирост производительности компилятора для каждого из трех решений. 7.2. Где синтаксис Оберона нарушает ограничения LL(1), то есть где необхо- дим предпросмотр более чем одного символа? Измените синтаксис так, чтобы он удовлетворял свойству LL(1). 7.3. Расширьте лексический анализатор таким образом, чтобы он допускал вещественные числа, как они определяются синтаксисом Оберона (см. прило- жение А.2).
Глава 8 Учет контекста, заданного объявлениями
66 Учет контекста, заданного объявлениями 8.1. Объявления Хотя языки программирования основаны на контекстно-свободных грамматиках в смысле Хомского, они не являются контекстно-свободными в обычном понима- нии этого слова. Контекстная зависимость проявляется в том, что каждый иден- тификатор в программе должен быть объявлен. Таким образом, он связывается с объектом вычислительного процесса, который имеет определенные неизменные свойства. Например, идентификатор связывается с переменной, и эта переменная имеет конкретный тип данных, указанный в объявлении идентификатора. Если появляющийся в операторе идентификатор ссылается на заданный его объявле- нием объект, а само объявление идентификатора находится за пределами этого оператора, то мы скажем, что объявление находится в контексте оператора. Очевидно, что учет контекста находится за пределами возможностей контек- стно-свободного анализа, но, несмотря на это, легко обрабатывается. Контекст представляется в виде структуры данных, которая содержит запись для каждого из объявленных идентификаторов. Эта запись (или вход) связывает идентифика- тор с соответствующим объектом и его свойствами. Такая структура данных на- зывается таблицей символов. Данный термин восходит к временам ассемблеров, когда идентификаторы назывались символами. Хотя структура таблицы симво- лов обычно сложнее, чем простой массив. Синтаксический анализатор будет расширен теперь таким образом, что при разборе объявления таблица символов будет пополняться. Для каждого объяв- ленного идентификатора добавляется запись. Итак: • каждое объявление влечет добавление новой записи в таблицу символов; • каждое появление идентификатора в операторе влечет поиск по таблице символов для определения атрибутов (свойств) объекта, обозначенного этим идентификатором. Типичный атрибут - класс объекта. Он указывает, будет ли идентификатор обозначением переменной, константы, типа или процедуры. Еще одним атрибу- том во всех языках программирования с типизацией данных служит тип объекта. Простейшей формой структуры данных для представления множества эле- ментов является список. Главный его недостаток заключается в относительно медленном поиске, потому что он должен быть просмотрен от начала до искомого элемента. Поскольку структуры данных не являются предметом нашего исследо- вания, то ради простоты мы объявим следующие типы, представляющие линей- ные списки: Object = POINTER ТО ObjDesc; ObjDesc = RECORD name: Ident; class: INTEGER; type: Type; next: Object; val: LONGINT END
Объявления 67 Так, например, следующие объявления представлены списком, показанным на рис. 8.1. CONST N = 10; TYPE Т = ARRAY N OF INTEGER; VAR x, у: T topScope —► name class type val next T^- Const Int 10 b. T’ Type a- ”x" Var T У Var T NIL ' 1 F p Рис. 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.dass := class; new.next := guard; x.next := new; obj := new ELSE obj := x.next; Магк('повторное объявление') END End NewObj Для ускорения поиска список часто заменяют древовидной структурой. Ее преимущество становится заметным только при довольно большом количестве записей. Для структурированных языков с локальными областями, то есть зонами видимости идентификаторов, таблица символов тоже должна быть структури- рованной, и тогда число записей в каждой области становится относительно не- большим. Опыт показывает, что древовидная структура не дает ощутимого пре- имущества над списком, хотя требует более сложного процесса поиска и наличия в записи трех указателей на потомков вместо одного. Отметим, что очередность записей все равно должна быть отражена в структуре, так как это существенно в случае параметров процедуры.
68 Учет контекста, заданного объявлениями 8.2. Записи о типах данных В языках, имеющих типы данных, проверка их совместимости является одной из важнейших задач компилятора. Проверка основана на атрибуте type, вносимом в каждую запись таблицы символов. Поскольку сами типы данных тоже могут объявляться, очевидным решением оказывается указатель на соответствующую за- пись о типе. Однако тип может быть объявлен анонимно, как в следующем примере: VAR a: ARRAY 10 OF INTEGER Здесь тип переменной а не имеет имени. Простое решение этой проблемы - заводить в компиляторе собственные типы данных, чтобы представлять типы как таковые. Тогда именованные типы представляются в таблице символов записями типа Object, которые, в свою очередь, ссылаются на элемент типа Туре. Туре = POINTER ТО 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 2 LONGREAL 8 LONGINT 4 CHAR 1 SHORTINT 1 BOOLEAN 1 REAL 4 SET 4
70 Учет контекста, заданного объявлениями Из вышесказанного заключаем, что каждый тип имеет размер, а каждая пере- менная - адрес. Атрибуты type.size и obj.adr определяются, когда компилятор обрабатывает объявления. Размеры элементарных типов задаются машинной архитектурой, а соответствующие им записи генерируются, когда компилятор загружается и инициализируется. Для объявляемых структурных типов их размер рассчиты- вается. Размер массива - это размер его элемента, умноженный на количество его эле- ментов. Адрес элемента - это адрес массива плюс индекс массива, умноженный на размер элемента. Пусть даны следующие общие объявления: ТУРЕ Т = ARRAY n OF ТО VAR а: Т Тогда размер типа и адрес элемента получаются следующим образом: size(T) = n*size(T0) adr(a[x]) = adr(a) + x*size(TO) Многомерным массивам a[0,0] a[0,l] 0 4 TYPE Т = ARRAY Пк-,.... ,ni,n0 OF ТО соответствуют следующие формулы (см. рис. 8.3): afl.O] all, 1] 8 12 size(T) = nk-i * ... *П| *п0* size(TO) Рис. 8.3. Представ- adr(a[Xk-i, ... , xt, х01) = adr(a)+ + xk-i * nk-2 * ... * n0 * sizeCTO) + ... ление матрицы + х2 * л, * n0 * size(TO) + х1 * п0 * size(TO) + х0 * size(TO) = adr(a) + (((.. Хк-ГПк-i + + х2)*п1 + Х))*По + х0) * size(TO) (схема Горнера) Заметьте, что при вычислении размера массива длины всех его размерностей известны, потому что они встречаются в тексте программы как константы. Одна- ко значения индексов при вычислении адреса элемента, как правило, неизвестны до выполнения программы. a: ARRAY 2 OF ARRAY 2 OF REAL Напротив, для записей и размер типа, и адрес поля известны во время компи- ляции. Давайте рассмотрим следующие объявления: TYPE Т = RECORD f0: То; f,: Tt; ... ; fk_!: Tk., END VAR г: T Тогда размер типа и адреса полей вычисляются в соответствии со следующими формулами: size(T) = size(T0) + ... + size(Tk-i) adr(r.fi) = adr(r) + offset(f|) offset(f|) = size(T0) + ... + size(T|.i) Абсолютные адреса переменных обычно неизвестны на этапе компиляции. Все генерируемые адреса должны рассчитываться относительно общего базового ад-
Представление данных во время выполнения 71 реса, который устанавливается во время выполнения. Тогда исполнительный ад- рес - это сумма этого базового адреса и относительного адреса, определяемого компилятором. Если память компьютера адресуется побайтно, как это часто бывает, то нужно иметь в виду следующее. Хотя каждый байт имеет собственный адрес, данные в память или из памяти обычно передаются пакетом из небольшого количества байтов (скажем, 4 или 8), называемым словом. Если выделение памяти произво- дится строго последовательно, то возможно, что переменная может занять (час- тично) несколько слов (см. рис. 8.4). Этого следует решительно избегать, так как иначе обращение к одной переменной потребует нескольких обращений к памяти, приводя к ощутимому замедлению. Простой метод решения этой проблемы - ок- ругление адреса каждой переменной до числа, кратного ее размеру. Этот процесс называется выравниванием. Такое правило применяется для элементарных типов данных. Точно так же подгоняется размер элементов массива, а размеры полей записей мы просто округляем до длины слова компьютера. Цена выравнивания - потеря некоторых байтов памяти компьютера, количество которых довольно не- значительно. VAR a:CHAR; b,c: INTEGER; d: REAL Невыровненные, Выровненные поля рваные поля з | 2 | I I о 3 2 10 ь a c d c b a d c d Рис. 8.4. Выравнивание при вычислении адресов Для генерации необходимых записей в таблице символов нужны следующие дополнения к процедуре синтаксического анализа объявлений: If sym = type THEN(* “TYPE1’ ident ”=” type *) Get(sym); WHILE sym=ident DO NewObj(obj, Typ); Get(Sym); IF sym = eql THEN Get(sym) ELSE Mark(";?”) END; Typel (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 Учет контекста, заданного объявлениями IdentListfVar, first); Typel(tp); objfirst; WHILE obj # guard DO Obj.type:=tp; INC(adr, obj.type.size); obj.val:=-adr; obj;=obj.next END; IF sym = semicolon THEN Cet(sym) ELSE MARK(“;?”) END END END; Здесь процедура IdentList используется для того, чтобы обработать список идентификаторов, а рекурсивная процедура Туре! служит для компиляции объявлений типов. PROCEDURE ldentList(dass: 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 syrrHdentTHEN NewObj(object,dass);Get(sym) ELSE МагкС'идентификатор?') END END; IF sym = colon THEN Get(sym) else Mark(“:?") END END END IdentList; PROCEDURE Type! (VAR type: OSG.Type); VAR obj, first: Object; tp: OSG.Type; BEGIN type := intType; (‘синхронизация*) IF (sym # ident) & (sym < array) THEN МагкС'тип?"); 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 МагкС'тип?") END ELSIF sym = array THEN Get(sym); expression(x); IF (sym = number) THEN n := val; Get(sym) ELSE МагкС'число?"); n:=l END; IF sym = of THEN Get(sym) ELSE MarkCOF?”) 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 ldentList(Fld, first); Type(tp); obj := first; WHILE obj # guard DO obj.type := tp; obj.val := type.size; INC(type.slze, obj.type.size); obj obj.next END
Упражнения 73 END ; type.fields := topScope.next; CloseScope; IF sym = end THEN Get(sym) ELSE MarkC'END?") END ELSE О55.Магк("идентификатор?") END END Ту pel; Следуя давней традиции, адресам переменных присваиваются отрицательные значения, то есть отрицательные смещения относительно общего базового адреса, устанавливаемого во время выполнения программы. Вспомогательные процеду- ры OpenScope и CloseScope служат для того, чтобы список полей записи не пере- мешивался со списком переменных. Каждое объявление записи устанавливает новую область видимости для идентификаторов полей, как того требует опреде- ление языка Оберон. Обратите внимание, что начало списка, в который добавля- ются новые записи, задается глобальной переменной topScope. 8.4. Упражнения 8.1. По определению область видимости идентификатора простирается от места его объявления до конца процедуры, в которой оказалось это объявление. Что нужно для того, чтобы эта область могла простираться от начала до конца про- цедуры? 8.2. Рассмотрим объявления указателей, как они определены в Обероне. В них указывается тип, с которым связан объявляемый указатель и который может по- явиться в тексте позже. Что нужно для того, чтобы примирить это послабление с общим правилом о том, что все объекты, на которые ссылаются, должны объяв- ляться до своего использования?

Глава 9 RISC-архитектура как цель
76 RISC-архитеюура как цель Стоит заметить, что наш компилятор вплоть до этого раздела мог быть разработан независимо от целевого компьютера, для которого он должен генерировать код. В самом деле, почему целевая структура машины должна влиять на синтаксический анализ и обработку ошибок? Напротив, такого влияния необходимо сознательно избегать. В результате, согласно принципу пошаговой разработки, генерация кода для произвольного компьютера может быть добавлена к существующему машин- но независимому анализатору, который служит для нее каркасом. Прежде чем взяться за эту задачу, должна быть выбрана целевая архитектура. Чтобы сохранить и разумную простоту компилятора, и чистоту проекта от де- талей. которые касаются только определенной машины и ее особенностей, мы обусловим архитектуру нашим собственным выбором. Таким образом мы полу- чим существенное преимущество в том, что она может быть приспособлена к нуж- дам исходного языка. Эта архитектура не существует как реальная машина, поэтому она виртуальна. Но так как каждый компьютер выполняет команды со- гласно фиксированному алгоритму, он может быть легко описан программой. А затем для выполнения этой программы может быть использован реальный ком- пьютер, который будет интерпретировать сгенерированный код. Такую програм- му называют интерпретатором, и она эмулирует виртуальную машину, которая, если можно так сказать, имеет полуреальное существование. Цель этой главы - не изложение мотивов выбора определенной виртуальной архитектуры со всеми ее деталями. Она скорее предназначена служить описатель- ным руководством, состоящим из неформального введения и формального опре- деления компьютера в виде интерпретирующей программы. Эту формализацию можно даже рассматривать в качестве примера точной спецификации процессора. При определении этого компьютера мы намеренно следуем линии, близкой RISC-архитектуре. Аббревиатура RISC (ReducedInstruction 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 а, с MVN а, с ADD а, Ь, с SUB а, Ь, с MUL а, Ь, с DIV а, Ь, с MOD а, Ь, с СМР Ь, с 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 DM a, b, im MODI a, b, im CMPI b, im R.a := Shiftfim, 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 Рис. 9.1. Блок-схема RISC-структуры
78 RISC-архитектура как цель В случае регистровых команд имеются два варианта. Или второй операнд яв- ляется непосредственным значением (F1), а знак 18-6итовой константы im рас- пространяется до 32 битов. Или второй операнд - это номер регистра (F0). Ко- манда сравнения СМР влияет на биты состояния Z и N (нуль и отрицательно). 2. Команды памяти (формат F2): LDW a, b, im LDB a, b, im POP а, Ь, 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 PC := PC + disp*4, если Z BNE disp PC := PC + disp*4, если ~Z BLT disp PC := PC + disp*4, если N BCE disp PC := PC + disp*4, если ~N BLE disp PC := PC + disp*4, если Z или N BCT disp PC := PC + disp*4, если ~(Z или N) BR disp PC := PC + disp*4 BSR disp R14 := PC; PC := PC + disp*4 (адрес относительно PC) RET disp PC := R.c Более подробно виртуальный компьютер определяется следующей програм- мой- интерпретатором. Заметим, что регистр 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; BCE = 51; BLE = 52; BCT=53; BR = 56; BSR = 57; RET = 58; VAR IR: LONGINT; N, Z: BOOLEAN; R‘: ARRAY 1 6 OF LONGINT; M‘: ARRAY MemSize DIV 4 OF LONGINT; W: Texts.Writer; (* R[ 1 5] есть 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[1 5] := start + ProgOrg; LOOP (* цикл интепретации *) nxt := R[1 5] + 4; IR := M[R[1 5] DIV 4]; opc := IR DIV 4000000H MOD 40H; a := IR DIV 400000H MOD 10H; b := IR DIV 40000H MODI OH; 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 с >= 20000H THEN DEC(c, 40000H) END (‘расширение знака*) ELSE (*F3‘) c := IR MOD 4000000H; IF с >= 2000000H THEN DEC(c, 4000000H) END (‘расширение знака*) END ; CASE opc OF MOV, MOVI: R[a] := ASH(c, b) (‘арифметический сдвиг*) I MVN, MVNI: R[a] := -ASH(c, b) I ADD, ADDI: R[a] := R[b] + c I SUB, SUBI: R[a] := R[b] - c I MUL, MULI: R[a] := R[b] * c I Div, DIVI: R[a] := R[b] DIV c I Mod, MODI: R[a] := R[b] MOD c I CMP, CMPI: Z := R[b] = c; N := R[b] < c I CHKI: IF (R[a] < 0) OR (R[a] >= c) THEN R[a] := 0 END I LDW: R[a] := M[(R[b] + c) DIV 4] I LDB: (‘не реализуется*) I POP: R[a] := M[(R[b]) DIV 4]; INC(R[b], c) I STW: M[(R[b] + c) DIV 4] := R[a] I STB: (‘не реализуется*) I PSH: DEC(R[b], c); M[(R[b]) DIV 4) := R[a] I RD: Texts.Scan(in); R[a] := in.i I WRD: Texts.Write(W, ""); Texts.Writelnt(W, R[c], 1) I WRH: Texts.WriteHex(W, R[c]) I WRL: Texts.WriteLn(W); Texts.Append(out, W.buf)
80 RISC-архитектура как цель I BEQ: IF Z THEN nxt := R[1 5] + c*4 END I BNE: IF ~Z THEN nxt := R[1 5] + c*4 END | BLT: IF N THEN nxt := R[ 15] + c‘4 END | BCE. IF ~N THEN nxt := R[1 5] + c*4 END I BLE: IF Z OR N THEN nxt := R[1 5] + c*4 END | BCT: IF ~Z & ~N THEN nxt := R[ 1 5] + c*4 END | BR. nxt := R[1 5] + c*4 I BSR: nxt := R[ 1 5] + c*4; R[ 14] := R[1 5] + 4 I RET: nxt := R[c MOD 1 OH]; IF nxt = 0 THEN EXIT END END ; R[1 5] := 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 увеличивается или уменьшается на значение с. Они позволят удобным способом передавать параметры процедуры (см. главу 12). 4. Команда CHKI просто обнуляет значение индекса, которое вышло за допус- тимые границы, потому что RISC не поддерживает прерываний.
Глава 10 Выражения и присваивания 'tA;i^ftj[Rwe- /~я ,. "Я2 10 .4 irtHjft.KCf-iwW’.-H- .: . J ^ж^^^^^ИШйЙМЙШЛЛж
82 Выражения и присваивания 10 .1. Прямая генерация кода по принципу стека В третьем примере главы 5 показано, как преобразовать выражение из обыкно- венной инфиксной в эквивалентную постфиксную запись. Наш идеальный ком- пьютер мог бы напрямую интерпретировать постфиксную запись. Как уже было показано, такой идеальный компьютер нуждается в стеке для хранения промежу- точных результатов. И подобная компьютерная архитектура называется стековой архитектурой. Компьютеры со стековой архитектурой не являются общепринятыми. Стеку предпочитают наборы явно адресуемых регистров. Конечно, набор регистров мо- жет легко использоваться для эмуляции стека. На его верхний элемент указывает глобальная переменная, представляющая указатель стека (SP) в компиляторе. Это возможно, поскольку количество промежуточных результатов известно на этапе компиляции, а использование глобальной переменной оправдано тем, что стек является глобальным ресурсом. Чтобы получить программу для генерации кода, соответствующего конкрет- ным конструкциям, мы должны сначала определить шаблоны требуемого кода. Этот метод, помимо выражений и присваиваний, будет в дальнейшем с тем же ус- пехом применяться и для других конструкций. Пусть код для данной конструк- ции К задается следующей таблицей: К code(K) Побочный эффект Ident LDW i, 0, adr(ident) INC(SP) number (ехр) MOVI i, 0, value code (exp) INC(SP) facO * facl code(facO) code(facl) MUL i, i, i+l DEC(SP) termO + terml code(termO) code(terml) ADD I, i, i+l ADD i, i, i+l DEC(SP) ident := exp code(exp) STW i, adr(ident) DEC(SP) DEC(SP) Для начала ограничимся операндами в виде простых переменных и пренебре- жем селекторами для структурных переменных. Сначала рассмотрим выражение u := х*у + z*w : Команда Смысл команды Стек Указатель стека LDW RO, base, x RO := x X SP= 1 LDW Rl, base, у Rl := у x, у 2 MUL RO, Rl, R2 RO := R0*R1 x*y 1 LDW Rl, base, z Rl := z x*y, z 2 LDW R2, base, w R2 := w x*y, z, w 3
Прямая генерация кода по принципу стека 83 MULR1,R1,R2 R1:=R1*R2 х*у, z*w 2 ADDRO.RO, R1 R1:=R1+R2 x*y + z*w 1 STW RO, base, u u := R1 - 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 = Iparen 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. Мы предполагаем, что она размещает коман- ду в глобальном массиве, используя переменную рс в качестве индекса, указыва- ющего на следующую свободную позицию в массиве. В таких допущениях процедура Put записывается в Обероне следующим образом, где LSH(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-процессору, но фактически и ко всем распространен- ным компьютерам. Мы принимаем этот факт во внимание, задавая адреса в виде пар, состоящих из смещения а и базы (регистра) г. 10 .2. Отсроченная генерация кода Рассмотрим в качестве второго примера выражение х + I. Согласно схеме из раз- дела 10.1, мы получаем соответствующий код: LDW 0, base, х R0 := х MOVI 1,0, 1 Rl := 1 ADD 0, 0, 1 RO := RO + R1 Мы видим, что полученный код правильный, но явно не оптимальный. Недо- статок заключается в том, что константа 1 загружается в регистр, хотя в этом нет необходимости, так как наш компьютер имеет команду, позволяющую добавлять константы непосредственно к регистру (режим непосредственной адресации). Ясно, что некоторый код был выдан поспешно. Выход состоит в том, чтобы в опре- деленных случаях задержать выдачу кода до тех пор, пока не будет точно извест- но, что лучшего решения не существует. Каким же образом должна быть реализо- вана такая отсроченная генерация кода? В общих чертах метод заключается в привязке к полученной синтаксической конструкции информации, которая должна использоваться для выбора порож-
Отсроченная генерация кода 85 даемого кода. Согласно принципам атрибутных грамматик, изложенным в главе 5, подобная информация хранится в виде атрибутов. Таким образом, генерация кода зависит не только от синтаксически сворачиваемых символов, но и от значе- ний их атрибутов. Это концептуальное расширение выражается в том, что про- цедуры синтаксического анализа снабжаются выходным параметром, который представляет эти атрибуты. Так как обычно атрибутов бывает несколько, то для таких параметров используется тип RECORD; назовем этот тип Item [19]. Для нашего второго примера необходимо установить, хранится ли (во время выполнения) значение множителя, слагаемого или выражения в регистре, как это было до сих пор, или оно известная константа. Последнее, скорее всего, приведет к команде с непосредственной адресацией. Теперь становится ясно, что атрибут должен задавать режим (mode) для множителя, слагаемого или выражения, то есть определять, где хранится его значение и как к нему обратиться. Подобный атрибут-режим соответствует режиму адресации команд компьютера, а диапазон его возможных значений зависит от множества режимов адресации целевого ком- пьютера. Каждому режиму адресации соответствует значение атрибута-режима элемента. Атрибут-режим также неявно вводится классами объектов. Классы объектов и режимы элементов частично совпадают. В случае нашей RISC-архи- тектуры существуют только три режима адресации: Режим элемента Класс объекта Режим адресации Дополнительные атрибуты Var Var Прямой а Значение памяти по адресу а Const Const Непосредственный а Значение есть константа а Reg - Регистр г Значение содержится в регистре R[r] Имея это в виду, мы объявляем тип данных Item как запись с полями mode, type, а и г. Ясно, что атрибутом элемента также является его тип. Но ниже об этом больше упоминаться не будет, потому что мы будем рассматривать только один тип - Integer. Теперь процедуры анализатора выглядят как функции с результатом типа Item. Однако из соображений программирования вместо функций предполагает- ся использовать именно процедуры с выходным параметром. Item = RECORD mode: INTEGER; type: Type; a, r: LONGINT; END Давайте теперь вернемся к нашему примеру, чтобы продемонстрировать гене- рацию кода для выражения х + 1. Процесс изображен на рис. 10.1. Преобразова- ние Var-Item в Reg-Item сопровождается выдачей команды LDW, а преобразова- ние Reg-Item и Const-Item в Reg-Item сопровождается выдачей команды ADDI.
86 Выражения и присваивания Отметим подобие типов Item и Object. Оба они описывают объекты, но если тип Object представляет объявленные, именованные объекты, видимость которых выходит за пределы их объявлений, то тип Item описывает объекты, которые все- гда жестко связаны со своей синтаксической конструкцией. Поэтому настоятель- но рекомендуется не создавать объекты типа Item динамически (в куче), а объяв- лять их как локальные параметры и переменные. PROCEDURE factor(VAR х: 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 = Iparen 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); Opl (minus, x) ELSE term(x) END ; WHILE (sym = plus) OR (sym = minus) DO
Отсроченная генерация кода 87 op := sym; Get(sym); term(y); Ор2(ор, х, у) END END SimpleExpression; PROCEDURE Statement; VAR obj: Object; x, y: Item; BEGIN IF sym = ident THEN find(obj); Cet(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; Генерирующие код операторы теперь собраны в двух процедурах Opl и 0р2. 1есь также используется принцип отсроченной генерации кода, чтобы избежать щачи арифметических команд, когда компилятор сам может выполнить эту ерацию. Это тот случай, когда оба операнда являются константами. Такой ме- а известен как свертка констант {constantfolding). PROCEDURE Opl (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 Opl; PROCEDURE Op2(op: INTEGER; VAR x, y: Item); (* x := x op у *) 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, у: 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 >= О THEN IF ya <= MAXflNTEGER) - x.a THEN x.a := x.a + y.a ELSE МагкС'индекс не целое") END ELSE IF ya >= MINONTEGER) - xa THEN xa := xa + ya ELSE МагкС'индекс вне диапазона') END END Сущность отсроченной генерации кода состоит в том, что код не выдается, пока не станет ясно, что не существует лучшего решения. Например, операнд не загружается в регистр, пока не выяснится, что это неизбежно. Мы даже отказываемся от распределения регистров согласно жесткому прин- ципу стека. Это выгодно в определенных случаях, которые будут объяснены поз- же. Процедура GetReg выдает и резервирует один из свободных регистров. Мно- жество свободных регистров удобно представить глобальной переменной regs. Конечно, необходимо позаботиться о том, чтобы освобождать регистры, как толь- ко их значение становится ненужным. PROCEDURE GetReg(VAR г; LONGINT); VAR i: INTEGER; BEGIN i := 1; WHILE (i < 1 5) & (i IN regs) DO INC(i) END ; INCL(regs, i); r := i END GetReg; Принцип отсроченной генерации кода полезен также во многих других случа- ях, но он становится необходимым, когда дело касается компьютеров со сложны-
Индексированные переменные и поля записей 89 и режимами адресации, для которых должен быть сгенерирован достаточно эф- ективный код за счет умелого использования доступных сложных режимов, качестве примера рассмотрим генерацию кода для CISC-архитектуры. Обычно ней предполагаются команды с двумя операндами, один из которых становится 'результатом. Рассмотрим выражение u := х + y*z и получим следующую после- >вательность команд: MOV у, RO RO := у MUL z, RO RO := RO * z ADD х, RO RO := RO + х MOV RO, u u := RO Она получается за счет отсрочки загрузки переменных до того момента, когда <и должны сливаться с другим операндом. Поскольку команда заносит резуль- IT во второй операнд, последний не может быть фактическим адресом перемен- )й, а может быть только временной переменной, обычно регистром. Команда ко- срования не выдается до тех пор, пока не выяснится, что это неизбежно, обочный эффект такой меры состоит в том, что, например, простое присваива- аех := у вообще исключает пересылку в регистр, а производится непосредствен- ) командой копирования, которая увеличивает эффективность и в то же время леньшает длину кода: MOV у, х х := у 0.3. Индексированные переменные и поля записей о сих пор мы имели дело только с простыми переменными в выражениях и усваиваниях. Обращение к элементам структурных переменных - массивов и шисей - требует выбора элемента согласно вычисленному индексу или иден- (фикатору поля соответственно. Синтаксически идентификатор переменной шровождается одним или несколькими селекторами. Это отражается в син- жсическом анализаторе вызовом процедуры selector в процедурах factor и atSequence: find(obj); Cet(sym); x.mode := obj.class; x.a := obj.adr; x.r := 0; selector(x) Процедура selector обрабатывает не один селектор, но, если нужно, всю цепоч- / селекторов. Из ее текста видно, что здесь также используется атрибут type опе- 1нда х. PROCEDURE selector(VAR х: Item); VAR у: Item; obj: Object; BEGIN WHILE (sym = Ibrak) OR (sym = period) DO IF sym = Ibrak THEN Get(sym); expression^ не мааив., END. IF x.type.form = Array Tntn шиелсл, n
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); Cet(sym); IF obj # guard THEN Field(x, obj) ELSE МагкСне определено") END ELSE Магк(”не запись") END ELSE МагкСидентификатор?') END END END END selector; Адрес выбираемого элемента вычисляется по формулам, приведенным в раз- деле 8.3. В случае идентификатора поля адрес вычисляется компилятором и рав- няется сумме адреса переменной и смещений ее полей. PROCEDURE Field(VAR х: Item; у: Object); (* х ;= х.у *) BEGIN INC(x.a, y.val); x.type := y.type END Field; В случае индексированной переменной код выдается согласно формуле adr(a[kj) = adr(a) + k * size(T) Здесь а обозначает переменную-массив, k - индекс и Т - тип элементов масси- ва. Вычисление индекса требует двух команд: умноженный на размер индекс до- бавляется к регистровой составляющей адреса. Пусть индекс хранится в регистре R.J, и пусть адрес массива хранится в регистре R.i. MULI j, j, size(T) ADD j, i,j Процедура Index выдает приведенный выше код для индекса, проверяет, дей- ствительно ли индексированная переменная - массив, и, если индекс - константа, вычисляет адрес элемента. PROCEDURE lndex(VAR х, у: Item); (* х х[у] *) VAR z: Item; BEGIN IF y.type # intType THEN МагкСиндекс не целое”) END ; IF y.mode = Const THEN IF (y.a < 0) OR (y.a >= x.type.len) THEN МагкСиндекс вне диапазона") END; 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
Индексированные переменные и поля записей 91 END; x.type := х.type.base END Index; Мы можем теперь привести код, получающийся для следующего программно- [фагмента, который содержит одно- и двумерные массивы: PROCEDURE Р1; VAR i, j : INTEGER; a: ARRAY 4 OF INTEGER; b: ARRAY 3 OF ARRAY 5 OF INTEGER; BEGIN I := a[j); I := a[2J; I := a[i+j]; I := b[i][j]; I := b[2][4J; I := a[a[i]] END Pl. adr-4,-8 adr-24 adr-84 LDW 0, base, -8 I := a[j] MULI 0,0,4 ADD 0, base, 0 LDW 1,0,-24 a STW 1, base, -4 I LDW 0, base,-l 6 i:=a[2] STW 0, base, -4 LDW 0, base, -4 i := a[i+J]; LDW l.base, -8 ADD 0,0,1 i+J MULI 0,0,4 ADD 0, base, 0 LDW 1,0,-24 STW l.base, -4 i LDW 1.base, -4 i:=b[i]0] MULI 0, 0, 20 ADD 0, base, 0 LDW 1, base,-8 j MULI 1,1,4 ADD 1,0,1 LDW 0, 1 ,-84 b STW 0, base, -4 I LDW 0, base, -28 i:=b[2][4] STW 0, base, -4 LDW 0, base, -4 I := a[a[i]] MULI 0,0,4 ADD 0, base, 0 LDW 1,0,-24 MULI 1,1,4 ADD 1 .base, 1 LDW 0,1,-24 STW 0, base, -4 Заметим, что правильность индекса может быть проверена, только если индекс - юистанта, то есть если он имеет уже известное значение. В противном случае ни-
92 Выражения и присваивания деке не может быть проверен до момента начала выполнения программы. Хотя проверка, конечно, избыточна в правильных программах, опускать ее не рекомен- дуется. Она вполне оправдана для сохранения структуры массива. Однако разра- ботчик компилятора должен попытаться достичь ее предельной эффективности. Проверка принимает форму оператора IF (к < 0) OR (к >= n) THEN HALT END где к - индекс и п - длина массива. Для нашего виртуального компьютера мы про- сто заводим соответствующую команду. В других же случаях должна быть найде- на подходящая последовательность команд. Нужно учесть следующее: так как в Обероне нижняя граница массива всегда 0, достаточно одного сравнения, если значение индекса считается беззнаковым целым числом. Это так, потому что от- рицательные значения в дополнительном коде содержат 1 в знаковом разряде, де- лая беззнаковое значение больше самого большого (знакового) целого значения. В связи с этим процедура Index дополняется генерацией команды СНК, кото- рая приводит к завершению вычисления в случае недопустимого индекса. IF y.mode # Reg THEN load(y) END ; PutfCHKI, y.r, 0, x.type.base,len); Put(MULI, y.r, y.r, x.type.base.size); В заключение приводится пример программы с вложенными структурами дан- ных. Из него ясно видно, как специальная обработка констант в селекторах упро- щает код для вычисления адресов. Сравните полученный код для переменных с индексом-выражением и с индексом-константой. Команды СНК ради краткости опущены. PROCEDURE Р2; TYPE RO = RECORD х, у: INTEGER END; Rl = RECORD u: INTEGER; v: ARRAY 4 OF RO; w: INTEGER offset 0 offset 4 END ; VAR i,j, k: INTEGER; offset 36 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 adr-4,-8,-12 adr-92 END P2. LDW 0, base, -4 MULI 0, 0, 40 ADD 0, base, 0 LDW 1,0, -92 STW I, base, -12 LDW 0, base,-16 STW 0, base, -12 LDW 0, base, -4 S[I].U k s[l].w
Индексированные переменные и поля записей 93 MULI 0, 0, 40 ADD 0, base, 0 LDW 1, base, -8 j MULI 1, 1,8 ADD 1,0,1 LDW 0, 1,-88 s[i].v[j].x STW 0, base, -12 LDW 0, base, -28 s[l].v[2].y STW 0, base, -1 2 LDW 0, base, -4 i MULI 0, 0, 8 ADD 0, base, 0 LDW 1, base, -12 k STW 1,0, -84 s[0].v[i].y Желание держать машинно-зависимые части компилятора отдельно от ма- нно-независимых частей наводит на мысль о том, чтобы собрать все генери- ощие код операторы в виде процедур в отдельном модуле. Мы назовем этот туль OSC и представим его интерфейс. Он содержит несколько процедур гене- ора кодов, с которыми мы уже столкнулись. Остальные будут объясняться ивах 11 и 12. DEFINITION OSC; IMPORT OSS, Texts, Fonts, Display; CONST Head = 0; Var = 1; Par = 2; Const = 3; Fid = 4; Typ = 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 MakeConstltem (VAR х: Item; typ: Type; val: LONGINT); PROCEDURE Makeitem (VAR x; Item; y: Object); PROCEDURE Field (VAR x: Item; y: Object); PROCEDURE Index (VAR x, y: Item); PROCEDURE Opl (op; INTEGER; VAR x: Item); PROCEDURE Op2 (op: INTEGER; VAR x, y: Item); PROCEDURE Relation (op: INTEGER; VAR x, y: Item); PROCEDURE Store (VAR x, y: Item); PROCEDURE Parameter (VAR x: Item; ftyp: Type; class: INTEGER); PROCEDURE CJump (VAR x: Item); PROCEDURE BJump (L: LONGINT); PROCEDURE FJump (VAR L: LONGINT); PROCEDURE Call (VAR x: Item); PROCEDURE lOCall (VAR x, y: Item); PROCEDURE Header (size: LONGINT); PROCEDURE Enter (size: LONGINT); PROCEDURE Return (size: LONGINT); PROCEDURE Open; PROCEDURE Close (VAR S: Texts.Scanner; globals: LONGINT); END OSG. 10.4. Упражнения 10.1. Усовершенствуйте компилятор Оберон-О таким образом, чтобы команды умножения и деления заменялись более быстрыми командами сдвига и маскиро- вания, когда множитель или делитель являются степенью 2. 10.2. Усовершенствуйте компилятор Оберон-О таким образом, чтобы код обра- щения к элементам массива включал проверку значения индекса на соответствие диапазону, заданному в объявлении массива. 10.3. Упростится ли компиляция присваиваний Оберона, если в них левую и правую части поменять местами, например е =: V? 10.4. Рассмотрите в Обероне случай многократного присваивания е =: Vo =' Vi =:... =; vn. Реализуйте его. Создает ли определение его семантики какие-нибудь проблемы? 10.5. Замените определение выражений Оберона определением Алгола 60 (см. упражнение 2.1) и реализуйте его. Обсудите достоинства и недостатки двух опре- делений.
Глава 11 Условные и циклические операторы и логические выражения . 07' 101' ере^:н*-ши'Л . . лНИЯ . .* < 105 •’06 '
96 Условные и циклические операторы и логические выражения 11.1. Сравнения и переходы Условные и циклические операторы реализуются с помощью команд перехода, называемых также командами ветвления. В качестве первого примера рассмот- рим самую простую форму условного оператора: IF х = у THEN StatSequence END Его отображение в последовательность команд очень простое: IF х = у EQL х, у BF L THEN StatSequence code(StatSequence) END L ... Наши соображения опять основываются на стековой архитектуре. Команда EQL проверяет два операнда на равенство и заменяет их в стеке логическим ре- зультатом. Следующая команда ветвления BF (переход, если ложь) переходит на метку L, если этот результат есть FALSE, и удаляет его из стека. Подобно EQL опре- деляются команды условного перехода для отношений <=, <, >=, # и >. Однако, к сожалению, такие дружественные компиляторам компьютеры не имеют широкого распространения. Чаще всего это обычные компьютеры, чьи ко- манды перехода зависят от результата сравнения значения регистра с 0. Обозна- чим их как BNE (переход, если не равно), BLT (переход, если меньше), BGE (пере- ход, если больше или равно), BLE (переход, если меньше или равно) и BGT (переход, если больше). Последовательность кодов, соответствующая приведен- ному выше примеру, становится такой: IF х = у code(Ri := х - у) BNEL THEN StatSequence code(StatSequence) END L ... Использование вычитания (x - у >= 0 взамен х >= у) таит в себе скрытую ловушку: вычитание может привести к переполнению, приводящему к останов- ке программы или к ошибочному результату. Поэтому вместо вычитания используется специальная команда сравнения СМР, которая не приводит к пере- полнению, но при этом корректно определяет, равна ли разность нулю, положи- тельна она или отрицательна. Результат обычно сохраняется в специальном ре- гистре, называемом кодом условия, который состоит из двух битов N и Z, указывающих, является ли разность отрицательной и нулевой соответственно В этом случае все команды условного перехода неявно обращаются к этому ре гистру как к аргументу. IFx = y СМР х, у BNE L THEN StatSequence code(StatSequence) END L ...
Условные и циклические операторы 97 11.2. Условные и циклические операторы Теперь возникает вопрос: как логическое значение может быть представлено зна- чением типа Item? В случае стековой архитектуры ответ прост: так как результат сравнения находится в стеке, как любой другой результат, никакого специального значения Item.mode не требуется. Однако команда СМР требует дальнейших раз- мышлений. Сначала мы ограничимся рассмотрением простых вариантов сравне- ний без последующих логических операций. В случае архитектуры с командой СМР в элементе, представляющем результат сравнения, необходимо указать, какой регистр содержит вычисленную разность и какое отношение соответствует этому сравнению. Для последнего требуется новый атрибут; мы назовем новый режим Cond, а его новый атрибут (поле записи) - с. Отображение отношений на значения поля с задается следующим образом: О #1 <2 >= 3 <= 4 >5 Конструкция со сравнениями - это выражение. Его синтаксис таков: expression = SimpleExpression [("="|"#’Т’<"Г,<=Т>Т,>=") SimpleExpression]. Соответствующая ему процедура синтаксического анализа легко расширяется следующим образом: PROCEDURE expression(VAR х: Item); VAR у: 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 МагкСневерный тип”) 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 MarkC'THEN?") END;
98 Условные и циклические операторы и логические выражения StatSequence; Fixup(x.a) IF sym » end THEN Cet(sym) ELSE Mark("END?") END; Процедура CJump(x) генерирует необходимую команду перехода согласно ее параметру х.с таким образом, что переход происходит, если указанное условие не выполняется. Здесь становится очевидной трудность, которая свойственна всем однопро- ходным компиляторам: значения адресов переходов еще неизвестны в момент выдачи команд перехода. Эта проблема решается добавлением адреса команды перехода в качестве атрибута генерируемого элемента типа Item. Данный атрибут используется позже, когда адрес перехода становится известным и можно завер- шить его формирование с истинным адресом. Это называется закреплением (fixup) адреса. Простое решение возможно, только если код размещается в глобальном массиве, элементы которого всегда доступны. Но это невозможно, если выдавае- мый код сохраняется непосредственно на диске. Для представления адреса неза- вершенной команды перехода мы используем поле элемента а. PROCEDURE CJump(VAR х,у: 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.MarkCBoolean?”); 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 выдает сообщение об ошибке, если х не имеет тип BOOLEAN. Отметим, что в командах перехода используются адреса относительно адреса са- мой команды (относительно PC), поэтому используется значение pc - L. Наконец, мы должны показать, как компилируется условный оператор самого общего вида; его синтаксис таков: "IF" expression "THEN" StatSequence {"ELSIF" expression "THEN" StatSequence) ["ELSE" StatSequence] "END" а соответствующие ему шаблоны кода таковы: IF expression THEN code(expression) Bcond LO
Условные и циклические операторы 99 StatSequence code(StatSequence) BR L ELSIF expression THEN LO code(expression) Bcond LI StatSequence code(StatSequence) BR L ELSIF expression THEN LI code(expression) Bcond L2 StatSequence code(StatSequence) BRL ELSE StatSequence Ln code(StatSequence) END L ... Отсюда операторы синтаксического анализатора могут быть получены как часть процедуры StatSeqence. Несмотря на то что число конструкций ELSIF и, сле- довательно, меток LI, L2,...,Ln может оказаться произвольным, достаточно един- ственной переменной элемента кода х. Для каждого экземпляра ELSIF ей присваи- вается новое значение. ELSIF sym = if THEN Cet(sym); expression(x); Cjump(x); IF sym = then THEN Cet(sym) ELSE MarkC'THEN?”) END ; StatSequence; L := 0; WHILE sym = elsif DO Get(sym); FJump(L); Fixup(x.a); expression(x); Cjump(x); IF sym = then THEN Cet(sym) ELSE MarkC'THEN?") END ; StatSequence END; IF sym = else THEN Cet(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 LI: LONGINT; BEGIN WHILE L # 0 DO LI := code[L] MOD 10000H; Fixup(L); L := LI END END FixLink; Компиляция оператора WHILE очень похожа на компиляцию простого услов- ного оператора IF. В дополнение к первому условному переходу вперед здесь необ- ходим безусловный переход назад. Синтаксис и соответствующий шаблон кода таковы: WHILE expression DO LO code(expression) Bcond LI StatSequence code(StatSequence) END BR LO LI Отсюда мы выводим соответствующую расширенную процедуру синтаксичес- кого анализа: ELSIF sym = while THEN Get(sym); L := pc; expression(x); Cjump(x); IF sym = do THEN Get(sym) ELSE MarkC'DO?") END ; StatSequence; Bjump(L); Fixup(x.a); IF sym = end THEN Get(sym) ELSE Markf'END?") END PROCEDURE BJump(L: LONGINT); BEGIN Put(BEQ, 0, 0, L - pc) END BJump В заключение приведем сгенерированный код для двух операторов с перемен- ными i и): IF i < j THEN i := j THEN i := 1 ELSE i := 2 END ; WHILE i > 0 DO i := i - 1 END 4 LDW 0, base, -4 i 8 LDW 1, base, -8 J 12 CMP 0, 0, 1 16 BGE 3 (переход через 3 команды на 28) 20 STW 0, base, -4 i := 0 24 BEQ 10 (переход через 10 команды на 64) 28 LDW 0, base, -4 32 LDW 1, base, -8 36 CMP 0, 0, 1
Логические операции 101 40 BNE 4 (переход через 4 команды на 56) 44 MOVI 0, 0, 1 48 STW 0, base, -4 i := 1 52 BEQ 3 (переход через 3 команды на 64) 56 MOVI 0, 0, 2 60 STW 0, base, -4 1 := 2 64 LDW 0, base, -4 68 BLE 5 (переход через 5 команд на 88) 72 LDW 0, base, -4 76 SUBI 0, 0, 1 80 STW 0, base, -4 i := 1 - 1 84 BEQ -5 (переход назад через 5 команд на 64) 88 11.3. Логические операции Конечно, было бы заманчиво обрабатывать логические выражения таким же спо- собом, как и арифметические. Но, к сожалению, это во многих случаях приводит не только к неэффективному, но даже к неправильному коду. Причина кроется в определении логических операций, а именно: р OR q = if р then TRUE else q p&q = if p then q else FALSE Это определение предполагает, что не обязательно вычислять второй операнд q, если результат однозначно определяется значением первого операнда р. Опре- деления языков программирования идут даже дальше, утверждая, что в таких случаях второй операнд вообще не должен вычисляться. Подобное правило ус- танавливается для того, чтобы второй операнд можно было оставить неопреде- ленным, дабы в противном случае не вызвать аварийного завершения программы. Самый распространенный пример использования указателя х: (x# NIL) & (xA.size > 4) Поэтому логические выражения с логическими операциями принимают вид условных операторов (или, точнее, условных выражений) и, следовательно, к ним применимы те же самые методы компиляции, что и для условных операторов. Как показывает следующий пример, логические выражения и условные операторы объединяются. Оператор IF (х <= у) & (у < z) THEN S END компилируется так же, как его эквивалент IF х <= у THEN IF у < z THEN S END END С целью получения подходящего шаблона кода рассмотрим сначала следую- щее выражение, содержащее три отношения, связанные операцией &. Мы опреде- ляем желаемый шаблон кода таким, как показано ниже, принимая во внимание в данный момент только левый шаблон. Идентификаторы а, Ь.f обозначают
102 Условные и циклические операторы и логические выражения числовые значения. Метки Т и F обозначают адреса назначения истинной и лож- ной ветвей соответственно. (а < Ь) & (с < d) & (е < f) CMP a, b CMP a, b BGEF BGE F CMP c, d CMP c, d BGEF BGE F CMP e, f CMP e, f BGEF BLTT (T) (F) Рис. 11.1. Шаблоны кода для логической операции & В левом шаблоне команда условного перехода выдается для каждой операции &. Переход выполняется, если предшествующее ему условие ложно (F-переход). Это происходит по команде BGE - для отношения <, BNE - для отношения = и т. д. Если мы рассмотрим задачу генерации требуемого кода, можем увидеть, что процедура синтаксического анализа term, которая, как известно, обрабатывает арифметические слагаемые, должна быть слегка расширена. В частности, перед обработкой второго операнда должна быть выдана команда перехода, а по ее окон- чании должен быть закреплен адрес перехода. Первая задача решается процеду- рой Ор1, последняя - процедурой Ор2. PROCEDURE term(VAR х: Item); VAR у: Item; op: INTEGER; BEGIN factor(x); WHILE (sym >= times) & (sym <= and) DO op := sym; Get(sym); IF op = and THEN OpUop, x) END ; factor(y); Op2(op, x, y) END END term; PROCEDURE OpUop; 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 -~ n END ' ~ Pc~' END Opl;
Логические операции 103 Если первый логический операнд представляется элементом х с режимом Cond, то в текущем положении х есть TRUE, и, значит, следом должны идти коман- ды вычисления второго операнда. Однако если элемент х не находится в режиме Cond, его нужно перевести в этот режим. Эта задача выполняется процедурой loadBooI. Мы предполагаем, что значение FALSE представляется как 0. Тогда, если х есть 0, а значение атрибута с = 1, активируется команда BEQ. PROCEDURE loadBool(VAR х: Item); BEGIN IF x.type.form # Boolean THEN OSS.MarkC'Boolean?") END ; load(x); x.mode := Cond; x.c := 1 END loadBool; Операция OR обрабатывается аналогично с той лишь разницей, что переходы выполняются, когда соответствующие им условия истинны (Т-переход). Коман- ды приведены в двойном списке со ссылками на поле b элемента кода. Постусло- вием последовательности операндов, связанных операцией OR, будет FALSE. Сно- ва рассмотрим только левый столбец шаблона кода: (а < b) OR (с < d) OR (е < f) CMP a, b CMP a, b BLTT BLTT CMPc, d CMP c, d BLTT BLTT CMP e, f CMP e, f BLTT BGEF (F) CD Рис. 11.2. Шаблоны кода для логической операции OR Теперь рассмотрим реализацию операции отрицания. Оказывается, что в рам- ках представленной схемы здесь вообще не нужно выдавать никаких команд. Долж- но быть только инвертировано значение условия в поле с элемента кода и перестав- лены местами списки F-переходов и Т-переходов. Результат операции отрицания для обоих выражений с операциями & и OR показан в правых колонках шаблонов кода на рис. 11.1 и 11.2. Необходимые процедуры расширяются следующим обра- зом: PROCEDURE SimpleExpression(VAR х: Item); VAR у: Item; op: INTEGER; BEGIN term(x); WHILE (sym >= plus) & (sym <= or) DO op := sym; Get(sym);
104 Условные и циклические операторы и логические выражения IF op = or THEN Opl (op, x) END ; term(y); Op2(op, x, y) END END SimpleExpression; PROCEDURE Opl (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 Opl; При компиляции выражений с операциями & и OR нужно следить за тем, чтобы перед каждой операцией & выполнялось условие Р, а перед каждой OR выполня- лось ~Р. Соответствующие операциям списки команд перехода (Т-список для &, F- список для OR) должны быть просмотрены, а адреса переходов в них закреплены. Это делается при вызовах процедуры FixLink в Opl. В качестве примеров рассмот- рим выражения (а < Ь) & (с < d)) OR ((е < f) & (g < h) (a < b) OR (c < d)) & ((e < f) OR (g < h) и соответствующие им коды: СМР а, b BGE F0 СМР с, d BLTT F0 СМР е, f BGE F СМР g, h BGE F CD CMP a, b BLTTO CMPc, d BGE F TO CMP e, f BLTT CMP g, h BGE F (D Рис. 11.3. Шаблоны кода для логических операций & и OR
Присваивание логическим переменным 105 Может так случиться, что список для подвыражения соединяется со списком для всего выражения (см. F-ссылку в шаблоне для & на рис. 11.3). Это соединение выполняется процедурой merged(a, b), выдающей в качестве результата конкате- нацию списков своих аргументов. Она вызывается в процедуре Ор2. PROCEDURE Ор2(ор: INTEGER; VAR х, у: Item); (* х := х ор у *) 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 loadBooKy) 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. Это достигается следующим шаблоном кода: Т ADDI 0, 0, 1 BEQ L F ADDI 0, 0, О I STW 0, q Из-за этого простое присваивание q := х < у превращается в ужасно длинную последовательность команд. Однако следует отдавать себе отчет в том, что логи- ческие переменные (обычно называемые флагами) встречаются (должны встре- чаться) нечасто, хотя в действительности тип BOOLEAN относится к фундамен- тальным. Было бы неправильно добиваться оптимальной реализации редко используемых операций ценой усложнения программы. В то же время очень важно, чтобы часто встречающиеся случаи обрабатывались наилучшим спо- собом. Тем не менее мы обрабатываем присваивания логическому элементу, не нахо- дящемуся в режиме Cond, как особый случай, а именно как обычное присваивание без использования переходов. Следовательно, присваивание р := q дает в резуль- тате ожидаемую последовательность кодов LDW1.0, q STW 1,0, р
106 Условные и циклические операторы и логические выражения Поэтому процедура Store становится такой: PROCEDURE Store(VAR х, у: Item); (* х := у *) 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 МагкСнеправильное присваивание") END ; EXCL(regs, x.r); EXCL(regs, y.r) END Store; 11.5. Упражнения 11.1. Превратите язык Оберон-О в его вариант Оберон-D, переопределив услов- ный оператор и оператор цикла следующим образом: statement = ... "IF" guardedStatements {"Г guardedStatements) "Fl" | "DO" guardedStatements {"|" guardedStatements) ”OD". guardedStatements = condition statement statement). Новая форма оператора IF Bo-So I B,.S, | ... I Bn.Sn Fl будет означать, что из всех истинных условий (логических выражений) В, выбира- ется наугад одно, и выполняется соответствующая ему последовательность опе- раторов S|. Если ни одно из условий не истинно, выполнение программы прекра- щается. Любая последовательность операторов S, будет выполняться только в том случае, если соответствующее условие В| истинно. Поэтому говорят, что В| являет- ся предохранителем S|. Оператор DO B0.S0I B,.S, | ... | B„.Sn OD будет означать, что пока среди условий В, есть истинные, случайным образом вы- бирается одно из них, и выполняется соответствующая ему последовательность операторов S,. Этот процесс завершается, как только все В| оказываются ложными. И здесь В| играют роль предохранителя. DO-OD - циклический недетерминиро- ванный оператор. Внесите в компилятор необходимые изменения. 11.2. Добавьте в Оберон-О и в его компилятор оператор FOR: statement = [assignment 1 ProcedureCall | IfStatement | Whilestatement | ForStatement. ForStatement = "FOR" identifier ":=" expression "TO” expression ["BY" expression] "DO" Statementsequence "END".
Упражнения -— ---------------------------------------------107 Выражение, предшествующее символу ТО, задает начальное, а следующее а» н„м - конечное значение переменной-счетчика, обозначенной идентифттоДм Выражение после BY задает шаг счетчика. В случае его отсутствия значениеX' равно 1. 11.3. Обдумайте реализацию оператора CASE в Обероне (см. приложение А2) Его основное свойство состоит в том, что он использует таблицу адресов перехода да вариантов и индексируемую команду перехода.

Глава 12 Процедуры и концепция локализации Л ' - . г- .......... 4-1U : <?ЯAgaet-ai.Mp огроменных... 112 ' ,i? f&pavEif.w ... ..114./ - - W вызовы Ш'-; . ‘> . иэ процедуры . 121 > ' м5 pftOiHvv/pu-функций...122/ ........ Ш...,
110 Процедуры и концепция локализации 12.1. Организация памяти во время выполнения Процедуры, которые также иногда называют подпрограммами, являются, воз- можно, самым важным инструментом для структурирования программ. Так как они встречаются часто, необходимо добиться, чтобы их реализация была эффек- тивной. Реализация основана на команде перехода, которая сохраняет текущее значение PC и, таким образом, точку возврата после завершения процедуры, когда это значение восстанавливается в регистре PC. Сразу возникает вопрос о том, где именно должен сохраняться адрес возврата. Во многих компьютерах он хранится в регистре, и мы тоже приняли такое реше- ние в нашем RISC-процессоре. Это гарантирует предельную эффективность, так как исключаются дополнительные обращения к памяти. Но необходимость сохра- нять значение регистра в памяти перед следующим вызовом процедуры неизбеж- на, так как иначе старый адрес возврата будет затираться, вследствие чего адрес возврата первого вызова будет утерян. В реализации компилятора это значение регистра связи должно сохраняться в начале каждого вызова процедуры. Очевидное решение для хранения связи - стек, по той причине, что активации процедур оказываются вложенные, причем процедуры завершаются в порядке, обратном их вызовам. Поэтому память для адресов возврата должна действовать согласно принципу LIFO (Last-In First-Out: последним вошел - первым вышел), что приводит к фиксированным кодовым последовательностям в начале и в конце каждой процедуры. Их называют прологом и эпилогом процедуры. Здесь мы будем использовать R1 3 в качестве указателя стека SP и R14 в качестве регистра связи LNK. R1 5 определяется как счетчик команд PC. Вызов BSR Р перейти к подпрограмме Пролог Р PSH LNK, SP, 4 LNK - в стек Эпилог POP LNK, SP, 4 LNK - из стека RET LNK вернуться Этот шаблон кода верен при условии, что команда BSR помещает адрес возвра- та в R14. Отметим, что это свойство аппаратуры (глава 9), тогда как использова- ние R13 в качестве указателя стека - лишь программное соглашение, установлен- ное проектом компилятора или базовой операционной системой. При каждом запуске системы R14 должен получать значение указателя на область памяти, от- веденную под стек. Алгол-60 ввел фундаментальное понятие локальных переменных. Оно означа- ло, что каждый объявленный идентификатор имел ограниченную область види- мости и существования. В Паскале (а также в Обероне) такой областью служит тело процедуры. Строго говоря, переменные могут быть объявлены локальными в процедуре таким образом, что они видимы и существуют только внутри этой процедуры. Отсюда следует, что для таких локальных переменных память авто- матически выделяется при входе в процедуру и освобождается по завершении
Организация памяти во время выполнения 111 процедуры. Поэтому локальные переменные разных процедур могут разделять одну и ту же область памяти, но, конечно, не одновременно. На первый взгляд кажется, что эта схема наносит определенный ущерб эффек- тивности механизма вызова процедуры. Однако, к счастью, это не так, потому что блоки памяти для наборов локальных переменных, как и для адресов возврата, могут выделяться по принципу стека. В сущности, адрес возврата тоже можно считать (скрытой) локальной переменной, и вполне естественно использовать один и тот же стек для переменных и адресов возврата. Эти блоки памяти называ- ют записями активации процедуры (procedure activation records), или кадрами ак- тивации (activation frames). Освобождение блока по завершении процедуры дос- тигается просто установкой указателя стека в его значение до вызова процедуры. Следовательно, выделение и освобождение локальной памяти оптимально эф- фективно. Адреса локальных переменных, генерируемых компилятором, всегда назнача- ются относительно базового адреса соответствующего кадра активации. Так как впрограммах большинство переменных - локальные, их адресация также должна быть высокоэффективной. Это достигается за счет резервирования регистра для хранения базового адреса и использования того факта, что исполнительный адрес равен сумме значения регистра и поля адреса команды (режим относительной ре- гистровой адресации, register relative addressing mode). Зарезервированный ре- гистр называют указателем кадра (frame pointer, FP). Эти соображения учтены в следующем прологе и эпилоге, где R12 исполняет роль указателя кадра: Пролог Р PSH LNK, SP, 4 PSH FP, SP, 4 MOV FP, О, SP SUBI SP, SP, n LNK - в стек (push link) FP - в стек FP := SP SP := SP - n (n = размер кадра) Эпилог MOV SP, 0, FP SP := FP POP FP, SP, 4 FP - из стека (pop FP) POP LNK, SP, 4 LNK - из стека RET LNK вернуться Кадры активации последовательно вызываемых проце- дур связываются в список своими базовыми адресами. Список называется динамической связью (dynamic link), по- тому что он отражает динамическую последовательность активаций процедур. Его начало находится в регистре-ука- зателе кадра FP (см. рис. 12.1). Состояние стека до и после вызова процедуры показано на рис. 12.2. Отметим, что эпилог возвращает стек в его первоначальное состояние, удаляя адрес возврата и начало списка динамической связи. Если мы внимательно рассмотрим необходимость двух указателей SP и FP, то можем прийти к выводу, что FP на са- мом деле лишний, потому что смещения адресов перемен- в стеке
112 Процедуры и концепция локализации Рис. 12.2. Состояние стека до и после вызова процедуры ных могли бы вычисляться относительно SP вместо FP. Однако это предположение верно, если размеры всех переменных известны во время компиляции. В случае от- крытых (динамических) массивов это не так, что станет видно позже. Но очевидно, что сохранение второго указателя (FP) требует при каждом вызове процедуры и возврате из нее дополнительных обращений к памяти, которые нежелательны. Для повышения эффективности и, в частности, для уменьшения длины цепо- чек команд как пролога, так и эпилога компьютеры с более сложными командами имеют специальные команды, соответствующие прологу и эпилогу. Подтвержде- нием тому - следующие два примера; второй имеет специальные регистры, пред- назначенные для указателей SP и FP. Однако количество необходимых команд ос- тается тем же. Motorola 680x0 National Semiconductor 32x32 Вызов BSRP BSR P Пролог LINK DI 4, n ENTER n Эпилог UNLNK DI 4 EXIT Возврат RTD RET 12.2. Адресация переменных Напомним, что локальная переменная адресуется относительно базового адреса кадра активации, содержащего данную локальную переменную, а базовый адрес содержится в регистре FP. Последний, однако, предназначается только для после- днего активного кадра и, следовательно, только для тех переменных, которые от- носятся к процедуре, из которой к ним обращаются. Во многих языках програм- мирования объявления процедур могут быть вложенными, что позволяет обращаться к переменным, которые локальны в некоторой процедуре, но не в той, где к ним обращаются. Следующий пример демонстрирует ситуацию, когда про- цедура R локальна в Q, a Q и S локальны в Р: PROCEDURE Р; VAR х: INTEGER; Объект Уровень Р О х 1
Адресация переменных 113 PROCEDURE Q; VAR у: INTEGER; Q У PROCEDURE R; VAR z: INTEGER; BEGIN x := у + z 2 3 END R; BEGIN R ENO Q; PROCEDURE S; BEGIN Q ENDS; BEGIN Q; S END P; Проследим цепочку вызовов P —> Q —» R. Заманчиво предположить, что при об- щении к переменным х, у или z в R их базовый адрес может быть получен лереме- иием по динамической цепочке. Число шагов при этом было бы разностью между уровнями обращения и объявления переменной. Эта разность равна 2 для х, 1 для у, г. Но это предположение неверно. В процедуру R можно также попасть по мочке вызовов Р —> S —> Q —» R, как показано на рис. 12.3. И тогда обращение переменной х за два шага привело бы нас к кадру активации S вместо Р. статическая связь динамическая связь Рис. 12.3, Динамические и статические связи в стеке Очевидно, что необходим второй список записей активации, который отража- пептический порядок вложенности, а не динамический порядок вызовов. Сле- Штельно, при каждом вызове процедуры должна создаваться вторая ссылка. Теперь в дополнение к адресу возврата и динамической связи так называемый айлроцедуры (procedure mark) содержит элемент статической связи (staticIM). Статическая ссылка процедуры Р указывает на запись активации процедуры, со- держащей Р, то есть процедуры, в которой Р объявлена локально. Необходимо от-
114 Процедуры и концепция локализации метить, что этот указатель не нужен для глобальных процедур, потому что по- вальные переменные адресуются непосредственно, то есть без использования ба- зового адреса. И хотя этот случай типичен и большинство процедур объявляются глобально, дополнительная сложность, вызываемая наличием статической связи, вполне приемлема. С некоторым допущением абсолютная адресация глобальни переменных может считаться особым случаем адресации локальных переменных приводящей к повышению эффективности. Наконец, отметим, что доступ к переменным посредством списка статически связей (переменных промежуточного уровня) менее эффективен, чем доспи к строго локальным переменным, поскольку каждый шаг по списку требует до- полнительных обращений к памяти. Для устранения потери эффективности был предложено и реализовано несколько решений. В конечном счете все они предпо- лагают отображение статического списка в множество базовых регистров. Мн считаем это оптимизацией «не в том месте». Во-первых, регистры - это ограни- ченный ресурс, которым не следовало бы так легко разбрасываться. А во-вторых, затраты на копирование элементов-связок в регистры при каждом вызове и воз- врате могут оказаться несколько дороже экономии, в частности потому, что обра- щения к переменным промежуточного уровня на практике встречаются весьма редко. Поэтому такая оптимизация может обернуться полной своей противопо- ложностью. В целях простоты и удобочитаемости компилятора Оберон-О, текст которого приведен в приложении С, обработка локальных переменных промежуточного уровня в нем не реализована. Глобальные переменные имеют фиксированные адреса, которые тожеследова- ло бы рассматривать относительно базы кадра. Их абсолютные значения опреде- ляются на этапе загрузки кода, то есть после компиляции, но до выполнения про- граммы. Следовательно, выдаваемый при компиляции объектный код мог бы сопровождаться списком адресов команд, которые обращаются к глобальным пе- ременным. Загрузчик должен добавить к этим адресам базовый адрес соответ- ствующего кадра глобальных переменных. Эта операция закрепления адресов может быть опущена, если компьютер имеет в качестве счетчика команд регистр адреса. Наш RISC-процессор именно таков, предоставляя доступ к PC посред- ством R15. Кадр глобальных переменных размещается непосредственно перед кадром кода. Следовательно, адреса глобальных переменных используют R15 в качестве базового адреса, и из смещения переменной должен вычитаться адрес текущей команды. 12.3. Параметры Параметры определяют интерфейс между вызывающей и вызываемой процеду- рами. Говорят, что параметры на вызывающей стороне фактические, а на вызыва- емой - формальные. Последние - в действительности только заглушки, вместо которых подставляются фактические параметры. По существу, подстановка-это присваивание фактического значения формальной переменной. Это значит, что
Параметры 115 каждый формальный параметр представлен переменной, связанной с процеду- рой, и каждый вызов процедуры сопровождается рядом присваиваний, называе- мых подстановкой параметров. В большинстве языков программирования параметры подразделяются, по крайней мере, на два вида. Первый - параметр-значение (параметр, передавае- мый по значению'), когда формальной переменной присваивается, согласно назва- нию, значение фактического параметра. Фактический параметр синтаксически представлен выражением. Второй вид параметра - параметр-ссылка (параметр, передаваемый по ссылке), когда формальной переменной присваивается, также согласно названию, ссылка на фактический параметр. Очевидно, что фактический параметр в этом случае должен быть переменной, потому что разрешается присва- инание формальному параметру, и это присваивание должно относиться к факти- ческой переменной. (Поэтому в Паскале, Модуле и Обероне параметр-ссылка на- зывается параметром-переменной.) Значение формальной переменной в этом случае является скрытым указателем, то есть адресом. Конечно, фактический параметр перед подстановкой должен быть вычислен. В случае параметров-переменных вычисление значения принимает форму иден- тификации переменной, означая, например, вычисление индекса для индексиро- ванных переменных. Но как определить адрес назначения такой подстановки? Здесь вступает в игру стековая организация памяти. Фактические значения про- сто последовательно помещаются в вершину стека; никаких явных адресов назна- чения не требуется. Рисунок 12.4 показывает состояние стека после размещения параметров, а также после вызова и пролога. Рис. 12.4. Подстановка параметров Теперь становится очевидным, что параметры могут адресоваться относи- тельно адреса кадра FP подобно локальным переменным. Но если локальные переменные имеют отрицательные смещения, то параметры имеют смещения по- ложительные. Особенно ценно то, что вызванная процедура обращается за пара- метрами именно туда, куда они были помещены вызывающей процедурой. Выде-
116 Процедуры и концепция локализации ленное для параметров пространство освобождается в эпилоге просто увеличени- ем значения SP. Эпилог MOV SP, 0, FP POP FP, SP, 4 POP LNK, SP, m + 4 RET LNK SP := FP FP - в стек параметры и LNK - в стек вернуться В случае CISC-компьютеров с прологом и эпилогом, представленными специ- альными командами, необходимое увеличение SP выполняется командой возвра- та, имеющей в качестве своего аргумента размер блока параметров (RET m). 12.4. Объявления и вызовы процедур Процедура для обработки объявлений процедур легко выводится из синтаксиса с помощью правил построения синтаксического анализатора. Новая записьвтаб- лице символов, сгенерированная для объявления процедуры, получает значение класса Proc, а ее атрибут а - текущее значение рс, адрес входа в пролог процедуры. После чего в таблице символов открывается новая область действия так, чтоб» (1) новые записи для локальных объектов автоматически попадали в эту область и (2) в конце процедуры локальные объекты легко удалялись, снова открывай предыдущую область действия. Здесь две процедуры OpenScope и CloseScope тоже воплощают принцип стека, а связь устанавливается в заголовочном элемен- те (класс Head, поле dsc). Объекты получают дополнительный атрибут lev, озна- чающий уровень вложенности объявленного объекта. Рассмотрим следующие объявления: CONST N = 10; VAR х: Т; PROCEDURE Р(х, у: INTEGER); ... Получающаяся таблица символов изображена на рис. 12.5. Указатель dscссы- лается на параметры х и у процедуры Р. PROCEDURE ProcedureDecI; 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); ldentList(Par, first) ELSE ldentList(Var, first) END ; IF sym = ident THEN find(obj); Get(sym); IF obj.class = Typ THEN tp := obj.type ELSE МагкС'тип?"); tp := IntType
Объявления и вызовы процедур 117 Рис. 12.5. Таблица символов, представляющая две области действия END ELSE Магк("идентификатор?"); 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 (* ProcedureDed *) Get(sym); IF sym = ident THEN procid := id; NewObj(proc, Proc); Cet(sym); parblksize := 8; INC(level); OpenScope; proc.val := -1; IF sym = Iparen 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(T) 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
118 Процедуры и концепция локализации I END ; proc.dsc:»topScope.next; IF sym = semicolon THEN Cet(sym) ELSE Mark(";?") END; locblksize := 0; declarations(locblksize); WHILE sym = procedure DO ProcedureDed; IF sym = semicolon THEN Get(sym) ELSE MarkC';?") END END ; proc.val := pc; Enter(locblksize); IF sym = begin THEN Get(sym); StatSequence END ; IF sym = end THEN Get(sym) ELSE MarkC'END?") END ; IF sym = ident THEN IF procid # id THEN МагкС'не подходит") END ; Get(sym) ELSE Магк("идентификатор?") END ; Return(parblksize - 8); CloseScope; DEC(level) END END ProcedureDed; Внутри тела процедуры параметры-значения обрабатываются как локальные переменные. Их записи в таблице идентификаторов имеют класс Var. Для пред- ставления параметров-ссылок вводится новый класс Par. Адреса (смещения) фор- мальных параметров определяются по следующей формуле, где последний пара- метр р„ получает наименьшее смещение, равное размеру следа процедуры (8). Размер параметров-ссылок всегда 4 и совпадает с размером адреса. adr(Pi) = size(pl+i) + ... + size(pn) + 8 К сожалению, это значит, что смещения не могут быть определены, пока» будет распознан весь список параметров. Более того, в случае байт-адресуемой памяти всегда выгодно увеличивать или уменьшать указатель стека на величии, кратную 4, чтобы параметры всегда выравнивались по границе слова. В случае Оберона-О нет необходимости обращать особое внимание на это правило, потому что все типы данных имеют размер, кратный 4. Локальные объявления обрабатываются процедурой синтаксического анали- затора declarations. Код для пролога выдается процедурой Enter после обработки локальных объявлений. Выдача эпилога осуществляется процедурой Return s конце ProcedureDed. PROCEDURE Enter(size: LONGINT); BEGIN PutfPSH, LNK, SP, 4); Put(PSH, FP, SP, 4); Put(MOV, FP, 0, SP); PutfSUBI, SP, SP, size) END Enter; PROCEDURE Return(size: LONGINT); BEGIN
Объявления и вызовы процедур 119 Put(MOV, SP, О, FP); Put(POP, FP, SP, 4); Put(POP, LNK, SP, size+4); PutBR(RET, LNK) END Return; Процедура Makeitem генерирует элемент типа Item, соответствующий данно- му объекту Object. Здесь нужно принять во внимание различие между адресацией .шкальных и глобальных переменных. (Как уже упоминалось, обработка пере- менных промежуточного уровня здесь не выполняется.) Отметим, однако, что па- раметры-ссылки (class = Par) требуют косвенной адресации. Так как архитектура RISC не имеет явного режима косвенной адресации, значение формального пара- метра, то есть адрес фактического, загружается в регистр. Тогда к фактическому параметру обращаются по этому регистру со смещением 0. PROCEDURE Makeltem(VAR х: Item; у: Object); VAR г: 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 МагкС'уровень!"); 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 := О END END Makeitem; Вызовы процедур генерируются в уже встречавшейся процедуре StatSequence с помощью вспомогательных процедур Parameter и Call: IF sym = ident THEN find(obj); Get(sym); Makeltem(x, obj); selector(x); IF sym = becomes THEN ... ELSIF x.mode = Proc THEN par := obj.dsc; IF sym = Iparen THEN Cet(sym); IF sym = rparen THEN Get(sym) ELSE LOOP expression(y); IF IsParam(par) THEN Parameter(y,par.type,par.dass); par := par.next ELSE МагкС'слишком много параметров") END ; IF sym = comma THEN Get(sym) ELSIF sym = rparen THEN Get(sym); EXIT ELSIF sym >= semicolon THEN Mark(")?"); EXIT ELSE MarkO или , ?") END END END END; IF obj.val < 0 THEN МагкС'вызов вперед")
120 Процедуры и концепция локализации j ------------------------------------------------------------------------1 ELSIF -IsParam(par) THEN Call(x) ; ELSE Магк("слишком много параметров") END PROCEDURE Parameter(VAR x: Item; ftyp: Type; class: INTEGER); i 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 i ELSE МагкС'неправильный режим параметра") 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 Магк("неверный тип параметра") END END Parameter; PROCEDURE lsParam(obj: Object): BOOLEAN; BEGIN RETURN (obj.class = Par) OR (obj.class = Var) & (obj.val > 0) END IsParam; PROCEDURE CalKVAR x: Item); BEGIN PutBR(BSR, x.a - pc) END Call; Здесь мы молчаливо полагаем, что адреса входов в процедуры уже известны, когда компилируется вызов. Таким образом, мы заранее исключаем ссылки впе- ред, которые могут возникать, например, в случае взаимных рекурсивных обра- щений. Если снять это ограничение, то адреса вызовов (переходов) вперед доЖ ны быть сохранены, чтобы закрепить адреса в командах переходов, когда их меси назначения станут известными. Этот случай подобен закреплению переходов впе- ред в условных и циклических операторах. В заключение приведем код, сгенерированный для следующей простой проце- дуры: PROCEDURE Р(х: INTEGER; VAR у: INTEGER); BEGIN х := у; у := х; Р(х,у); Р(у,х) END Р; 0 PSH LNK, SP, 4 пролог 4 PSH FP, SP, 4 8 MOV FP, О, SP 12 SUB1 SP, SP, 0 нет локальных переменных
Стандартные процедуры 121 16 LDW О, FP, 8 20 LDW 1, О, О 24 STW 1, FP, 12 х ;= у 28 LDWO, FP, 8 32 LDW1, 0, 12 36 STW 1, FP, 12 у ;=x 40 LDWO, FP, 12 X 44 PSH 0, SP, 4 48 LDWO, FP, 8 adr(y) 52 PSH 0, SP, 4 56 BSR -14 P(x,y) 60 LDWO, FP, 8 64 LDW1, 0, 0 У 68 PSH 1, SP, 4 72 ADDI 0, FP, 12 adr(x) 76 PSH 0, SP, 4 80 BSR -20 P(y,x) 84 MOVSP, 0, FP эпилог 88 POP FP, SP, 4 92 POP LNK, SP, 12 связь и параметры - из стека 96 RET LNK 12.5. Стандартные процедуры большинство языков программирования содержат некоторые процедуры и функ- ции, не требующие объявления в программе. Они, как говорят, предопределены и могут быть вызваны отовсюду, так как всюду видимы. Это такие хорошо извест- ные функции, как, например, абсолютная величина числа (ABS), преобразования типа (ENTIER, ORD) или часто встречающиеся операторы, которые получили аббревиатуры и доступны па многих компьютерах как особые команды (INC, DEC). Общее свойство всех этих так называемых стандартных процедур состоит в том, что им соответствует либо всего одна команда, либо короткая последова- тельность команд. Поэтому эти процедуры обрабатываются компиляторами со- всем по-другому; вызов для них не генерируется, вместо этого необходимые ко- манды вставляются прямо в код. Такие процедуры называются встраиваемыми (in-line). Этот термин обретает смысл только тогда, когда становится понятным Лежащий в его основе способ реализации. Следовательно, стандартные процедуры выгодно считать отдельным классом объектов. Отсюда сразу становится очевидной необходимость специальной обра- ботки их вызовов. Для Оберона-О мы определяем процедуры Read, Write, WriteHexn H'lteLn, которые, с одной стороны, вводят элементарные средства ввода-вывода, асдругой - служат для демонстрации предлагаемой обработки предопределенных
122 Процедуры и концепция локализации процедур. В таком случае термин «стандартная» - скорее всего, заблуждение,тогда как «предопределенная» и «встраиваемая» отражают суть дела. При инициализа- ции компилятора соответствующие встраиваемым процедурам записи занося™ в таблицу символов, а именно в самую внешнюю область видимости, называемую вселенной, которая всегда остается открытой (см. приложение С). Новый атрибут класса обозначается SProc, а атрибут val (а для Items) задает нужную процедуру. IF sym - ident THEN find(obj); Cet(sym); Makeltem(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); Testlnt(y) END ; lOCalKx, 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 lOCall; В заключительном примере приводятся последовательность из трех операто- ров и итоговый код: Read(x); Write(x); WriteLn 4 READ 0, 0, 0 8 STW 0, 0, -4 x 12 LDW 0, 0, -4 x 16 WRD 0, 0, 0 32 WRL 0, 0, 0 12.6. Процедуры-функции Процедура-функция - это процедура, идентификатор которой обозначает одно- временно и алгоритм, и его результат. Она активируется не оператором вызова, а операндом выражения. Поэтому вызов процедуры-функции должен позабо- титься еще и о возврате результата. Таким образом, возникает вопрос, какие ре- сурсы следует использовать. Если основная наша цель - генерация эффективного кода с минимальным чис- лом обращений к памяти, то регистр - первый кандидат для временного хранения
Упражнения 123 результата функции. Если принимается такое решение, мы должны отказаться от возможности определения функций со структурным результатом, поскольку структурные значения не могут храниться в регистре. Если же такое ограничение считается неприемлемым, то в стеке должно отво- зиться место для размещения структурного результата. Обычно оно добавляется (области параметров кадра активации. Результат функции считается неявным ираметром-переменной. В связи с этим SP увеличивается перед тем, как выдается год для первого параметра. Итак, все понятия, содержащиеся в языке Оберон-О и реализованные в его гомпиляторе, рассмотрены. Полный текст компилятора приводится в приложе- ние. 12.7. Упражнения 12.1. Усовершенствуйте компилятор Оберон-О таким образом, чтобы требование прогой локальности переменных или их полной глобальности могло быть снято. 12.2. Добавьте в компилятор Оберон-О стандартные функции, генерирующие «страиваемый код. Рассмотрите ABS, INC, DEC. 12.3. Замените понятие VAR-параметра понятием OUT-параметра. ООТ-пара- иетрпредставляет собой локальную переменную, значение которой присваивает- ся соответствующему ей фактическому параметру при выходе из процедуры. Он является антиподом параметра-значения, когда значение фактического парамет- ра присваивается формальной переменной при входе в процедуру.

Глава 13 Элементарные типы данных
126 Элементарные типы данных 1S.1. Типы REAL и LONGREAL Еще в 1957 году в Фортране целые и вещественные числа считались различными типами данных. Так было не только потому, что для них нужны были различные внутренние представления, но и потому, что по общему признанию программист должен был осознавать, когда можно ждать точных расчетов (а именно для целых чисел), а когда - только приближенных. То, что с помощью вещественных чисел может быть получен лишь приближенный результат, может быть понято с учетом того, что вещественные числа представлены масштабируемыми целыми числами с фиксированным, конечным количеством цифр. Их тип называется REAL, а дей- ствительное значение х представляется парой целых чисел е и т, как определено уравнением х = Be w х т, где 1 < т < В Это так называемое представление с плавающей точкой', е называют показате- лем степени, m - мантиссой. Основание В и смещение w являются фиксированны- ми для всех значений REAL и характеризуют выбранное представление числа. Два стандарта IEEE представлений с плавающей точкой устанавливают следующие значения В и w, а к компонентам е и m добавляется бит s для знака: Тип REAL LONGREAL В w Число битов для е Число битов для m Общее число 2 127 8 23 32 2 1023 11 52 64 Точный вид этих двух типов, названных в Обероне REAL и LONGREAL, опре- деляется следующими формулами: х = (-1 )s х 2е'127 х 1 .т х = (-1 )s х 2е-,°23 х 1 .т В следующих примерах приведены представления с плавающей точкой для нескольких чисел: Десятичное 5 е l.m Двоичное Шестнадцатеричное 1.0 0 127 1.0 0 01111111 00000000000000000000000 3F80 0000 0.5 0 126 1.0 0 01111110 00000000000000000000000 3F0O 0000 2.0 0 128 1.0 0 10000000 00000000000000000000000 4000 0000 10.0 0 130 1.25 0 10000010 01000000000000000000000 4120 0000 0.1 0 123 1.6 0 01111011 10011001100110011001101 3DC CCCD -1.5 1 127 1.5 1 01111111 10000000000000000000000 BFCO 0000 Два примера иллюстрируют случай LONGREAL: 1.0 0 1023 1.0 0 01111111111 00000000 ... 00000000 3FF0 0000 0000 000 0.1 0 1019 1.6 001111111011 10011001 ... 10011010 3FB9 9999 9999 999А Эта логарифмическая форма по существу исключает значение для 0. Значение 0 считается особым случаем и представляется всеми битами, равными 0. Сточки зрения свойств чисел оно представляет собой особый случай разрыва. Крометого, стандарты IEEE устанавливают два дополнительных специальных значения:
Совместимость между числовыми типами данных 127 е=0 (с m t 0) и е = 255 (или е = 1023), которые считаются недопустимыми ре- яьтатами и называются NaN (Not a Number - не число). Обычно программист не должен беспокоиться об этих спецификациях, да и про- яировщика компилятора они тоже не затрагивают. Типы REAL и LONGREAL образуют абстрактные типы данных, обычно интегрированные в аппаратные сред- ою, которые предоставляют набор команд, применяемых к представлению с пла- шицей точкой. Если этот набор полный, то есть охватывает все основные чис- ловые операции, то представление можно рассматривать как скрытое, так как такиедругие программируемые операции от него не зависят. Во многих компь- втерах команды для операций с плавающей точкой используют специальный на- fop регистров. Причина в том, что часто для реализации всех команд с плавающей точкой используются отдельные сопроцессоры, так называемые устройства сшвтцей точкой (floating-point unit, FPU), которые и содержат этот набор реги- стров с плавающей точкой. 13.2. Совместимость между числовыми типами данных Значения всех переменных с числовым типом данных суть числа. Поэтому нет видимой причины не объявить их всех совместимыми при присваиваниях. Но, как tie было сказано, числа разных типов представляются в компьютерах разными мдовательностями битов. Следовательно, всякий раз, когда числотипа 719 при- паивается переменной типа Т1, должно быть выполнено преобразование его Оставления, которое занимает некоторое конечное время. Тогда возникает •рос, нужно ли, дабы не отвлекать внимания, скрывать его от программиста или тиать его явным, так как оно влияет на эффективность программы. Последнее жигается путем объявления различных типов как несовместимых и обеспече- явных предопределенных функций преобразования типа. Влюбом случае, чтобы быть полным, набор команд компьютера должен также сдержать команды преобразования, которые превращают целые числа в числа кивающей точкой, и наоборот. То же самое сохраняется на уровне языка пре- мирования. В Обероне различные типы данных могут использоваться в одном арифмети- ком выражении, которое называется смешанным. В этом случае компилятор Лиен вставить неявные команды преобразования, приводя представление опе- рев к применяемой арифметической операции. Определение Оберона обеспечивает не два, а целое множество числовых типов itHux. Они упорядочены в том смысле, что больший тип содержит все значения, ^надлежащие меньшему типу. Это понятие называют включением типов (type ‘'elusion): SHORT с INTEGER с LONGINT с REALc LONGREAL
128 Элементарные типы данных По определению, тип результата выражения равен большему из типов опери- дов. Этим правилом определяются команды преобразования, которые вставляют- ся компилятором. Несколько более мягкие правила совместимости типов устанавливаются дм присваивания, мягкие по отношению к строгому правилу о том, что тип перемен- ной-приемника должен совпадать с типом выражения-источника. Оберон опреде- ляет, что тип переменной должен включать тип выражения. Таким образом, допу- стимы следующие примеры: VAR i, j: INTEGER; k: LONGINT; x, y; REAL; z: LONGREAL; i:-j; INTEGER g INTEGER k :- i; INTEGER C LONGINT z :« x; REAL c LONGREAL x I; INTEGER c REAL (нет преобразования) (INTEGER в LONGINT) (REAL в LONGREAL) (INTEGER в REAL) Преобразования между целочисленными типами просты и эффективны, так как они состоят только из сдвига знакового бита. Гораздо сложнее преобразова- ния из целого в представление с плавающей точкой. Они состоят из такой норма- лизации мантиссы, чтобы 1 < m < 2, и упаковки знака, показателя степени и ман- тиссы в одно слово. Обычно эта задача решается соответствующей командой. Однако если тип выражения больше типа переменной, то присваивание иногда невозможно, так как присваиваемое значение может быть вне диапазона значе- ний, заданного типом переменной. Поэтому перед преобразованием необходии контроль диапазона значений при выполнении. Такое преобразование должно стать видимым в исходном языке посредством явной функции. Оберон предос- тавляет следующие функции: SHORT INTEGER в SHORTINT LONGINT в INTEGER LONGREAL в REAL ENTIER REAL в LONGINT LONGREAL в LONGINT ENTIER(x) выдает наибольшее целое число, не превосходящее х. В Обероне правила преобразования типа просты и понятны, но таят в себе ло- вушку. Иногда при перемножении двух операндов типа INTEGER(REAL) желатель- но получить результат типа LONGINT (LONGREAL). Однако в присваиваниях k: = i*j + k z: = х * у + z согласно данным правилам, произведение имеет тип INTEGER (REAL), и оно пре- образуется к своему «длинному» типу только при последующем сложении. Чтобы вычислить произведение с большей точностью, перед умножением требуется при- нудительное преобразование: k := LONG(i) * LONG(j) + к z := LONG(x) * LONG(y) + z Простота правил совместимости - огромное достижение проектировщика компилятора. Принцип обработки выражения ясно описан выше. Только про-
Тип данных SET 129 цедуры компиляции для выражений и слагаемых, а также для присваивания дол- жны быть дополнены анализом вариантов. Чем больше разных числовых типов, гем больше вариантов нужно различать. 13.3. Тип данных SET Блоки памяти в компьютерах состоят из небольшого количества битов, которые могут интерпретироваться по-разному. Они могут представлять целые числа со знаком или без знака, числа с плавающей точкой или логические данные. Вопрос «способе введения в языки программирования высокого уровня логических по- следовательностей битов оставался спорным в течение долгого времени. Предло- жение использовать их в качестве множеств принадлежит Хоару [6]. Предложение привлекательно, потому что множество - математически убеди- тельная абстракция. Оно удачно представляется в компьютере своей характерис- тической функцией F. Если х - множество элементов из базового упорядоченного множества М, то F(x) -последовательность логических величин bt со значениями <iсодержится в х». Если мы выберем слово (состоящее из Nбитов) для представ- ления значений типа SET, то базовое множество будет состоять из целых чисел 0,1, .Л-1. Число N обычно тартало, что диапазон значений для типа SET весьма тираничен. Однако основные операции над множествами - пересечение, объеди- нение и разность - чрезвычайно эффективны. Примеры множеств, представлен- ных последовательностями битов слова длиной N: х N-1 ... 7 6 5 4 3 2 1 О {0,2,4,6,...} О ...01010101 {0,3,6,...} 0 ...010 010 0 1 {} 0 ... О О О О О О О О Операции для множеств в Обероне реализуются логическими командами, дос- тупными на каждом компьютере. Это возможно благодаря следующим свойствам ирактеристической функции. Отметим, что мы используем нотацию Оберона ия множественных операций, то есть х+у для объединения и х*у для пересе- чения: ci <(i С х+у ): (i с х) OR (i g у)) ci((i c x‘y): (i ex) 4 (i cy)) ci ((i C x-y) : (i g x) & ~(i c y)) Ci ((i G x/y): (i g x) *, (i g y)) Объединение Пересечение Разность Симметрическая разность Следовательно, команда OR может использоваться для объединения мно- жеств, AND - для пересечения множеств и XOR - для симметрической разности. 8 результате получается очень эффективная реализация, потому что операция •ыполняется над всеми элементами (битами) одновременно (параллельно). При- мерами над базовым множеством {0,1,2,3} являются: |0,1} + {О, 2} = {0, 1, 2} ООП OR 0101=0111 |0,1)‘{0, 2} = {0} 0011& 0101 =0001
130 Элементарные типы данных {0, 1}-{0, 2} = {1} 0011 & ~ 0101 = 0010 {О, 1}/{0, 2} = {1, 2} ООП XOR0101 =0110 В завершение покажем код для множественного выражения (a+b)*(c+d): LDW 1,0, а LDW 2, О, b OR 1,1,2 LDW 2,0, с LDW 3, 0, d OR 2, 2, 3 AND 1,1,2 Проверка членства (включения) / IN х реализуется проверкой бита. Если такой команды нет, цепочка битов должна быть определенным образом сдвинута с по- следующей проверкой знакового разряда. Тип SET особенно полезен, если базовое множество состоит из порядковых номеров набора символов (CHAR). Эффективность в этом случае несколькосни- жается, потому что для представления такого множества обычно требуется 256 би- тов (32 байта). Даже в 32-битовых компьютерах для выполнения операций над таким множеством требуется 8 логических команд.^ 13.4. Упражнения 13.1. Расширьте язык Оберон-О и его компилятор типом данных REAL (и/или LONGREAL) с арифметическими операциями +, -, * и /. RISC-архитектура долж- на быть соответственно расширена рядом команд с плавающей точкой и набором регистров с плавающей точкой. Выберите одну из следующих альтернатив: а. Тип результата операции всегда совпадает с типом операндов. Типы INTEGER и REAL не могут смешиваться. Но при этом существуют две функции преобразования ENTIER(x) и REAL(i). b. Операнды типов INTEGER и REAL (и LONGREAL) могут смешиваться в выражениях. Сравните сложность компиляторов для этих двух вариантов. 13.2. Расширьте язык Оберон-О и его компилятор типом данных SET с опера- циями + (объединение), * (пересечение), - (разность) и с отношением IN (член- ство). Кроме того, следующим дополнительным синтаксисом вводятся конструк- торы множеств. Как вариант,выражения в конструкторах множеств могут ограничиваться константами. factor = number | set |... . set = [element element}]"}". element = expression expression]. 13.3. Расширьте язык Оберон-О и его компилятор типом данных CHAR с фун- кцией ORD(ch) (порядковый номер символа ch в множестве символов) и CHR(k) (k-й символ в множестве символов). Переменная типа CHAR занимает один байт в памяти.
Глава 14 Открытые массивы, указательный и процедурный типы . • кме структуры- •' ли..................• .13$.^ ТИПЕ!.............i36;f.- ! .....а-.,. 13&-
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) Дескриптор с тремя входами, помещенный в стек как пара- метр процедуры Р (рис. 14.1), и соответствующий ему код выг- лядят следующим образом: adr(a) X 10 +♦ 20 +8 Рис. 14.1. Дескриптор для открытого массива MVI 1,0, 20 R1 := 20 PSH 1, 30, 4 len - в стек, R14 = SP ADDI 1,0, 10 R1 := 10 PSH 1, 30, 4 len - в стек ADDI 1,0, а Rl := adr(a) PSH 1, 30, 4 adr - в стек BSR Р call Если открытый параметр-массив передается по значению, его значение долж- но быть скопировано в известное и заранее выделенное место в памяти, как и в случае скалярного значения. Однако эта операция может потребовать значитель- ных затрат, если массив большой. В случае структурных параметров программис- ты должны всегда использовать опцию VAR, если копирование не обязательно. Конечно, код для операции копирования лучше вставить после пролога про- цедуры, а не в точке вызова. Тогда шаблон кода для вызова становится одинако- вым и для параметров-значений, и для параметров-переменных, за исключением того, что для первых операция копирования исключается из пролога. Отведенная под формальный параметр память, очевидно, содержит не массив, а дескриптор массива, размер которого известен. Место для копии выделяется на верхушке стека, а указатель стека увеличивается (или уменьшается) на размер
Динамические структуры данных и указатели 133 массива. В случае многомерных массивов размер вычисляется (при выполнении) как произведение отдельных длин на размер элемента. Здесь SP изменяется при выполнении на величину, которая неизвестна при компиляции. Поэтому в общем случае работать с одним индексным регистром (SP) невозможно; становится необходим указатель кадра FP. 14.2. Динамические структуры данных и указатели Оберон предлагает два вида структур данных: массив (все элементы одного типа, Лородная структура) и запись (разнородная структура). Более сложные структу- рыдолжны программироваться особым образом, то есть они должны создаваться во время выполнения программы, поэтому они называются динамическими. Таким об- разом, компоненты структуры создаются одна за другой; память для каждой из них наделяется отдельно. Они необязательно располагаются в памяти последователь- и. Связи между компонентами задаются явно посредством указателей. Для реализации такой концепции нужно обеспечить механизм выделения па- мяти во время выполнения. В Обероне он представляется стандартной процеду- рой NEW(x). Она выделяет память динамической переменной и присваивает адрес наделенного блока переменной-указателю х. Из этого следует, что указатели суть адреса. Обращение к переменной, на которую ссылается указатель, обязательно косвенное, как в случае с VAR-параметрами. На самом деле VAR-параметр пред- ставляет собой скрытый указатель. Рассмотрим следующие объявления: TYPE Т = POINTER ТО TDesc; TDesc = RECORD х, у : LONGINT END; VAR a, b : T; Код для присваивания а.х := b.y с обращениями по указателям а и b прини- мает вид LDW 1, FP, b RT := b LDW 2, T.y R2 := b.y LDW 3, FP, a R3 := a STW 2, 3, x a.x := R2 Переход от указателя записи к самой записи называется разыменованием. В Обероне явный оператор разыменования обозначается символом л. Очевидно, чтоа.х — это сокращение более явной формы аЛ.х. Неявная операция разыменова- ния распознается, когда символу селектора (точке) предшествует не запись, а ука- затель. Всякий, кто писал программы, которые перегружены действиями с указателя- ми, знает, как легко в них допустить ошибку с катастрофическими последствия- ми. Чтобы объяснить, почему это так, рассмотрим следующие объявления типов: ТО = RECORD х, у : LONGINT END ; Т1 = RECORD х, у, z : LONGINT END;
134 Открытые массивы, указательный и процедурный типы Пусть а и b - переменные-указатели, и пусть а указывает на запись типа ТО, а b - на запись типа Т1. Тогда обозначение a.z указывает на неопределенное зна- чение несуществующей переменной, а присваивание a.z: = Ь.х сохраняет значе- ние в некоторое неопределенное место, возможно, разрушая другую переменную, расположенную по этому адресу. Эта опасная ситуация элегантно устраняется закреплением указателя за опре- деленным типом данных. Это позволяет обеспечить правильность значений ука- зателей во время компиляции без потери эффективности выполнения. Эта блес- тящая идея принадлежит Хоару и впервые реализована в Алголе W [6]. Тип, с которым связан указатель, называется его базовым типом. РО = POINTER ТО ТО; Pl = POINTER ТО Т1; Теперь компилятор может проверить и гарантировать, что переменной-указа- телю р могут быть присвоены только такие значения, которые указывают на пере- менную ее базового типа. Считается, что значение NIL, не указывающее ни на ка- кую переменную вообще, относится ко всем типам указателей. В приведенном примере обозначение a.z теперь становится неверным, потому что z не поле запи- си типа ТО, с которой а связана. Если каждая переменная-указатель инициализи- руется значением NIL, то этого достаточно, чтобы предварять каждое обращение по указателю проверкой его на NIL. В этом случае указатель не указывает ни на одну переменную, и любое выражение с ним должно быть ошибочным. Такая проверка весьма проста, но из-за частого ее применения производитель- ность падает. От необходимости иметь для нее явный шаблон кода можно увиль- нуть (зло)употреблением механизма защиты памяти, имеющегося на многих ком- пьютерах. В этом случае проверка сводится не к сравнению а = NIL, а, скорее, к выяснению, будет ли a.z допустимым незащищенным адресом. Если, как обыч- но, NIL представляется адресом 0 и адреса 0...N-1 защищены, то ошибочные NIL- ссылки ловятся, как только их адресные смещения оказываются меньше N. Тем не менее кажется, что этот метод должен быть вполне приемлемым на практике. Введение указателей требует нового класса объектов в таблице символов, а так- же нового режима адресации элементов. Оба предполагают косвенную адресацию. Поскольку VAR-параметры также требуют косвенной адресации, а режим косвенно- сти уже существует, естественно воспользоваться им для доступа по ссылке. Од- нако имя Ind оказалось бы теперь более подходящим, чем Par. Обозначение Режим (mode) х Var Прямая адресация хл Ind Косвенная адресация хл.у Ind Косвенная адресация со смещением Следовательно, операция разыменования (обычно подразумеваемая) преобра- зует режим адресации элемента из Var в /nd. Подведем итоги. 1. Понятие указателя легко вписывается в нашу систему контроля совмести- мости типов. Каждый тип указателя связан с базовым типом, а именно, с типом переменной, на которую он ссылается.
Динамические структуры данных и указатели 135 2. хл обозначает разыменование, реализуемое с помощью косвенной адре- сации. 3. Указатели сохраняют тип, если они инициализируются значением NIL, а все обращения по ним предваряются проверкой на NIL. Память для переменных, к которым обращаются по указателю, выделяется газовом процедуры NEW(p). Мы предполагаем ее наличие в операционной систе- ме, поддерживающей среду выполнения программ. Размер блока, который дол- ин быть выделен, определяется базовым типом р. До сих пор мы пренебрегали проблемой повторного использования памяти. Для абстрактных программ она действительно не имеет значения, йодля конкрет- ных имеет решающее значение, так как память, в сущности, конечна. Современ- ные операционные системы предлагают централизованное управление памятью со сборкой мусора. Существуют различные схемы повторного использования па- ши, но здесь мы не будем на них останавливаться. Мы ограничимся единствен- ным вопросом, имеющим значение для разработчика компиляторов: какие дан- ные нужно предоставить сборщику мусора, чтобы в любое время все свободные блоки памяти могли быть благополучно обнаружены и восстановлены? Перемен- ная больше не нужна, когда на нее нет ссылок, исходящих от объявленных пере- менных-указателей. Чтобы определить, существуют ли такие ссылки, сборщик мусора требует следующие данные: 1) адреса всех объявленных переменных-указателей, 2) смещения всех полей в динамически выделенной памяти и 3) размер каждой динамически размещенной переменной. Эта информация доступна на этапе компиляции, и она должна быть подготов- лена так, чтобы быть доступной для сборщика мусора на этапе выполнения. Вэтом смысле компилятор и система должны интегрироваться. Предполагается, что система включает механизмы управления памятью, в частности распредели- тель памяти NEW и сборщик мусора. Чтобы сделать эту информацию доступной на этапе выполнения, процедура NEW не только выделяет блок памяти, но и снабжает его дескриптором типа той переменной, которой выделена память. Естественно, такой дескриптор должен быть создан только однажды, так как нет нужды дублировать его для каждого эк- земпляра переменной того же типа. Поэтому блок просто снабжается указателем «адескриптор типа, который остается невидимым для программиста. Этот указа- тель называют тэгом типа (рис. 14.2). Дескриптор типа, как легко видеть, сокращенная форма объекта, описываюше- га тип в таблице символов компилятора, ограниченная данными, необходимыми пя повторного использования памяти. Из такой концепции вытекает следующее: 1. Компилятор должен генерировать дескриптор для каждого типа (RECORD) и добавлять его к объектному файлу. 2. Процедура NEW(p) вдобавок к адресу р получает дополнительный, скрытый параметр, задающий адрес дескриптора базового типа для указателя р.
136 Открытые массивы, указательный и процедурный типы дескриптор типа Рис. 14.2. Указатель, разыменованная переменная и дескриптор типа 3. Загрузчик программы должен истолковать дополнительную информацию объектного файла и сгенерировать дескрипторы типов. Дескриптор типа задает размер переменной и смещение для всех динамичес- ких полей (рис. 14.3). Рис. 14.3. Переменная с дескриптором типа Однако всего этого еще недостаточно. Для того чтобы можно было обходить структуры данных, должны быть известны их корни. Поэтому объектный файл дополняется списком всех объявленных переменных-указателей. Этот список ко- пируется при загрузке в память. Он должен включать скрытые указатели, ссыла- ющиеся на дескрипторы типов. Поскольку дескрипторы не должны генериро- ваться для всех типов данных, Оберон ограничивается только указателями на записи. С учетом роли записей в динамических структурах данных это оправдано. 14.3. Процедурные типы Если в языке процедуры могут передаваться как параметры или если они могут оказываться значениями переменных, то становится необходимым введение про- цедурных типов. Каковы же характеристики таких типов, то есть значений, кото- рые могут принимать переменные такого типа? Процедурные типы вошли в употребление с появлением Алгола 60. Там они существовали только в неявном виде. Параметр в Алголе 60 мог быть процедурой
Процедурные типы 137 (формальной процедурой). Однако его тип не указывался; известно только, что параметр обозначал некоторую процедуру или функцию. Спецификация типа била недостаточно полной и образовала неудачную брешь в системе типов Алго- а В Паскале она была сохранена для совместимости с Алголом. Однако язык Мо- ма-2 уже нуждался в законченной и безопасной спецификации, в нем, помимо параметров, допускались также переменные с процедурными значениями. Таким образом, процедурные типы получили тот же статус, что и остальные типы дан- шх. В этом отношении Оберон следует той же концепции, что и Модула-2 [18]. Из чего же состоит эта безопасная спецификация, называемая сигнатурой про- ведуры? Она содержит все спецификации, необходимые для обеспечения совмес- тности между фактическими и формальными параметрами, а именно их количе- пво, тип каждого параметра, их вид (значение или ссылка) и тип результата в случае процедуры-функции. Следующий пример иллюстрирует сказанное: PROCEDURE F(x, у : REAL): REAL; BEGIN END F PROCEDURE H(f: PROCEDURES, v : REAL): REAL); VAR a, b: REAL; BEGIN a := f(a + b, a - b) ENDH При компиляции объявления процедуры Н проверяется совместимость типов между (а + Ь) и и и, соответственно, между (а - Ь) и v, а также может ли результат f бить присвоен переменной а. При вызове H(F) проверяется не только совмесги- мость типов результатов F и f, но и совместимость типов их параметров, то есть между и и х и между у и v. Отметим, что идентификаторы и иудолжны встречать- ся в программе исключительно как имена формальных параметров формальной процедуры f. Поэтому фактически они лишние, но могут оказаться полезными имментариями для читателя, если им даны осмысленные имена Паскаль, Модула и Оберон предполагают согласование имен как основу для установления совместимости типов. В случае процедур-параметров было сделано молочение: достаточно только структурной совместимости. Если бы требова- лось согласование имен, то типу (сигнатуре) каждой процедуры, используемой •качестве фактического параметра, нужно было бы давать явное имя. При проек- чровании языка такой подход был признан слишком громоздким. Тем не менее пурпурная совместимость требует, чтобы компилятор мог сравнивать два спис- ^параметров на соответствие типов. Таким образом, процедура может быть присвоена переменной приусловиисо- Шегствия друг другу двух списков параметров. Присвоенная процедура активи- руется при обращении к переменной. Вызов будет косвенным. Фактически зго (Фис объектно-ориентированного программирования, где процедуры связаны с полями переменных-записей, называемых объектами. Такие связанные про- цедуры называют методами. В отличие от Оберона методы, однажды объявлен-
138 Открытые массивы, указательный и процедурный типы ные и связанные, не могут быть изменены. Все экземпляры класса обращаются к одним и тем же методам. Реализация процедурных типов и методов оказывается удивительно пропой, если пренебречь проблемой контроля совместимости типов. Значение перемен- ной или поля записи процедурного типа является просто адресом входа в присво- енную им процедуру. Это имеет силу только в том случае, если мы потребуем, что- бы присваивались только глобальные процедуры, то есть процедуры, которые не встроены в некоторый контекст. Такое охотно принимаемое ограничение объяс- няется следующим примером, который нарушает это ограничение. При выполне- нии Q под именем v контекст, содержащий переменные а и Ь, уже отсутствует. TYPE Т = PROCEDURE (u: INTEGER); VAR v: Т; г: INTEGER; PROCEDURE Р; VAR а, Ь: INTEGER; PROCEDURE Q(VAR x: INTEGER); BEGIN x := a + b END Q; BEGIN v := Q END P; ... P; v(r) ... 14.4. Упражнения 14.1. Расширьте язык Оберон-О и его компилятор открытыми массивами: а) для одномерных VAR-параметров; Ь) для многомерных VAR-параметров; с) для параметров-значений. 14.2. Расширьте язык Оберон-О и его компилятор процедурами-функциями: а) с результатом скалярного типа (INTEGER, REAL, SET); b) с результатом любого типа. 14.3. Некоторый модуль Муправляет структурой данных, детали которой дол- жны быть скрыты. Несмотря на это, должна существовать возможность приме- нить любую заданную операцию Р ко всем элементам структуры. С этой целью® М экспортируется процедура Enumerate, которая позволяет задавать Р в качеств своего параметра. Возьмем в качестве простого примера операции Р подсчет всея элементов структуры данных и приведем требуемое решение: PROCEDURE Enumerate^: PROCEDURE (е: Element)); PROCEDURE CountElements*; VAR n: INTEGER; PROCEDURE Cnt(e: Element); BEGIN n := n + 1 END Cnt; BEGIN n := 0; M.Enumerate(Cnt); Texts.Writelnt(W, n, 6) END CountElements;
Упражнения 139 К сожалению, это решение нарушает установленное в языке Оберон ограниче- ние, которое заключается в том, что процедуры, используемые как параметры, должны объявляться глобально. Это вынуждает нас объявить Cnt, а вместе с ней и переменную-счетчик п за пределами CountElements, хотя обе функции опреде- ленно не должны быть глобальными. Реализуйте процедурные типы так, чтобы упомянутое ограничение могло быть снято, а предложенное решение стало бы приемлемым. Какова этому цена? 14.4. Наш RISC-процессор (см. главу 9) имеет стековые команды «втолкнуть» и «вытолкнуть» (PSH и POP). Они используются для реализации стековой парадиг- мы кадровой активации процедур. Если мы добавим альтернативную команду «втолкнуть» PSHu, то вместе с существующей командой POP сможем использовать ее для реализации FIFO-буфера (очереди - Прим, перев.): PSHu: M[R[b] div 4] := R[a]; INC(R[b], c) Но каким образом понятие буфера и использование буферных команд могут быть представлены в языке Оберон-О? Мы предлагаем понятие бегунка (rider). Предположим, что бегунок г и буфер В объявляются как VAR В: ARRAY N OF INTEGER; г: RIDER ON В; и что бегунки размещаются в регистрах. Тогда пусть для бегунка определяются следующие операции: SET(r, В, I) бегунок устанавливается на B[i] гЛ означает элемент, на котором стоит г. После обращения или присваивания г перемещается на слсдущий элемент буфера. Напишите соответствующий модуль с бу<|к-ром и процедурами для извлече- ния и сохранения его элементов. Исследуйте преимущества этого понятия, а так- же его проблемы. Реализуйте его как дополнение к компилятору. 14.5. Наш RISC-процессор (см. главу 9) является, как говорят, бвйт-ориеити- рованным. Но обмен данными с памятью всегда осуществляется группами по 4 байта, называемыми словами. Адреса слов всегда кратны 4. Младшие два бита адреса игнорируются. Давайте воспользуемся байтовой адресацией для распространения понятия бегунка(см. 14.4) на буфер ил байтов (литер). Расширим набор комант RISC твумя командами PSHB и РОРВ. Они используют регистр в качестве (байтового буфера и обмениваются очередным словом с памятью только пос ____ ' ращения к буферу Команда РОРВ определяется с те Хш ЧГТ^П’П’ "’"ШМ обра.юм. РОРВ: циклически сдвинуть R(aJ на 8 битое вппл.г. IF R[b] MOD 4 . О THEN R(a] M[R.b DIV 41 R[b] R[b] + 1 * END: Состояния R[aJ и RlbJ.io и после каждой к каждой команды очередной байт становится мТ" ДЫ ₽°Р8 ""^ «ны ННЖГ ПоС-lf М ^"'ИМ байтом регистра Ria).
140 Открытые массивы, указательный и процедурный типы Ria] Rib] РОРВ Ria] R[b] 0 загрузка слова DCBA 1 DCBA 1 ротация ADCB 2 ADCB 2 ротация BADC 3 BADC 3 ротация CBAD 0 CBAD 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] до и после каждой команды РОИ показаны ниже: Ria] Rib] PSHB R[a] Rib] ...A 0 ротация А... 1 A..B 1 ротация ВА.. 2 BA.C 2 ротация СВА. 3 CBAD 3 сохрание результата DCBA 0 Определите конструкцию байтового бегунка в языке Оберон-О и реализуйте его как расширение компилятора.
Глава 15 I I Модули и раздельная компиляция
142 Модули и раздельная компиляция 15.1. Принцип скрытия информации Алгол 60 ввел принципы текстуальной локальности идентификаторов и ограни- ченного времени жизни именованных объектов во время выполнения. Облает» видимости идентификатора в тексте называют областью действия (scope) [ 12|,и она распространяется на весь блок, в котором идентификатор объявлен. Согласно синтаксису, блоки могут быть вложенными, следовательно, правила о видимости и областях действия должны уточняться. Алголом 60 устанавливается, что иден- тификаторы, объявленные в блоке В, видимы в самом В и во всех блоках, содержа- щихся в В. Но они невидимы в окружении В. Из этого правила разработчик заключает, что память для локальной в блоке! переменной х должна быть выделена, как только управление передается В, и мо- жет быть освобождена, как только управление покидает В. Когда управление ока- зывается за пределами В, переменная х не только не видима, но и прекращает существовать. Важное достоинство состоит в том, что память не остается выде- ленной всем переменным программы. Однако в некоторых случаях весьма желательно более длительное существо- вание переменной в состоянии невидимости. Тогда при повторной передаче управления блоку В оказывается, что переменная х появляется в нем с ее преды- дущим значением. Этот специальный случай был обеспечен в Алголе 60 посред- ством собственных (own) переменных. Но это решение, как вскоре обнаружилось было весьма неудовлетворительным, в частности в связи с рекурсивными проце- дурами. Изящное и очень приемлемое решение проблемы собственности было найдено примерно в 1972 году с введением модульной структуры. Оно было принято в языках Модула [16], Mesa [ 11 ] и позже под названием «пакет» в Аде. Синтакси- чески модуль напоминает процедуру и состоит из локальных объявлений и следу- ющих за ними операторов. Однако, в отличие от процедуры, модуль не вызывает- ся, а его операторы выполняются только раз, а именно когда модуль загружен. Локально объявленные в модуле объекты являются статическими и существуют, пока модуль остается загруженным. Операторы в теле модуля служат только дл инициализации его переменных. Эти переменные не видимы вне модуля; на са- мом деле они скрыты. Термин «скрытие информации» был придуман Д. Л. Парна- сом (D. L. Pamas) и стал важным понятием в программной инженерии. Оберон предоставляет возможность сделать отдельные идентификаторы, объявленные в модуле, видимыми в его окружении. В этом случае говорят, что эти идентифика- торы экспортируются. Собственная переменная х, объявленная внутри Алгол-процедуры Р, теперь будет объявлятся, как и сама Р, локально в модуле М. Теперь экспортируется Р,но не х. Детали реализации Р, как и сама переменная х, скрыты в среде М, но х при этом существует и сохраняет значение между вызовами Р. Желание скрыть определенные объекты и детали особенно оправдано, если система состоит из различных частей, чьи задачи довольно хорошо разделяются,и если эти части сами по себе достаточно сложны. Это типично для организацией-
Раздельная компиляция 143 ной единицы, которая владеет структурой данных. В этом случае структура дан- ных скрывается внутри модуля и доступна только через экспортируемые проце- дуры. Программист этого модуля устанавливает определенные инварианты, та- кие как условия непротиворечивости, которые управляют структурой данных. Можно гарантировать, что эти инварианты сохраняются, потому что они не могут быть нарушены частями системы вне модуля. Как следствие ответственность про- граммиста эффективно ограничивается процедурами внутри модуля. Эта инкап- суляция деталей, ответственная исключительно за указанные инварианты, явля- ется истинной целью скрытия информации и концепции модуля. Типичные примеры модулей и скрытия информации - файловая система, скрывающая структуру файлов и их каталогов, лексический анализатор компиля- тора, скрывающий исходный текст и его лексикографическую структуру, или ге- нератор кода компилятора, скрывающий сгенерированный код и структуру целе- вой архитектуры. 15.2. Раздельная компиляция Было бы заманчиво потребовать, чтобы модули, подобно процедурам, вкладыва- лись друг в друга. Такая возможность предоставляется, например, языком Моду- ла-2. Однако на практике подобная гибкость едва ли полезна. Обычно вполне до- статочно «плоской» модульной структуры. Поэтому мы будем считать все модули глобальными, а их окружением - весь мир. Гораздо важнее вложенности модулей возможность их раздельной разработ- ки и компиляции. Очевидно, что последнее возможно, только если модули явля- ются глобальными, то есть невложенными. Причиной такого требования явля- ется тот простой факт, что программное обеспечение никогда не планируется, не реализуется и не тестируется в строго заданном порядке, напротив, оно разраба- тывается пошагово, вбирая на каждом шаге некоторые дополнения и исправле- ния. Программное обеспечение не «пишется», а растет. В связи этим понятие модуля имеет фундаментальное значение, потому что позволяет разрабатывать отдельные модули раздельно в предположении о неизменности их интерфейсов импорта. Множество экспортируемых объектов фактически образует интер- фейс модуля с его партнерами. Если интерфейс остается неизменным, реализа- ция модуля может быть улучшена (и откорректирована) без необходимости ис- правления и перекомпиляции модулей клиентов. Это реальное обоснование для раздельной компиляции. Преимущество такой концепции становится особенно явственным, если про- граммное обеспечение разрабатывается командой разработчиков. Как только дос- тигается соглашение о разделении системы на модули и об их интерфейсах, члены команды могут независимо продолжить реализацию своих модулей. Даже если на практике окажется, что последующие изменения в спецификации интерфейсов вносились довольно редко, упрощение коллективной работы, благодаря раздель- ной компиляции модулей, трудно переоценить. Успешная разработка сложных систем решительно зависит от концепции модуля и раздельной компиляции.
144 Модули и раздельная компиляция Тут читатель может подумать, что все это не так ново, что независимое про- граммирование модулей и их связывание загрузчиком программ, как показано на рис. 15.1, было в повсеместном использовании, начиная с эры ассемблеров и пер- вых компиляторов Фортрана. Рис. 15.1. Независимая компиляция модулей А и В Однако думать так - значит игнорировать тот факт, что языки программиро- вания более высокого уровня предлагают значительно более сильную защиту от ошибок и несовместимостей благодаря понятию статического типа. Эта неоцени- мая, но слишком часто недооцениваемая, выгода пропадает, если контроль сов- местимости типов гарантируется только внутри модулей, а не между ними. Сле- довательно, информация о типах всех импортируемых объектов должна быть доступна всякий раз, когда модуль компилируется. В отличие от независимой компиляции (рис. 15.1), где эта информация недоступна, компиляцию с межмо- дульным контролем совместимости типов (рис. 15.2) называют раздельной ком- пиляцией. Информация об импортируемых объектах - это, в сущности, фрагмент табли- цы символов, описанной в главе 8. Данный фрагмент таблицы символов, преобра- зованный в линейную форму, называется символьным файлом. Компиляция моду- ля А, который импортирует модули В1,..., Вп (то есть их объекты), теперь требует, в дополнение к исходному тексту А, символьные файлы В1 ,...,Вп. В дополнение к объектному коду (A.obj) генерируется также символьный файл (A.sym). Рис. 15.2. Раздельная компиляция модулей А и В
Реализация символьных файлов 145 15.3. Реализация символьных файлов Из предыдущих обсуждений мы можем, во-первых, заключить, что компиляция списка импорта модуля требует чтения символьного файла каждого модуля в списке. Таблица символов компилируемого модуля заполняется импортируе- мыми символьными файлами. Во-вторых, из этого следует, что по окончании ком- пиляции выполняется обход обновленной таблицы символов, чтобы получить символьный файл с записями, соответствующими каждому элементу таблицы символов, помеченному для экспорта. Для примера на рис. 15.3 показан фрагмент таблицы символов при компиляции модуля А, импортирующего В. Внутри В иден- тификаторы Тиб помечены звездочкой для экспорта. MODULE А, IMPORTS; VARxBT. BEGIN xf — 1; END А Рис, 15.3. Таблица символов А с импортом В Рассмотрим для начала процесс генерации символьного файла M.sym модуля М. На первый взгляд задача состоит только в обходе таблицы и выдаче в надлежа- щем линеаризованном виде записей, соответствующих каждому помеченному элементу. Таблица символов, в сущности, является списком объектов с указателя- ми на древовидные структуры типов. В таком случае наиболее подходящим спо- собом будет, пожалуй, линеаризация структур, использующая характерный пре- фикс для каждого элемента. Это иллюстрируется примером на рис. 15.4. Проблема возникает из-за того, что каждый объект содержит как минимум Указатель, ссылающийся на его тип. Запись значений указателей в файл пробле- матична, если не сказать больше. Наше решение заключается в том, чтобы при просмотре таблицы символов заносить в файл описание типа в момент первого
IHlt 146 Рис. 15.4. Линеаризованная форма таблицы символов с двумя массивами его появления. Таким образом, запись о типе помечается и получает уникальный ссылочный номер. Номер хранится в дополнительном поле записи типа ObjectDesc Если позже на этот тип снова ссылаются, то вместо его структуры выводится его ссылочный номер. Такой способ не только позволяет избежать повторов при записи одних и its же описаний типа, но решает также проблему рекурсивных ссылок, как показано на рис. 15.5. Для ссылочных номеров используются положительные значения. Для указа- ния на то, что ссылочный номер используется впервые и, стало быть, вслед заним TYPE Р = POINTER ТО R; R = RECORD x. у: INTEGER; next: P END class name type adr next Рис. 15.5. Циклическая ссылка в типе
Реализация символьных файлов 147 идет описание типа, номер получает знак минус. При чтении символьного файла таблица типов Т строится со ссылками на соответствующие структуры типов. Если читается положительный ссылочный номер г, то требуемым указателем бу- дет Т[г]; если же г отрицательный, то читаются следующие за ним данные о типе, а указателем, ссылающимся на вновь созданный дескриптор, становится Т[-г]. В отличие от данных о других объектах, информация о типе может не только импортироваться, но и реэкспортироваться. Поэтому необходимо указать модуль, от которого происходит экспортируемый тип. Чтобы сделать это возможным, мы используем так называемый якорь модуля. В заголовке каждого символьного файла есть список объектов-якорей, по одному на каждый импортируемый мо- дуль, являющийся реэкспортируемым, то есть содержащим тип, на который ссы- лается экспортируемый объект. Рисунок 15.6 иллюстрирует такую ситуацию; мо- дуль С импортирует модули А и В, в силу чего из В импортируется переменная х, тип которой происходит из А. Контроль совместимости типов для присваивания вроде у: = х основывается на предположении, что оба указателя на типы х и у ссы- лаются на один и тот же дескриптор типа. Если это не так, возникает ошибка. Рис. 15.6. Реэкспорт топа А.Т из модуля В Отсюда мы заключаем, что при компиляции модуля М нужны не только табли- цы символов явно импортируемых модулей, но и тех модулей, на типы которых ссылаются либо прямо, либо косвенно. Это становится поводом для беспокой- ства, потому что компиляция любого модуля может потребовать чтения символь- ных файлов всей иерархии модулей. Таким образом можно опуститься до самых глубин операционной среды, откуда не импортируются ни переменные, ни проце- дуры, а разве только один-единственный тип. Результатом этого может стать не только избыточная загрузка огромного количества данных, но и большой расход памяти. Хотя наше беспокойство оправдано, оказывается, что последствия всего этого менее драматичны, чем может показаться [3]. Дело в том, что большинство
148 Модули и раздельная компиляция необходимых таблиц символов уже присутствуют по другим причинам. Поэтому дополнительной работы остается совсем немного. Тем не менее стоит продуман возможность сокращения такой чрезмерной работы. В первых компиляторах для Модулы и Оберона применялся следующий метод. Пусть модуль М прямо или косвенно импортирует типы из модулей МО, Ml ит.д Решение состоит в том, чтобы включить в символьный файл модуля М полис описания импортируемых типов, избегая таким образом ссылок на вторичные модули МО, Ml и т. д. Однако такое довольно очевидное решение вызывает неко- торые сложности. В примере на рис. 15.6 символьный файл модуля В, очевидно, содержит полное описание типа Т. Контроль совместимости для присваивания у := В.х, будучи высокоэффективным, просто сравнивает два указателя типа. По- этому конфигурация, показанная справа на рис. 15.6, уже должна существовать после загрузки. Ввиду того, что реэкспортируемые типы в символьных файлах специфицируются не только собственным модулем, при загрузке символьного файла необходимо проверить, существует ли уже читаемый тип. Это возможно в случае, когда символьный файл модуля, определяющего тип, уже загружен или когда тип уже был прочитан при загрузке других символьных файлов. Упомянем здесь также еще одну связанную с типами небольшую сложность, которая возникает потому, что типы могут появляться под разными именами (псевдонимами). Хотя использование псевдонимов является редкостью, опреде- ление языка, к сожалению, допускает их. Они немного выразительнее, если сино- нимы происходят от разных модулей, как показано на рис. 15.7. Рис. 15.7. Типы с псевдонимами При загрузке символьного файла модуля В выясняется, что В.Т1 и А.ТО, оба указывающие на объект описания типа, должны фактически указывать на одна и тот же дескриптор объекта. Чтобы определить, какой из двух дескрипторов должен быть уничтожен, а какой оставлен, узлы типа (типа Structure) снабжа- ются обратным указателем на исходный объект типа (типа Object), здесь - на ТО.
Адресация внешних объектов 149 15.4. Адресация внешних объектов Главное достоинство раздельной компиляции состоит в том, что изменение моду- ля М не оказывает негативного влияния на клиентов М, если интерфейс М остает- ся неизменным. Напомним, что интерфейс состоит из полного набора экспорти- руемых объявлений. Изменения, которые не затрагивают интерфейса, могут вноситься, так сказать, втайне, причем без ведома программистов, пишущих кли- ентские программы. Такие изменения даже не должны требовать перекомпиля- ции клиентских программ, использующих новые символьные файлы. Истины ради спешим добавить, что экспортируемые процедуры не должны менять своей семантики, так как компиляторы не могут наверняка обнаружить таких измене- ний. Следовательно, если мы говорим, что интерфейс остался неизменным, то яв- но относим это к объявлениям типов и переменных, а также к описаниям проце- дур, а к их семантике - только неявно. Если в некотором модуле неэкспортируемые процедуры и переменные изме- няются, добавляются или удаляются, то их адреса, конечно, также меняются, и, следовательно, то же самое произойдет с другими, возможно экспортируемыми, переменными и процедурами. Это приведет к изменению таблицы символов и в силу этого также к повреждению клиентских модулей. Но это, очевидно, проти- воречит требованиям раздельной компиляции. Решение этой дилеммы заключается в том, чтобы не включать адреса в сим- вольный файл. Это значит, что адреса должны вычисляться при загрузке и связы- вании модуля. Поэтому вдобавок к своему адресу (для внутримодульного ис- пользования) экспортируемый объект получает уникальный номер. Этот номер соответствует месту адреса в символьном файле. Как правило, эти номера назна- чаются строго последовательно. Следовательно, при компиляции клиента ему доступны только специальные номера модуля, но не адреса. Эти номера, как упомянуто ранее, должны превра- щаться в исполнительные адреса при загрузке. Для этого нужно располагать знаниями о позициях таких незаполненных адресных полей. Вместо исполь- зования объектного файла со списком всех позиций таких адресов элементы списка закрепления адресов вставляются в команды на истинные места еще не- известных адресов. Это аналог метода вычисления адресов перехода вперед (см. главу 11). Если все такие заполняемые адреса собрать в единый список зак- репления, то это будет соответствовать рис. 15.8а. Каждый элемент задается па- рой, состоящей из номера модуля mno и номера записи епо. Проще снабдить каждый модуль отдельным списком. Тогда в объектном файле потребуется не один-единственный корень списка закрепления, а по одному для каждого списка, что соответствует рис. 15.86. На рис. 15.8в показана другая крайность, когда для каждого импортируемого объекта создается отдельный список закрепления. Ка- кое из трех представленных решений будет выбрано, зависит от того, сколько информации можно поместить на место исполнительного адреса, который заме- нит ее в конечном счете.
150 Модули и раздельная компиляция а Список всего Список для Список для каждого каждого модуля объекта Рис. 15.8. Три вида списков адресных привязок в объектных файлах 15.5. Проверка конфигурационной совместимости Может показаться запоздалым задаваться теперь вопросом: «Зачем вообще введе- ны символьные файлы?» Допустим, нужно откомпилировать модуль М, который импортирует модули МО и Ml. Очевидным решением было бы непосредственно перед компиляцией М перекомпилировать модули МО и Ml и объединитьтрипо- лученные в результате таблицы символов. Компиляцию модулей МО и Ml можно легко запустить при компиляции М, прочитав его список импорта. Хотя повторная компиляция того же самого исходного текста - пустая трата времени, такой метод применяется в различных коммерческих компиляторахдля (расширенного) Паскаля и Модулы. Однако серьезный недостаток этого метода заключается не столько в необходимости дополнительных усилий, сколько в от- сутствии гарантии совместимости связываемых модулей. Предположим, что И должен быть перекомпилирован после некоторых изменений, сделанных в исход- ном тексте. Тогда вполне вероятно, что после первоначальной формулировки Ми его компиляции в МО и Ml тоже могут быть внесены изменения, которые повредят М. Может даже оказаться, что исходные версии МО и Ml, доступные в настояше* время программисту клиента М, больше не совместимы с актуальными объектны- ми файлами МО и М1. Этот факт уже не может быть обнаружен при компиляции!! и почти наверняка приводит к катастрофе, когда связываются и выполняются не- согласованные части.
Проверка конфигурационной совместимости 151 Однако символьные файлы не допускают изменений, как исходные файлы; они закодированы и невидимы для текстового редактора. Они могут изменяться только целиком. Для обеспечения совместимости каждый символьный файл снабжается уникальным ключом. Таким образом, символьные файлы делают мо- дули доступными, не раскрывая при этом их исходного текста. Клиент может по- лагаться на предоставленное ему определение интерфейса, а благодаря ключу ему гарантируется также совместимость этого определения с существующими реали- зациями. Пока такой гарантии нет, вся идея модулей и раздельной компиляции хоть и заманчива, но едва ли полезна. Так, например, рис. 15.9 показывает слева ситуацию при компиляции модуля В, а справа - при компиляции модуля С. Между этими двумя компиляциями модуль А был изменен и перекомпилирован. В связи с этим файлы символов модулей В и С содержат якори модуля А с различающимися ключами, а именно 8325 в В и 8912 в С. Компилятор проверяет ключи, замечает их различие и выдает сообщение об ошибке. Если же модуль А изменен после перекомпиляции С (с измененным ин- терфейсом), то несогласованность может и должна быть обнаружена при загрузке и связывании этой конфигурации модулей. С этой целью те же ключи включают- ся и в соответствующие объектные файлы. Поэтому можно обнаружить несогла- сованность импорта А в В и С до попытки выполнения. Это крайне важно. Рис. 15.9. Несогласованность версий модуля Пара «ключ - имя» используется в качестве характеристики каждого модуля, и такая пара содержится в заголовке каждого символьного и объектного файлов. Как уже упоминалось, имена модулей в списке импорта тоже дополняются их ключом. Эти соображения приводят к структуре символьных файлов, определен- ных в приложении АЗ. Уникальные ключи модуля могут быть сгенерированы различными алгорит- мами. Простейшим, возможно, является использование текущих времени и даты, которые, соответствующим образом закодированные, дают желаемый ключ. Не- достаток состоит в том, что этот метод не совсем надежен. Даже если разрешаю- щая способность часов - одна секунда, то одновременные компиляции на разных
152 Модули и раздельная компиляция компьютерах могут сгенерировать одинаковый ключ. Гораздо важнее то, что две компиляции одного и того же исходного текста должны всегда генерировать один и тот же ключ; но они этого не сделают. Следовательно, если в модуль внесены изменения, которые позже окажутся ошибочными, то перекомпиляция его перво- начальной версии даст в результате новый ключ, который сделает старые клиен- ты как бы испорченными. Лучший метод генерировать ключ состоит в том, чтобы использовать в каче- стве параметра сам символьный файл, как при вычислении контрольной сумш, Но этот метод тоже не во всем безопасен, потому что для разных файлов символов могут получиться одинаковые ключи. Но зато его преимущество в том, что каж- дая перекомпиляция одного и того же текста генерирует один и тот же ключ. Клю- чи, вычисленные таким способом, называются отпечатками (fingerprints). 15.6. Упражнения 15.1. Включите раздельную компиляцию в ваш компилятор Оберон-О. Язык рас- ширяется включением списка импорта (см. приложение А.2) и маркером в объяв- лениях экспортируемых идентификаторов. Используйте технику символьньв файлов и введите правило, которое не позволяет извне присваивать значенияэкс- портируемым переменным, то есть в импортирующих модулях они считаются пе- ременными «только для чтения». 15.2. Реализуйте способ отпечатков для генерации ключей модуля.
Глава 16 Оптимизация и структура пре/постпроцессора .-/д:7 < * .'З.-Исг►£»>-.!•. J) Л‘- < <Л l.'-.p’ If. Гвь мр>5адг..>'-,. /p«?S НыГО V» ^^^йИ^^В|В1'вВШВЙйаЙ*|1|88Яя ^^^Я®ИЙ^ОЙЖйв1ийвЙ1ЙйЖ
154 Оптимизация и структура пре/постпроццессора 16.1. Общие соображения Если мы проанализируем код, сгенерированный компилятором, разработанный в предыдущих главах, то можем легко увидеть, что он правильный, но довольно примитивный, и во многих случаях может быть усовершенствован. Причиназак- лючается прежде всего в прямолинейности выбранного алгоритма, который транслирует языковые конструкции вне зависимости от их контекста в фиксиро- ванные последовательности команд. Он с трудом различает особые случаи и не использует их выгод. Прямолинейность такой схемы приводит к результатам, ко- торые лишь отчасти отвечают требованиям экономии памяти и скорости выпол- нения. Это неудивительно, поскольку исходный и целевой языки попросту не со- гласуются друг с другом, в связи с чем мы наблюдаем семантический разрыв между языком программирования, с одной стороны, и системой команд и архи- тектурой машины - с другой. Чтобы генерировать код, который использует имеющиеся команды и ресурсы машины, более эффективно, должны быть применены более изощренные схемы трансляции. Они называются оптимизациями, а использующие их компилято- ры - оптимизирующими компиляторами. Следует обратить внимание, что этот термин, хотя и широко распространен, в сущности является эвфемизмом (разно- видностью синонима - Прим, перев.). Вряд ли кто-либо готов утверждать, что сге- нерированный им код оптимален во всех отношениях, то есть совсем не поддается усовершенствованию. Так называемая оптимизация есть не что иное, как усовер- шенствование. Однако мы согласимся с общепринятой терминологией и тоже бу- дем использовать термин «оптимизация». Совершенно очевидно, что чем изощреннее алгоритм, тем лучше получаемый код. В общем, можно утверждать, что чем лучше сгенерированный код и чем быст- рее его выполнение, тем сложнее, больше и медленнее будет компилятор. В неко- торых случаях компиляторы построены так, чтобы дать возможность выбирать уровень оптимизации - низкий, пока программа находится в разработке, и высо- кий - после ее завершения. Попутно отметим, что оптимизация может выбирать- ся с разными целями, такими как ускорение выполнения или сжатие кода. Эти два критерия обычно требуют различных алгоритмов генерации кода и зачастую про- тиворечивы, явно указывая на то, что нет такого понятия, как точно определен- ный оптимум. Неудивительно, что одни меры по улучшению кода могут дать значительные выгоды при скромных усилиях, тогда как другие могут потребовать существенно- го наращивания сложности и размера компилятора, принося лишь умеренные улучшения кода, просто потому, что они редко применяются. В действительное^ существует огромная разница в соотношении между усилиями и выгодой. Преж- де чем проектировщику компилятора решиться на включение в него изощренных средств оптимизации или кому-то решиться на приобретение хорошо оптимизи- рующего, медленного и дорогого компилятора, стоит выяснить это соотношение» решить, действительно ли нужны обещаемые улучшения.
Простые оптимизации 155 Кроме того, мы должны различать те оптимизации, эффект от которых может быть получен за счет иного написания исходной программы, и те, где это невоз- можно. Первый вид оптимизации оказывает услугу главным образом бездарному или неряшливому программисту, а всех остальных пользователей только обреме- няет увеличенными размерами и уменьшенной скоростью компилятора. В каче- стве противоположного примера рассмотрим случай компилятора, который ис- ключает умножение, если один множитель имеет значение 1. При вычислении адреса элемента массива, когда индекс должен умножаться на размер его элемен- тов, ситуация складывается по-разному. Здесь размер нередко равен 1. но умно- жение не может быть исключено ловким трюком в исходной программе. Следующий критерий классификации средств оптимизации - зависимость их от данной целевой архитектуры. Существуют приемы, которые можно выразить непосредственно на исходном языке независимо от целевого компьютера. Приме- ры целенезависимой оптимизации приводятся в следующих известных тожде- ствах: х + 0 = х х * 2 = х + х b & TRUE = Ь b & ~b = FALSE IF TRUE THEN A ELSE В END = A IF FALSE THEN A ELSE В END = В С другой стороны, существуют оптимизации, которые возможны только бла- годаря особым свойствам данной архитектуры. Например, существуют компью- теры, которые объединяют в одной команде умножение и сложение, сравнение и условный переход. В таких случаях компилятор должен распознать шаблон кода, который позволит воспользоваться такой специальной командой. Наконец, обратим также внимание на то, что чем больше оптимизаций со зна- чительным эффектом может быть включено в компилятор, тем беднее должна быть его первоначальная версия. В связи с этим громоздкая структура многих коммерческих компиляторов, происхождение которой трудно понять, приводит Эк удивительно слабым первоначальным рабочим характеристикам, которые де- лают оптимизационные средства абсолютно необходимыми. 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 Деление (целых чисел) обрабатывется таким же образом. Если делитель равен 2к для некоторого целого числа к, то делимое просто сдвигается на к битов вправо. Для операции остатка от деления просто выделяются самые младшие к битов. 16.3. Исключение повторных вычислений Наверное, самый известный случай целенезависимой оптимизации - устранение общих подвыражений. На первый взгляд, этот случай может быть классифициро- ван как избирательная оптимизация, потому что перевычисление того же самого подвыражения может быть достигнуто простым изменением исходной програм- мы. Например, присваивания х: = (а+Ь)/с; у: = (a+b)/d могут быть легко заменены тремя более простыми присваиваниями с использова- нием вспомогательной переменной и: и: = a+b; х: = и/с; b: = u/d Конечно, эта оптимизация касается количества арифметических операций, во не касается количества присваиваний или ясности исходного текста. Поэтому во- прос, будет ли такое изменение улучшением вообще, остается открытым. Более критичен случай, когда улучшения невозможно достичь изменением ис- ходного текста, как показано в следующем примере: a[i, j] := a[i, j] + b[i, j] Здесь одно и то же вычисление адреса выполняется трижды, и каждый раз оно включает по крайней мере одно умножение и одно сложение. Общие подвыраже- ния неявны и не видны непосредственно в исходном тесте. Оптимизация можи быть выполнена только компилятором. Устранение общих выражений имеет смысл, только если они вычисляются по- вторно. Хотя это возможно и в случае, когда выражение встречается в исходив тексте лишь однажды: WHILE i > 0 DO z := х + у; i := i - I END Так как x и у остаются неизменными во время повторения, сумма должна быть вычислена только однажды. Компилятор должен вынести присваивание перемен- ной z за пределы цикла. Во всех перечисленных случаях код программы может быть улучшен за счет избирательного анализа контекста. Но это именно то, что заметно прибавляетра-
Распределение регистров 157 боты при компиляции. Представленный здесь компилятор для Оберон-О - не слишком подходящая основа для такого вида оптимизации. Техника упрощения выражений путем использования значений, вычисленных на предыдущем шаге цикла, то есть с учетом рекуррентных соотношений, подобна вынесению константных выражений за пределы циклов. Если, например, адрес элемента массива adr(a[i]) = k*i + аО, то adr(a[i+1 ]) - adr(a[ij) + к. Этот случай особенно част и потому уместен. Например, адреса индексных переменных в опе- ! раторе FOR i := О ТО N-1 DO a[i] := b[i] ‘ c[i] END могут быть вычислены простым прибавлением константы к их предыдущим зна- чениям. Такая оптимизация приводит к существенному сокращению времени вы- полнения. Тест со следующим примером матричного умножения показал удиви- тельные результаты: FOR i := О ТО 99 DO FORj := О ТО 99 DO FOR k := О ТО 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. Распределение регистров Доминирующей темой в оптимизации являются использование и распределение регистров процессора. В представленном компиляторе Оберон-О регистры исполь- зуются исключительно для хранения безымянных промежуточных результатов во время вычисления выражений. Для этой цели обычно достаточно нескольких регистров. Однако характерная черта современных процессоров - значительное число регистров с временем доступа, существенно меньшим, чем доступ к основ- ной памяти. Использование их только для промежуточных результатов означало бы нерациональное использование наиболее ценных ресурсов. Главная цель хоро- шей оптимизации кода - наиболее эффективное использование регистров с целью сокращения числа обращений к относительно медленной основной памяти. Хоро- шая стратегия использования регистров лает больше преимуществ, чем любой другой вид оптимизации.
158 Оптимизация и структура пре/постпроццессора Широко распространенный метод - это распределение регистров с использо- ванием раскрашенного графа. Для каждого значения, возникающего в процессе вычисления (то есть для каждого выражения), определяются точка его возникно- вения и точка его последнего использования. Они задают границы области его существования. Очевидно, значения различных выражений могут быть сохране- ны в одном и том же регистре тогда и только тогда, когда их области существова- ния не перекрываются. Области представляются вершинами графа, в котором дуга между двумя вершинами означает, что две области перекрываются. Тогда под распределением N доступных регистров для возникающих значений можно понимать раскраску графа в N цветов таким образом, что соседние вершины все- гда имеют разные цвета. Это значит, что значения с перекрывающимися областя- ми всегда размещаются в разных регистрах. Более того, некоторые скалярные локальные переменные размещаются вовсе не в памяти, а в выделенных им регистрах. Для достижения оптимального исполь- зования регистров применяются изощренные алгоритмы, способные определить, к каким переменным обращаются чаще всего. Очевидно, что необходимая «бух- галтерия» обращений к переменным разрастается, в связи с чем страдает скорость компиляции. К тому же необходимо позаботиться о том, чтобы сохранить регист- ровые значения в памяти перед вызовом процедуры и восстановить их при выходе из процедуры. Скрытая опасность состоит в том, что затраты, необходимые для решения этой задачи, превосходят получаемую экономию. Во многих компилято- рах локальные переменные размещаются в регистрах только в процедурах, кото- рые не содержат никаких вызовов самих себя (процедуры-листья) и потому вызы- ваются наиболее часто, так как являются листьями в дереве, представляющем иерархию вызовов процедур. Детальная проработка всех этих проблем оптимизации находится за рамками вводного курса о построении компиляторов. Поэтому сделанного выше наброска будет достаточно. Во всяком случае, такие методы дают понять, что для генерации кода, близкого к оптимальному, необходимо учитывать гораздо больше информа- ции о контексте, чем в нашем относительно простом компиляторе Оберон-О. Его структура не вполне подходит для достижения высокой степени оптимизации. Но он превосходно служит в качестве быстрого компилятора, производящего весьма приемлемый, хотя и не оптимальный код, что присуще фазе разработки системы, особенно для образовательных целей. Раздел 16.5 показывает другую, несколько более сложную структуру компилятора, которая больше подходит для включения в его состав алгоритмов оптимизации. 16.5. Структура пре/постпроцессорного компилятора Наиболее важной чертой компилятора, разработанного в главах 7-12, является то, что исходный текст читается ровно один раз. В связи с этим код генерируется «на лету». В любой момент информация об операндах ограничивается элемента-
Структура пре/постпроцессорного компилятора 159 ми, обозначающими операнд, и таблицей символов, представляющей объявления. Так называемая пре/постпроцессорная (fron- tend/backend) структура компилятора, ко- торая была кратко упомянута в главе 1, су- щественно отличается в этом отношении. Препроцессорная часть также читает исход- ный текст только однажды, но вместо того, чтобы генерировать код, она формирует структуру данных, представляющую про- грамму в удобной для дальнейшей обработ- ки форме. Вся информация, содержавшаяся в операторах, отображается в эту структуру данных. Ее называют синтаксическим дере- вом, потому что она также отражает синтакси- ческую структуру текста. Несколько упрощая, можно сказать, что препроцессор компили- рует объявления в таблицу символов, а опе- раторы - в синтаксическое дерево. Эти две структуры данных образуют интерфейс для постпроцессорной части, задача которой - Рис. 16.1. Компилятор, состоящий из препроцессора и постпроцессора генерация кода. Синтаксическое дерево предоставляет быстрый доступ фактически ко всем частям программы и представляет программу в предварительно обработан- ной (preprocessed) форме. Такой процесс компиляции показан на рис. 16.1. В главе 1 мы указали на одно существенное преимущество такой структуры: разделение компилятора на целенезависимый препроцессор и целезависимый по- стпроцессор. В дальнейшем мы сосредоточимся на интерфейсе между этими дву- мя частями, а именно на структуре синтаксического дерева. Более того, мы пока- жем, как генерируется подобное дерево. Точно так, как в исходной программе операторы ссылаются на объявления, синтаксическое дерево ссылается на записи в таблице символов. Это приводит к понятному желанию объявить элементы таблицы символов (объекты) таким об- разом, чтобы можно было ссылаться на них из самой таблицы символов так же. как из синтаксического дерева. В качестве основного введем тип Object, который может видоизменяться для представления констант, переменных, типов и проце- дур. Общим для всех останется только атрибут type. Здесь и дальше мы восполь- зуемся возможностью Оберона, названной расширением типа 114|. Object = POINTER ТО ObjDesc; ObjDesc = RECORD type: Type END ; ConstDesc = RECORD (ObjDesc) value: LONGINT END ; VarDesc « RECORD (ObjDesc) adr, level: LONGINT END ; Таблица символов состоит из списков элементов, по одному для каждой обла- сти действия (см. раздел 8.2). Элементы состоят из имени (идентификатора) и ссылки на объект с таким именем.
160 Оптимизация и структура пре/постпроццессора Ident = POINTER ТО IdentDesc; IdentDesc = RECORD name: ARRAY 32 OF CHAR; obj: Object; next: Ident END ; Scope = POINTER TO ScopeDesc; ScopeDesc = RECORD first: Ident; dsc: Scope END ; Синтаксическое дерево лучше представить как двоичное дерево. Назовем его элемент Node (узел). Если синтаксическая конструкция имеет форму списка, она представляется вырожденным деревом, в котором последний элемент имеет пус- тую ветвь. Node = POINTER ТО NodeDesc; NodeDesc = RECORD (Object) op: INTEGER; left, right: Object END Рассмотрим в качестве примера следующий фрагмент текста программы: VAR х, у, z: INTEGER; BEGIN z := х + у - 5; ... Препроцессор анализирует исходный текст и формирует таблицу символов и синтаксическое дерево, как показано на рис. 16.2. Представления типов даннш опущены. Представления вызовов процедур, операторов IF и WHILE и последовательнос- ти операторов показаны на рис. 16.3-16.5. Заключительные примеры демонстрируют, как генерируются описанные структуры данных. Читатель должен сравнить эти фрагменты компилятора с со-
Структура пре/постпроцессорного компилятора 161 Рис. 16.4. Операторы IF и WHILE ответствующими процедурами компилятора Оберон-О, приведенного в приложе- нии С. Все последующие алгоритмы используют вспомогательную процедуру New, которая генерирует новый узел. PROCEDURE New(op: INTEGER; х, у: Object): Item; VAR z: Item; BEGIN New(z); z.op := op; z.left:« x; z.rlghty; RETURN z END New; PROCEDURE factorO: Object; VAR x: Object; c: Constant; BEGIN IF sym - ident THEN x This(name); Cet(sym); x selector(x)
162 Оптимизация и структура пре/постпроццессора ELSIF sym = number THEN NEW(c); c.value := number; Get(sym); x :=c ELSIF sym = Iparen THEN Cet(sym); x := expressionO; IF sym = rparen THEN Get(sym) ELSE Mark(22) END ELSIF sym = not THEN Get(sym); x := New(not, NIL, factorO) ELSE ... END ; RETURN x END factor; PROCEDURE termO: Object; VAR op: INTEGER; x: Object; BEGIN x := factorO; WHILE (sym >= times) & (sym <= and) DO op := sym; Get(sym); x := New(op, x, factorO) END ; RETURN x END term; PROCEDURE statementO: 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, expressionO) ELSIF ... END ELSIF sym = while THEN Get(sym); x := expressionO; IF sym = do THEN Get(sym) ELSE Mark(25) END ; x := New(while, x, statseqO); IF sym = end THEN Get(sym) ELSE Mark(20) END ELSIF ... END ; RETURN x END statement Эти фрагменты ясно показывают, что структура препроцессора предопределе- на синтаксическим анализатором. Программа стала даже несколько проще. Но нужно иметь в виду, что для краткости в этих процедурах опущен контроль типов. Тем не менее контроль типов как целенезависимая задача определенно относится к препроцессору. 16.6. Упражнения 16.1. Улучшите генерацию кода компилятора Оберон-О так, чтобы значения и ад- реса, однажды загруженные в регистр, могли бы повторно использоваться без пе- резагрузки. Так, для примера z := (х - у) * (х + у); у := х
! Упражнения 163 представленный компилятор сгенерирует последовательность команд LDW 1, 0, х LDW 2, О, у SUB 1, 1, 2 LDW 2, О, х LDW 3, О, у ADD 2, 2, 3 MUL 1, 1, 2 STW 1,0, z LDW 1, 0, x STW 1,0, у Улучшенная версия должна генерировать LDW 1, 0, х LDW 2, 0, у SUB 3, 1, 2 ADD 4, 1, 2 MUL 5, 3, 4 STW 5, 0, z STW 1, О, у Измерьте вручную выгоду на разумно большом количестве тестовых вариантов. 16.2. Какие дополнительные команды архитектуры RISC из главы 9 были бы Желательны для облегчения реализации предыдущих упражнений и генерации э°лее короткого и более эффективного кода? 16.3. Оптимизируйте компилятор Оберон-О таким способом, чтобы скалярные 1еРеменные по возможности размещались на регистрах вместо памяти. Измерьте достигнутый выигрыш и сравните его с тем, что получен в упражнении 16.1. Как орабатываются переменные в качестве VAR-параметров? 16.4. Создайте модуль OSCx, который заменяет OSC (см. текст в приложении . и генерирует код для CISC-архитектуры х. Заданный интерфейс OSC должен ,ь,ть сохранен насколько это возможно, для того чтобы модули OSS и OSP оста- 'Ись неизменными.
Приложение А. Синтаксис А. 1. Оберон-О 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" {IdentListtype ";”}] {ProcedureDeclaration ";"}. module = "MODULE" identdeclarations ["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" | "В" | "C" | "D" | ”E" I "F". digit = "0" | "1" | "2" | "3" I "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 = [IdentListtype], 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 = ”=" | "#" | “<” I | ’>" | ">=” | IN ) IS. SimpleExpression = [”+"]"-"] term {AddOperator term). AddOperator = “+" || OR . term = factor {MulOperator factor}. MulOperator = | | DIV | MOD | . factor = number | CharConstant | string | NIL I set I designator [ActualParameters) | "(" expression ")" |factor. set = ”{" [element element}] "J". element = expression ["..” expression], ActualParameters = "(" [ExpList]. statement = [assignment I ProcedureCall I IfStatement | CaseStatement | Whilestatement I Repeatstatement | Loopstatement | ForStatement | WithStatement I 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 qualidentqualident 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 ";"} I 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 I RECORD type name {type name} END типы и поля записей | PROCTYP type name {[VAR] type name END . типы процедур и их параметров Слова, состоящие из заглавных букв, обозначают терминальные символы. В символьных файлах они кодируются целыми числами. OldType и Module обо- значают номера типа и модуля, то есть это ссылки на предварительно определен- ные объекты.
Приложение В Набор символов ASCH о О nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel 8 bs 9 ht A If 8 vt C ff D cr E so F si 1 2 die del I dc2 " dc3 # dc4 $ nak % syn & etb can ( em ) sub * esc + fs gs
Приложение С Компилятор Оберон-О Компилятор разбит на три модуля, а именно сканер 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 х, у, z: INTEGER; BEGIN Read(x); Read(y); Read(z); Write(x+y+z); WriteLn END Add активированная командой Оберона OSP.Exec Add 3 5 7, выдаст в качестве резуль- тата 15.
1 Лексический анализатор 169 С. 1. Лексический анализатор MODULE OSS; (* NW 1 9.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; Iss* =11; geq* = 12; leq‘= 13; gtr* = 14; period* =18; comma* = 19; colon* = 20; rparen* = 22; rbrak* » 23; of* = 25; then* = 26; do* = 27; Iparen* =29; Ibrak* = 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; 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.WritelntfW, p, 1); Texts.Write(W, ""); Texts.WriteStringfW, msg), Texts.WriteLn(W); Texts.AppendfOberon.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 Приложение С. Компилятор Оберон-О 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] ;= OX; к := 0; WHILE (k < nkw) & (id # keyTab[k].id) DO INC(k) END; IF к < 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) + ORDC'O”)) DIV 10 THEN val ;= 10 * val + (ORD(ch) - ORDC'O'')) ELSE МагкСчисло слишком большое"); val := О END; Texts.Read(R, ch) UNTIL (ch < "0") OR (ch > "9") END Number; PROCEDURE comment; BEGIN Texts.Read(R, ch); LOOP LOOP WHILE ch = T 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 МагкС'комментарий не завершен"); 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 ITexts.Read(R, ch); sym := plus I Texts.Read(R, ch); sym := minus I Texts.Read(R, ch); sym := eql I Texts.Read(R, ch); sym := neq | Texts.Read(R, ch); IF ch = "=" THEN Texts.Read(R, ch); sym := leq ELSE sym := Iss END | Texts.Read(R, ch);
; Лексический анализатор 171 IF ch = "=" THEN Texts.Read(R, ch); sym := geq ELSE sym := gtr END ITexts.Read(R, ch); sym := semicolon ITexts.Read(R, ch); sym := comma ITexts.Read(R, ch); IF ch = "=" THEN Texts.Read(R, ch); sym := becomes ELSE symcolon END ITexts.Read(R, ch); sym := period I Texts.Read(R, ch); IF ch = THEN comment; Cet(sym) ELSE sym := Iparen END I ”)”: Texts.Read(R, ch); sym := rparen I Texts.Read(R, ch); sym := Ibrak ITexts.Read(R, ch); sym := rbrak I "0”.."9": Number; I "A" .. "Z", ”a".."z": Ident I 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.OpenReaderfR, T, pos); Texts.Read(R, c > END Init; PROCEDURE EnterKW (sym: INTEGER; name: ARRAY OF CHAR); BEGIN keyTab[nkw].sym := sym; COPY(name, keyTabfnkwJ.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"); EnterKWtnull, "EXIT”); EnterKW(then, "THEN"): EnterKW(type, "TYPE"); EnterKW(null, "WITH”); EnterKW(array, "ARRAYS; EnterKW(begin, "BEGIN);
172 Приложение С. Компилятор Оберон-О 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. С.2. Синтаксический анализатор MODULE OSP; (* NW 23.9.93 / 9.2.95") IMPORT OSC, OSS, MenuViewers, Oberon, TextFrames, Texts, Viewers; CONST WordSize = 4; VAR sym: INTEGER; loaded: BOOLEAN; topScope, universe: OSC.Object; (* связанные списки, оканчивающиеся охраной *) guard: OSC.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; О55.Магк("повторное объявление") 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.Markf’He объявлен”); EXIT END; s := s.dsc END END find; 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.dass = 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-------------------*) PROCEDUREA expression (VAR x: OSG.Item); PROCEDURE selector (VAR x: OSG.Item); VAR y: OSG.Item; obj: OSG.Object; BEGIN WHILE (sym = OSS.Ibrak) OR (sym = OSS.period) DO IF sym = OSS.Ibrak THEN OSS.Get(sym); expression(y); IF x.type.form = OSG.Array THEN OSG.Index(x, y) ELSE OSS.Mark(”He массив") END; IF sym = OSS.rbrak THEN OSS.Get(sym) ELSE OSS.MarkCJ?*) END ELSE OSS.Get(sym); IF sym = OSS.ident THEN IF x.type.form = OSC.Record THEN FindField(obj, x.type.fields); OSS.Get(sym); IF obj # guard THEN OSGFiekXx, obj) ELSE OSSMarkf не определено") END ELSE OSS.MarkfHe запись") END ELSE О55.Магк("идентификатор?") END END END END selector; PROCEDURE factor (VAR x: OSG.Item); VAR obj: OSG.Object; BEGIN (*sync*)
174 Приложение С. Компилятор Оберон-О IF sym < OSS.Iparen THEN О55.Магк("идентификатор?“); REPEAT OSS.Get(sym) UNTIL sym >= OSS.Iparen END; IF sym = OSS.ident THEN find(obj); OSS.Get(sym); OSG.Makeltem(x, obj); selector(x) ELSIF sym = OSS.number THEN OSG.MakeConstltem(x, OSG.intType, OSS.val); OSS.Get(sym) ELSIF sym = OSS.Iparen THEN OSS.Get(sym); expression(x); IF sym = OSS.rparen THEN OSS.Get(sym) ELSE OSS.MarkC')?") END ELSIF sym = OSS.not THEN OSS.Get(sym); factor(x); OSG.Opl (OSS.not, x) ELSE OSS-МагкСсомножитель?"); OSG.Makeltem(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.Opl (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.Opl (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.Opl (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 OSC.Parameters, fp.type, fp.dass); fp := fp.next ELSE 055.Магк("спишком много параметров") 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.Iparen THEN OSS.Get(sym) ELSE OSS. MarkO?") END; expression(x); IF sym = OSS.rparen THEN OSS.Get(sym) ELSE OSS.MarkOD END END param; BEGIN (* StatSequence *) LOOP (‘синхронизация*) obj := guard; IF sym < OSS.ident THEN OSS.MarkConepaTop?"); REPEAT OSS.Getfsym) UNTIL sym >= OSS.ident END; IF sym = OSS.ident THEN find(obj); OSS.Get(sym); OSG.Makeltem(x, obj); selector(x); IF sym = OSS.becomes THEN OSS.Getfsym); expression^); OSGAorefx, y) ELSIF sym = OSS.eql THEN OSS.MarkO D; OSS.Ge«sym); expression^) ELSIF x.mode = OSG.Proc THEN par := obj.dsc; IF sym = OSS.Iparen THEN OSS.Getfsym); IF sym = OSS.rparen THEN OSS.Getfsym) ELSE LOOP parameter(par); IF sym = OSS.comma THEN OSS.Getfsym) ELSIF sym = OSS.rparen THEN OSS.Getfsym); EXIT ELSIF sym >• OSS.semicolon THEN EXIT ELSE OSS.MarkC) или , Г) END END END END; IF obj.val < 0 THEN О55.Магк("вызов вперед") ELSIF -IsParam(par) THEN OSC.Call(x) ELSE О55.МагкСслишком мало параметров") END ELSIF x.mode - OSG.SProc THEN IF obj.val <= 3 THEN param(y) END; OSG.IOCallfx, y) ELSIF obj.dass = OSG.Typ THEN О55.МагкСнеправильный оператор?") ELSE OSS.MarkConepaTop?") END ELSIF sym-OSS.If THEN OSS.Getfsym); expresslon(x); OSG.Qump(x);
176 Приложение С. Компилятор Оберон-О IF sym = OSS.then THEN OSS.Get(sym) ELSE OSS.MarkC'THEN?") END; StatSequence; L := 0; WHILE sym = OSS.elsif DO OSS.Get(sym); OSG.FJump(L); OSG.FixLink(x.a); expression(x); OSG.QJump(x); IF sym = OSS.then THEN OSS.Get(sym) ELSE OSS.MarkC'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.MarkC'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.MarkC'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.MarkC; ?") 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 О55.Магк(”идентификатор?") END END; IF sym = OSS.colon THEN OSS.Get(sym) ELSE OSS.MarkC':?") 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.МагкС'тип?"); 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 О55.МагкСтипГ) END ELSIF sym = OSS.array THEN OSS.Get(sym); expression(x); IF (x.mode # OSG.Const) OR (x.a < 0) THEN О55.Магк("неверный индекс1) END; IF sym = OSS.of THEN OSS.Get(sym) ELSE OSS.MarkCOFT) 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.size0; OpenScope; LOOP IF sym = OSS.ident THEN ldentList(OSG.FId, first); Type(tp); obj := first; WHILE obj # guard DO obj.type := tp; obj.val := type.size; INCftype.size, obj.type.size); obj := obj.next END END; IF sym = OSS.semicolon THEN OSS.Get(sym) ELSIF sym = OSS.ident THEN OSS.MarkC; ?’) ELSE EXIT END END; type.fields := topScope.next; CloseScope; IF sym = OSS.end THEN OSS.Get(sym) ELSE OSS.MarkCEND?") END ELSE 055.Магк("идентификатор?") 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) 4 (sym # OSS.end) THEN ОЗЗ.МагкСобмялениеГ); 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 NewObjfobj, OSG.Const); OSS.Get(sym); IF sym = OSS.eql THEN OSS.Get(sym) ELSE OSS.MarkC»?") ENO; expression(x); IF x.mode = OSG.Const THEN obj.val: x.a; obj.type > x.type ELSE О55.МагкСвыражение не константа") END; IF sym - OSS.semicolon THEN OSS.Getfsym) ELSE OSS.MarkC;?")
178 Приложение С. Компилятор Оберон-О 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("=?'j END; Type(obj.type); IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.MarkC';?") END END END; IF sym = OSS.var THEN OSS.Get(sym); WHILE sym = OSS.ident DO ldentList(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.MarkC; ?") END END END; IF(sym >= OSS.const) & (sym <= OSS.var) THEN О55.Магк("объявяение?") ELSE EXIT END END END declarations; PROCEDURE ProcedureDeci; 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); ldentList(OSG.Par, first) ELSE ldentList(OSG.Var, first) END; IF sym = OSS.ident THEN find(obj); OSS.Get(sym); IF obj.class = OSG.Typ THEN tp := obj.type ELSE О55.Магк("идентификатор?"); tp := OSG.intType END ELSE О55.Магк("идентификатор?"); tp := OSG.intType END; IF first-class = OSG.Var THEN parsize := tp.size;
Синтаксический анализатор 179 IF tp.form >= OSG.Array THEN OSS.MarkfHe параметр записи") END; ELSE parsize := Wordsize END; obj := first; WHILE obj # guard DO obj.type ;= tp; INCfparblksize, parsize); obj := obj.next END END FPSection; BEGIN (* ProcedureDecI *) OSS.Getfsym); IF sym = OSS.ident THEN procid := OSS.id; NewObjfproc, OSG.Proc); OSS.Getfsym); parblksize ;- marksize; OSG.IncLevel(l); OpenScope; proc.val:« - 1; IF sym = OSS.Iparen THEN OSS.Getfsym); IF sym = OSS.rparen THEN OSS.Getfsym) ELSE FPSection; WHILE sym = OSS.semlcolon DO OSS.Getfsym); FPSection END; IF sym - OSS.rparen THEN OSS.Getfsym) ELSE OSS.MarkC)?") 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 DECflocblksize, WordSize) ELSE obj.val := locblksize; obj:» obj.next END END; proc.dsc := topScope.next; IF sym = OSS.semlcolon THEN OSS.Getfsym) ELSE OSS.MarkC;?") END; locblksize := 0; declaratlonsflocblksize); WHILE sym = OSS.procedure DO ProcedureDecI; IF sym = OSS.semicolon THEN OSS.Getfsym) ELSE OSS.MarkC;?") END END; proc.valOSG.pc; OSG.EnterflocblksIze); IF sym » OSS.begin THEN OSS.Getfsym); StatSequence END; IF sym = OSS.end THEN OSS.Getfsym) ELSE OSS.MarkCEND?") END; IF sym “ OSS.ident THEN IF procid # OSS.id THEN OSS-МагкСне подходит") END; OSS.Getfsym) END; OSG.Returnfparblksize - marksize); CloseScope; OSG.IncLevelf - 1) END END ProcedureDecI; PROCEDURE Module (VAR S: Texts.Scanner);
180 Приложение С. Компилятор Оберон-О 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.WriteStringfW, modid); Texts.WriteLn(W); Texts.Append(Oberon.Log, W.buf) ELSE О55.Магк(''идентификатор?'') END, IF sym = OSS.semicolon THEN OSS.Get(sym) ELSE OSS.MarkC’;?") END; declarations(varsize); WHILE sym = OSS.procedure DO ProcedureDecI; IF sym = OSS.semicolon THEN OSS.Cet(sym) ELSE OSS.MarkC';?") 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.MarkC'END?") END; IF sym = OSS.ident THEN IF modid # OSS.id THEN OSS.MarkC’He подходит”) END; OSS.Get(sym) ELSE О55.Магк("идентификатор?") END; IF sym # OSS.period THEN OSS.MarkC'. ?") END; CloseScope; IF -OSS.error THEN COPY(modid, S.s); OSG.CIose(S, varsize); Texts.WriteString(W, "код сгенерирован"); Texts.Writelnt(W, OSG.pc, 6); Texts.WriteLn(W); Texts.Append(Oberon.Log, W.buf) END ELSE OSS.MarkC'MODULE?") END END Module; PROCEDURE 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.MarkedViewerO; IF (v.dsc # NIL) & (v.dsc.next IS TextFrames.Frame) THEN OSS.Initfv.dsc.nextCTextFrames.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® END END ELSIF S.class = Texts.Name THEN NEW(T); Texts.OpenfT, S.s); OSS.Init(T, 0); OSS.Cet(sym); Module® END END Compile; PROCEDURE Decode*; VAR V: MenuViewers.Viewer; T: Texts.Text; X, Y: INTEGER; BEGIN T := TextFrames.TextC”); 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.NewTextfT, 0), TextFrames.menuH, X, Y); OSG.Decode(T) END Decode; PROCEDURE Load*; VAR S: Texts.Scanner; BEGIN IF -OSS.error & -loaded THEN Texts.OpenScanner(S, Oberon.Par.text, Oberon.Par.pos); OSG.Load®; loaded := TRUE END END Load; PROCEDURE Exec*; VAR S: Texts.Scanner; BEGIN IF loaded THEN Texts.ppenScanner(S, Oberon.Par.text, Oberon.Par.pos); Texts.Scan®; IF S.dass = Texts.Name THEN OSG.Exec® 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, "OberonO Compiler 9.2.95"); Texts.WriteLn(W); Texts.Append(Oberon.Log, W.buf); NEW(guard); guard.dass:- OSG.Var; guard.type OSG.IntType; guard.val0; topScope :- NIL; OpenScope;
182 Приложение С. Компилятор Оберон-О enter(OSG.Typ, 1, "BOOLEAN", OSG.boolType); enterfOSG.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. С.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; Fid* = 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; DM = 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 = 1 5; (‘reserved registers*) TYPE Object* = POINTER TO ObjDesc; Type* = POINTER TO TypeDesc; Item* = RECORD mode*, lev*: INTEGER; type*: Type; a*, b, c, r: LONGINT; END; ObjDesc* = RECORD class*, lev*: INTEGER; next*, dsc*: Object; type*: Type; name*: OSS.ident; val*: LONGINT END; TypeDesc* - RECORD form*: INTEGER;
Генератор кода 183 fields*: Object; base*: Type; size*, len*: INTEGER END; VAR boolType*, IntType*: Type; curlev*, pc*: INTEGER; eno: 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 (‘выдать команду перехода*) codefpc] := ASH(op - 40H, 26) + (disp MOD 4000000H); INC(pc) END PutBR; PROCEDURE TestRange (x: LONGINT); BEGIN (*18-битовое значение*) IF (x >= 20000H) OR (x < - 20000H) THEN О55.МагкСзначение слишком большое”) END 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); EXCLfregs, x.r); x.r:- r ELSIF x.mode = Const THEN TestRange(x.a); GetReg(x.r); Put(MOVl, x.r, 0, x.a) END;
184 Приложение С. Компилятор Оберон-О x.mode := Reg END load; PROCEDURE loadBool (VAR x: Item); BEGIN IF x.type.form # Boolean THEN OSS.MarkC'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 (LO, LI: LONGINT): LONGINT; VAR L2, L3: LONGINT; BEGIN IF LO # 0 THEN L2 := LO; LOOP L3 := code[L2] MOD 40000H; IF L3 = 0 THEN EXIT END; L2 := L3 END; code[L2] := code[L2] - L3 + LI; RETURN LO ELSE RETURN LI END END merged; PROCEDURE fix (at, with: LONGINT); BEGIN code[at] := code[at] DIV 400000H * 400000H + (with MOD 400000H) END fix; PROCEDURE FixWith (LO, LI: LONGINT); VAR L2: LONGINT; BEGIN WHILE LO # 0 DO L2 := codefLO] MOD 40000H; fix(LO, LI - LO); LO := L2 END END FixWith; PROCEDURE FixLInk* (L: LONGINT); VAR LI: LONGINT; BEGIN WHILE L # 0 DO LI := code[L] MOD 40000H; fix(L, pc - L); L := LI END
Генератор кода 185 END FixLink; (*---------------------------------------------*) PROCEDURE IncLevel* (n: INTEGER); BEGIN INC(curlev, n) END IncLevel; PROCEDURE MakeConstltem* (VAR x: Item; typ; Type; val: LONGINT); BEGIN x.mode := Const; x.type := typ; x.a ;= val END MakeConstltem; PROCEDURE Makeitem* (VAR x: Item; y: Object); VAR r: LONGINT; BEGIN x.mode := y.dass; 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.MarkC'yponeHb!"); 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 О55.Магк("индекс не целое") END; IF y.mode = Const THEN IF (y.a < 0) OR (y.a >= x.type.len) THEN О55.Магк("неверный индекс") END; lNC(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); EXCUregs, x.r); x.r := y.r END; x.type := x.type.base END Index; PROCEDURE Opl * (op: INTEGER; VAR x: Item); (* x :- op x *) VAR t: LONGINT; BEGIN IF op = OSS.minus THEN IF x.type.form # Integer THEN О55.Магк("неверный тип")
186 Приложение С. Компилятор Оберон-О 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 Opl; PROCEDURE Op2* (op: INTEGER; VAR x, y: Item); (* x := x op у *) 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 О55.Магк("неверный тип") 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 О55.Магк("неверный тип") 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 := У-С ELSIF op = OSS.and THEN x.a := merged(y.a, x.a); x.b := y.b; x.c := У-с END ELSE О55.Магк("неверный тип") END; END Op2; PROCEDURE Relation* (op: INTEGER; VAR x, y: Item); (‘ x := x ? у *)
Генератор кода 187 BEGIN IF (x.type.form # Integer) OR (y.type.form # Integer) THEN О55.Магк("неверный тип") ELSE PutOp(CMP, x, y); x.c := dp - OSS^eql; EXCLfregs, y.r) < ' END; x.mode := Cond; x.type ;= boolType; x.a := 0; x.b := 0 END Relation; PROCEDURE Store* (VAR x, y: Item); (* x := у *) VAR r: LONGINT; BEGIN IF (x.type.form IN {Boolean, Integer}) & (x.type.form = y.type.fbrm) THEN IF y.mode = Cond THEN Put(BEQ + negated(y.c), y.r, 0, y.a); EXCLfregs, y.r); y.a .= pc - 1; FixLink(y.b); GetRegty.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 О55.Магк("неправильное присваивание") END; EXCL(regs, x.r); EXCL(regs, y.r) ELSE О55.Магк("невоместимое присваивание") 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 О55.Магк(”неправильный режим параметра") 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 О55.Магк("неверный тип параметра") END END Parameter;
188 Приложение С. Компилятор Оберон-О PROCEDURE CJump* (VAR х: Item); BEGIN IF x.type.form = Boolean THEN IF x.mode # Cond THEN loadBool(x) END; PutBR(BEQ + negated(x.c), x.a); EXCUregs, x.r); FixLink(x.b); x.a := pc - 1 ELSE OSS.MarkCBoolean?”); x.a := pc END END (Jump; 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 lOCall* (VAR x, y: Item); VAR z: Item; BEGIN IF x.a < 4 THEN IF y.type.form # Integer THEN OSS.MarkC'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); EXCUregs, y.r) ELSIF x.a = 3 THEN load(y); PutfWRH, 0, 0, y.r); EXCUregs, y.r) ELSE Put(WRL, 0, 0, 0) END END lOCall; PROCEDURE Header* (size: LONGINT); BEGIN entry := pc; Put(MOVI, SP, 0, RISC.MemSize - size); (*иницилизация SP*) 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;
Генератор кода 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*; BEGIN curlev := 0; pc := 0; eno := 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, comnamefcno]); comadrfcno] := 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 < eno) & (S.s # comname(i)) DO INC(i) END; IF i < eno 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.Writelnt(W, вход * 4, 6); Texts.WriteLn(W); i := 0; WHILE i < pc DO w := code[i]; op := w DIV 4000000H MOD 40H; Texts.Writelnt(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.WritelntfW, w DIV 400000H MOD 10H, 4);
190 Приложение С. Компилятор Оберон-О Texts.Write(W, Texts.Writelnt(W, w DIV 40000H MOD ЮН 4)- Texts.Write(W, ELSE a w MOD 4000000H; IF a >= 2000000H THEN DEC(a, 4000000H) (‘расширение знака*) END END; Texts.Writelnt(W, a, 6); Texts.WriteLn(W); INC(i) END; Texts.WriteLn(W); Texts.AppendfT, W.buf) END Decode; BEGIN Texts.OpenWriter(W); NEW(boolType); boolType.form := Boolean; boolType.size := 4; NEWOntType); intType.form ;= Integer; intType.size := 4; mnemo[MOV] := "MOV ”; mnemo(MVN] := "MVN "; mnemo[ADD] := "ADD "; mnemo(SUBl := "SUB"; mnemo[MUL] := "MUL"; mnemo[Div] := "DIV ”; mnemo[Mod] := "MOD "; mnemo[CMP] := "CMP"; mnemo[MOVI] := "MOVI"; mnemo[MVNI] := "MVNI"; mnemoIADDI] := "ADDI"; mnemo[SUBI] := "SUBI"; mnemo[MULI] := "MULI"; mnemo[DIVI] := "DIVI”; mnemo[MODI] := "MODI"; mnemo[CMPI] ;= "CMPI"; mnemojCHKI] := "CHKI”; mnemo[LDW] := "LDW"; mnemo[LDBJ := "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 “; mnemofBSR] := "BSR"; mnemo[RET] := "RET"; mnemo[RD] := "READ"; mnemo[WRD] := "WRD ”; mnemo[WRH] := "WRH "; mnetno[WRL] := "WRL"; END OSG.
Литература 1, Aho А 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. Е. 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 Р, Mossenbock Н. Ein Compiler-Generator fur 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. (Рус- ский перевод: Вирт H. Программирование на языке Модула-2. - М.: Мир, 1987.) 19. Wirth N. and Gutknecht J. Project Oberon. Wokingham: Addison-Wesley, 1992. 20. Ахо А., Сети P„ Ульман Дж. Компиляторы: принципы, технологии и инстру- менты. - М.: Издательский дом «Вильямс», 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. Формат 70x100 */1б. Гарнитура «Петербург». Печать офсетная. Усл. печ. л. 36. Тираж 1000 экз. № Web-сайт издательства: www.dmk-press.ru
вн1яц ЧИ1’Н . «фат ааап» лапа жата ими» »>пнмшттт1ч attain» */>•«</« а «мм aaataainarf *» atrar. • и »»« "ммакии омра <nJtefM> • vn шиива*»ячи4» *d> «aputad» ОМ» mhu'i rauapadbi «aaaaane димфй t na> а» адтап мм «аааммтамхй “"<«'< «иамг iunwum чалма» жар амал •* **» ммжг ««"> •«*• *» • • ц»,» н aoaaadnd» амтаалжля 1*»* «г»»» аоатамжж »«и »«••*•»»•<•'• • >«uatw та «ома wad* пашам о даатаг ом панам мдам аж дам мммд а ал I! 1ч^им</« <Лм»п маму aawba/a нмиаа/и^мм» • мамам» хамита арпимн «штамм тМяаогагап» им хжапап» «амт wa lap амааамАм^ ' '«нмшогафв «трлиу «ветлами *» таамаам а Мм маргарита «амага мм* наааию а»»т> .паа амНим отжиш дмиамфвфт а 'шмваим • амаагта «ад мшчиаапа я <и> -даг дм- vaaia»»adkai олммАмдаа» м>ьд< • а « дааам »»«— дн >аь«цаМм м>» хи» ачндпта аимаагя^я* амамаамаяма ам^ма мим at nwawarAi^kaa гаматдгя жч»*А>4а< аау амии над в awaaw «аа**4в •пин »аааик.> Ла^опа а датам нам «мм «ааним^м адаам а тавмамйм »ам|* <>«апаи тмра4» *иаам|мам aaJMaA Да «ааа 'ДМ ам вам • «ааааам* аха^М* "’*«ааа<ю4а >ям«и»о4агт«Мми апама аааааяпма а ним ihMiiim<а • пшмрдй »а«1таа«раимг <м» аяпп а»1«^ аайамаамаа аааваа4аа«м4Ь м« •Ли. гм» л а ХЙ» оман та <мярирааа таымммдш ata чйЬ «аде >arf имам мн aaiiirtaadt aruua а мнг»>>4аааг<1>м>а н'н»д1лм> taaiatkua »<uhmuj а «м янгиалб a«u»aat4»uda atf aotlapo iHHrandaaarduida жж» га >•<.чхлама out aaarua им жadц паи .«aardatti а матаашш- авыагаа -.ь.। tr>atdi ндаталп аж najdioa i>rd п »<4^ч> *аяп <иш ай» adaaad» «и •<i.l<u»t auain raarmalaaunde txk» iauanwa м авпам at» artvadnra ма lai >иам Itdi4 r>wiiH|| а«ндга<1<>фии atw«v> • шагмтм > амаом а адаа» aodoiKifHUHOM эинэойхэоц