Текст
                    Д.Грис
КОНСТРУИРОВАНИЕ КОМПИЛЯТОРОВ ДЛЯ ЦИФРОВЫХ ВЫЧИСЛИТЕЛЬНЫХ МАШИН
Compiler Construction for Digital Computers
David Gries Cornell University
JOHN WILEY & SONS, INC.
NEW YORK • LONDON • SYDNEY • TORONTO ♦ 1971
Д. Грис
Конструи рова н ие компиляторов для цифровых вычислительных машин
ПЕРЕВОД С АНГЛИЙСКОГО
Е. Б. ДОКШИЦКОЙ, Л. А. ЗЕЛЕНИНОЙ, Л. Б. МОРОЗОВОЙ, В. С. ШТАРКМАНА
ПОД РЕДАКЦИЕЙ
Ю. М. БАНКОВСКОГО, Вс. С. ШТАРКМАНА
ИЗДАТЕЛЬСТВО «МИР»
МОСКВА 1975
УДК 681.142.2
В книге крупнейшего американского специалиста в области компиляторов Д. Гриса нашел отражение богатый опыт разработки и использования трансляторов для ЭВМ третьего поколения. Подробно рассмотрены как теоретические основы разработки компиляторов (теория формальных языков и грамматик), так и практические вопросы их реализации (лексический и синтаксический анализ, организация памяти и таблиц, оптимизация программ).
Книга1 может служить учебником для тех, кто впервые сталкивается с разработкой компиляторов, и справочным пособием для опытных специалистов в этой области. Книга окажет большую помощь при подготовке в университетах и институтах квалифицированных кадров по системному программированию.
Редакция литературы по математическим наукам
20204-019
Г ~Q4i joi),75 19-75	© Перевод на русский язык, «Мир», 1975
ПРЕДИСЛОВИЕ РЕДАКТОРОВ ПЕРЕВОДА
Советский читатель уже имел возможность познакомиться с автором этой книги, профессором Корнельского университета Дэвидом Грисом по превосходному обзору «Системы построения трансляторов», написанному им совместно с Дж. Фелдманом. Перевод обзора был опубликован в 1971 году в сборнике «Алгоритмы и алгоритмические языки», вып. 5. И хотя зйачительная часть обзора в переработанном виде вошла в книгу, книга построена в совершенно ином плане. Она, по существу, представляет собой систематическое изложение методов конструирования компиляторов. В ее основу положено несколько курсов-лекций, прочитанных автором в ряде американских университетов и в нескольких международных школах. Поэтому неудивительно, что книге свойственна хорошая дидактическая форма, отражающая тщательно продуманную .методику изложения предмета.
Автор в своем предисловии говорит о том, что книга содержит весь необходимый материал по курсу 15 «Конструирование компиляторов», рекомендованному Комитетом университетских программ при АСМ. (Мы сочли целесообразным поместить вслед за нашим предисловием программу этого курса.) В настоящее время книга широко используется при изучении курса «Конструирование компиляторов» во многих зарубежных (не только американских) университетах. Редакторы перевода могут также отметить свой положительный опыт использования этой книги (еще в оригинале) при работе со студентами механико-математического факультета МГУ.
Не следует, однако, думать, что это всего лишь учебное пособие. В книге систематизирован и прекрасно изложен многолетний коллективный опыт разработки компиляторов. Заметим, что в список литературы вошло около 200 работ, на которые автор ссылается, иллю
6
ПРЕДИСЛОВИЕ РЕДАКТОРОВ ПЕРЕВОДА
стрируя как интуитивные приемы, так и формализованные методы, применяющиеся при конструировании компиляторов. Поэтому книгу с интересом прочтут и те, у кого уже есть немалый собственный опыт в создании трансляторов. Вероятно, многие обнаружат в книге и «свои изобретения», которые, оказывается, уже давно известны и широко применяются.
Время не внесло кардинальных изменений в концепции и точки зрения, изложенные в книге. Поэтому мы лишь в отдельных местах добавили ссылки на работы, появившиеся после ее выхода.
Видимо, нет особой необходимости говорить подробнее о том, что содержится в книге и как ее следует читать. Этому вопросу уделено большое внимание в предисловии автора. Заметим лишь, что это первая, и, насколько нам известно, пока единственная в мировой литературе книга, содержащая систематическое изложение методов компиляции, не привязанное к какому-либо одному конкретному языку или тем более к одной конкретной реализации языка.
Стало уже правилом, что при переводе литературы по программированию переводчики и редакторы испытывают серьезные терминологические затруднения. Не была исключением и работа над этой книгой. Можно отметить два типа затруднений: во-первых, иногда соответствующие русские термины вообще отсутствуют, и, во-вторых, для некоторых понятий существует множество (как правило, одинаково неудачных) терминов. Несмотря на то что этой стороне работы над переводом уделялось много внимания, ни переводчики, ни редакторы не удовлетворены в полной мере ее результатами.
По поводу некоторых терминов стоит сказать отдельно. Мы приняли встречавшийся уже в литературе по программированию термин «литера» как «алфавитно-цифровой знак» (character), оставив за термином «символ» (symbol) более общий смысл. Менее последовательны мы были в отношении перевода слова string. В разделах, связанных с математической лингвистикой, используется термин «цепочка», применительно к языкам программирования сохранен термин «строка». Слово operator переводится как «оператор», и поэтому statement переводится как «инструкция» (исходя, в частности, из рекомендации IFIP, см. Гоулд [71]). Мы отважились на введение терминов «хеширование», «хеш-адресация», «хеш-функция» и т. п., происходящих от английского слова hash, понимая, что на первых порах это слово будет резать слух. Но нам представлялось
ПРЕДИСЛОВИЕ РЕДАКТОРОВ ПЕРЕВОДА
7
неразумным обходиться без одинакового корня для всей совокупности терминов, связанных с данным кругом понятий, причем хотелось иметь корень, не загруженный другими значениями и дающий минимальную свободу для образования производных слов. Все попытки воспользоваться известными терминами типа «перемешанные таблицы», «рандомизация», «функция расстановки» приводили к длинным и тяжеловесным словосочетаниям.
Необходимо отметить, что английский оригинал этой книги издан не типографским способом и содержит довольно много опечаток и неточностей. При переводе многие из них были исправлены без специальных оговорок, номы, разумеется, не можем гарантировать, что не осталось незамеченных или привнесенных ошибок. Наверное, нам не удалось в полной мере сохранить стиль автора, точность и лаконичность его языка. Можно лишь сказать, что от чтения оригинала мы получили удовольствие и надеемся, что читателю перевода достанется хоть некоторая его часть.
Над переводом работали: Е. Б. Докшицкая (гл. 4—7), Л. А. Зеленина (предисловие, гл. 1—3, 20), Л. Б. Морозова (гл. 8—10,17 и приложение), В. С. Штаркман (гл. 11—16, 18, 19, 21 и дополнение).
Ю. М. Баяковский Вс. С. Штаркман
Программа курса 15 «Конструирование компиляторов»
(Из журнала „Communication of the ACM", 11, N 3, 1968, 181)
Этот курс посвящается главным образом изучению методов анализа исходного языка и генерации эффективной объектной программы. Конечно, в нем следует рассмотреть некоторые теоретические вопросы, но он должен иметь практическую направленность, т. е. в результате изучения этого курса студенты должны научиться конструировать компиляторы. Поэтому в задание по программированию необходимо включить реализацию компонент компилятора и, возможно, проектирование простого компилятора целиком (групповое задание).
Приводимый здесь перечень тем по объему, по-видимому, превышает разумные размеры курса. Поэтому преподаватель должен сделать некоторый отбор материала.
1.	Обзор методов ассемблирования, техники работы с таблицей символов и методов макрогенерации. Обзор методов синтаксического анализа и других способов распознавания различных конструкций программы. Обзор методов компиляции, загрузки и выполнения готовой программы, выделение среди прочих представлений скомпилированной программы представления на языке загрузки.
2.	Методы однопроходной компиляции. Перевод арифметических выражений из постфиксной формы на язык машины. Эффективное использование регистров и временных переменных.
3.	Распределение памяти для констант, простых переменных, массивов и временных переменных. Функции и процедуры, структура независимых блоков, структура вложенных блоков и динамическое распределение памяти.
4.	Объектный код для переменных с индексами, функции отображения и информационные векторы. Компиляция инструкций переходов. ..
5.	Подробное рассмотрение общей организации простого компилятора. Таблицы символов. Лексический анализатор (сканер), синтаксический анализатор, генератор объектного кода, стеки операторов и операндов, подпрограммы вывода и диагностика ошибок.
9
6.	Типы данных, функции преобразования данных из одного типа в другой, выражения и инструкции смешанного типа.
7.	Компиляция подпрограмм и функций. Вызов параметров по адресу, по наименованию и по значению. Побочный эффект в подпрограммах. Ограничения, налагаемые при однопроходном выполнении. Объектный код для передачи параметров. Объектный код для тела подпрограммы.
8.	Языки, предназначенные для написания компиляторов: TMG (Макклюр), COGENT (Рейнольдс), GARGOYLE (Гарвик), МЕТА II (Шорр) и TGS — II (Читэм).
9.	Техника раскрутки. Обсуждение метакомпилятора, написанного на своем собственном языке.
10.	Методы оптимизации. Анализ частоты использования конструкций программы с целью определения наиболее важных сторон оптимизации.
11.	Локальная оптимизация, проводимая в целях более эффективного использования специальных команд, таких, как загрузка в регистр постоянного значения, запись нуля в память, замена знака на противоположный, прибавление к содержимому памяти, умножение или деление на два, возведение в квадрат, возведение в степень с целочисленным показателем степени и сравнение с нулем. Оптимизация вычисления индекса.
12.	Оптимизация выражений. Вычисление общих подвыражений и другие методы. Минимизация числа используемых временных переменных и арифметических регистров в машинах с несколькими регистрами.
13.	Оптимизация циклов. Несколько способов программирования типичных циклов. Использование индекс-регистра для самого внутреннего цикла. Классификация циклов в зависимости от целей оптимизации.
14.	Проблемы глобальной оптимизации. Построение и анализ графа программы. Изменение последовательности вычислений для максимальной «разгрузки» самого внутреннего цикла. Вынесение инвариантных подвыражений.
ПРЕДИСЛОВИЕ
Компиляторы и интерпретаторы являются неотъемлемой частью любой вычислительной системы — без них нам пришлось бы все программировать на автокоде или даже в машинных командах! Поэтому компиляторы стали важной практической областью научных исследований, связанных с вычислительными машинами. Цель данной книги состоит в том, чтобы ясно и последовательно изложить основные методы, используемые при написании компиляторов, что позволит новичку легче освоить эту область, а специалисту ориентироваться в литературе.
В книге рассматриваются так называемые синтаксически-уп-равляемые методы компиляции. В самом деле, свыше одной трети книги посвящено теории формальных языков и автоматическому распознаванию синтаксиса. Я убежден, что любой, кто занимается составлением компиляторов, должен знать основы этого предмета. Это не означает, что каждый компилятор должен быть написан с использованием методов автоматического распознавания синтаксиса. Существует много языков программирования, для которых эти методы не пригодны. Но знание основ теории формальных языков позволит разработчику глубже понять то, что происходит в компиляторе, поможет ему более систематически и эффективно провести проектирование и программирование компилятора.
Однако синтаксический анализ составляет лишь небольшую часть компилятора, и поэтому я включил главы по таким разделам, как организация таблиц символов, нейтрализация ошибок, генерация готовой программы, оптимизация программы и т. д. Некоторые разделы (преобразование констант, шаговые компиляторы) были опущены, чтобы сохранить разумный размер книги.
Имеется в виду, что книга послужит двум целям: с одной сто
12
' ПРЕДИСЛОВИЕ
роны, она может быть использована профессиональными программистами, интересующимися или занимающимися составлением компиляторов, в качестве самоучителя или справочника, с другой стороны, она содержит материал односеместрового курса «Конструирование компиляторов».
Фактически книга покрывает (и перекрывает) все разделы, которые перечислены в программе курса 15 (конструирование компиляторов), рекомендованном Комитетом университетских программ при АСМ и опубликованном в мартовском номере журнала Communications of the АСМ за 1968 год.
Читатель должен иметь по крайней мере годичный опыт программирования на языке высокого уровня (например, на АЛГОЛе ФОРТРАНе или PL/1) и на автокоде, он также должен уметь читать и понимать программы на АЛГОЛе. В некоторых разделах книги встречаются ссылки на элементарную теорию булевых матриц (в книге содержится краткое введение в эту теорию). Предполагается, что читатель знаком с такими понятиями, как множество, объединение двух множеств и т. д. Кроме того, читатель должен иметь математические знания, скажем, на уровне младших курсов ВУЗов с математическим уклоном.
Необходимость в опыте работы с языком высокого уровня очевидна, поскольку эта книга о транслирующих программах, написанных на подобных языках. Также необходим опыт работы с автокодом. Однако опыт программирования на автокоде более важен для полного понимания работы вычислительных машин. Фактически нам почти не придется иметь дело с каким-либо конкретным автокодом. Несколько программ на языке ассемблера IBM 360, рассеянных по книге, могут быть опущены без ущерба для понимания остального материала.
Компилятор — это тоже программа, написанная на некотором языке. Следовательно, примеры частей компилятора должны быть даны на некотором языке программирования и для этой цели я выбрал язык, подобный АЛГОЛу, так как он наиболее удобен для чтения. Примеры, которые приводятся в книге, как правило, весьма коротки и в них довольно легко разобраться. Там, где многочисленные детали приводят к потере ясности рассматриваемой проблемы, я оставлял за собой право пользоваться словесным описанием вместо АЛГОЛа. Я очень тщательно проверил эти программы вручную,
ПРЕДИСЛОВИЕ
13
но хочу предупредить читателя, что не все они были отлажены на машине!
Краткое * описание используемого в книге нестандартного АЛГОЛа приводится в приложении. Это описание очень конспективное и предполагает предварительное знание АЛГОЛа. Если читатель не знаком с АЛГОЛом и синтаксическим описанием языков, ему следует читать это приложение не ранее, чем он изучит вторую главу.
Книга содержит гораздо больше материала, чем можно охватить в односеместровом курсе. Поэтому приведем рекомендуемый минимум:
Глава 1. Введение.
Глава 2. Грамматики и языки; опустить разд. 7.
Глава 3. Сканеры (программы лексического анализа); опустить разд. 4, 5, 6.
Глава 4. Методы нисходящего разбора; главным образом разд. 3. Глава 5. Грамматики простого предшествования.
Глава 8. Организация памяти в готовой программе; опустить разд. 6 и 9.
Глава 9. Организация таблиц символов.
Глава 10. Информация в таблице, символов; опустить разд. 2.
Глава 11. Внутренние формы исходной программы; опустить разд. 4 и 5.
Глава 12.' Введение в семантические программы.
Глава 13. Семантические программы для конструкций языка, подобного АЛГОЛу; опустить разд. 6.
Глава 14. Отведение памяти переменным в готовой программе; опустить разд. 3.
Глава 16. Интерпретаторы.
Глава 21. Советы разработчику компилятора.
Вы далее заметите, что из методов синтаксического анализа я придаю особое значение методу простого предшествования. И совсем не потому, что он считается наилучшим (наверное, это наихудший метод), а потому, что он самый легкий для изучения. Если на чтение курса будет отведено больше времени, то желательно, чтобы лектор добавил свои любимые методы восходящего синтаксического разбора: предшествование операторов (разд. 6.1); предшествование более высокого порядка (разд. 6.2); матрицы переходов
14
ПРЕДИСЛОВИЕ
(разд. 6.4); продукционный язык (гл. 7) и др. из числа тех, что не вошли,в минимум.
Можно также изменить порядок изучения материала. В самом деле, во время чтения курса лучше всего объединить изучение отдельных теоретических разделов с разделами более практического плана. Глава 8 об управлении памятью при работе готовой программы является совершенно независимой, тогда как гл. 9, 10, 11 и 16 по организации таблиц символов, внутреннему представлению исходной программы и интерпретаторам могут быть изучены в указанном порядке в любой момент времени.
Глава 21 заслуживает особого внимания. Она представляет собой подборку разнородных фактов и соображений, которые разработчик компиляторов должен непременно знать. Эти сведения собраны вместе по той причине, что они либо по тематике не подходят ни к одной из имеющихся глав, либо слишком важны, чтобы быть затерянными среди другого материала. Читателю следует время от времени просматривать эту главу и читать разделы, интересующие его в текущий момент.
Курс «Конструирование компиляторов» должен сопровождаться практическими занятиями. Студенты (группами от одного до трех человек) должны написать и отладить компилятор или интерпретатор для некоторого простого языка. Только в этом случае они действительно поймут, как работают компиляторы. С учебной точки зрения, интерпретаторы лучше, так как при этом студентам не надо заботиться о массе мелочей, связанных с машинным языком; идеи, несомненно, важнее мелочей. Именно поэтому транслятор следует программировать на языке высокого уровня. Мой опыт говорит, что язык PL/1 и язык, подобный АЛГОЛу, лучше отвечает этой цели, чем ФОРТРАН. Компиляторы, написанные на ФОРТРАНе, получаются более длинными и менее обозримыми. В тех случаях, когда в распоряжении студентов оказывается система построения трансляторов, ею следует обязательно воспользоваться. Чтобы работа проходила более разнообразно и творчески, нужно начать с базового простого языка, содержащего целые переменные, присваивания, выражения, метки и переходы, условные инструкции и, наконец, простые инструкции ввода и вывода. Затем в каждой группе язык расширяется путем добавления одной или двух новых возможностей. Например, можно добавить массивы, структуры, различ
ПРЕДИСЛОВИЕ
15
ные типы данных, блочную структуру, процедуры, макрокоманды, циклы.
Компилятор можно писать и отлаживать постепенно в процессе прослушивания курса. Сначала разрабатывается сканер, затем-синтаксический анализатор, далее программы для работы с таблицами символов и, наконец, программы семантической обработки. Интерпретатор можно разрабатывать и писать сразу после того, как будут пройдены относящиеся к нему главы. Таким образом, работа равномерно распределяется в течение семестра, а не концентрируется в конце.
Большинство ссылок на публикации собраны в последнем разделе каждой главы, хотя некоторые ссылки встречаются и в других разделах. Появление имени автора автоматически является ссылкой на публикацию, включенную в библиографию. Литература в библиографии приведена в алфавитном порядке по фамилиям авторов; причем работы каждого автора располагаются в хронологическом порядке. Если у автора более одной публикации, то ссылка представляется как <имя автора > [ <год >], где год — ссылка на год публикации. Например, Грис [68]. Если автор имеет более одной публикации в каком-то году, то первой будет приведена публикация с ссылкой [ <год > а], второй — [ <год > Ы и т. д. Таким образом, ссылка Флойд [64b] относится к статье Флойда о синтаксисе языков программирования.
За исключением заголовков и некоторых рисунков эта книга была отпечатана на IBM 360/65 с использованием программы ФОРМАТ, написанной Джеральдом М. Бернсом [69].
Автор также в долгу перед Джоном Эрманом, который внес несколько важных изменений и добавлений в программу. Использование программы ФОРМАТ облегчило редактирование оригинала и дало возможность снабжать слушателей материалами на различных этапах прохождения курса. Однако это заставило меня отклониться от принятых обозначений. Печатающее устройство, на котором была подготовлена книга, не имеет верхних и нижних индексов (кроме как от 0 до 9). Отсутствие индексов заставило меня писать последовательность из п символов как S[l], S[2]...Sin]1).
Когда смысл очевиден, мы просто записываем это как51,52, ...,Sn.
*) В переводе некоторые обозначения оригинала заменены на более традиционные— Прим. ред.
16
ПРЕДИСЛОВИЕ
Разделы этой книги возникли как конспекты курсов лекций, посвященных проектированию компиляторов и прочитанных в Стенфорде и Корнелле. Я воспользовался этим материалом в переработанном виде в сокращенных курсах в Мичиганской летней школе, в Анн-Арборе, в Корнелле и в 1970 году на Международном семинаре по системам программирования в Израиле. Я благодарен слушателям этих курсов за их критические замечания. Работая над книгой, я получал полезные советы от многих лиц; среди них Рихард Конвей, Джерри Фелдман, Джон Рейнолдс, Боб Роузин и Алан Шоу. Приношу мою искреннюю признательность Стиву Брауну, который внимательно прочитал рукопись, выявил много ошибок и сделал ценные пометки и критические замечания. В заключение, хочу поблагодарить мою жену, которая проявила поразительное терпение, выдержку и понимание в то время, когда я писал эту книгу.
Д. Грис
Глава 1
Введение
1.1.	КОМПИЛЯТОРЫ, АССЕМБЛЕРЫ, ИНТЕРПРЕТАТОРЫ
Транслятор — это программа, которая переводит исходную программу в эквивалентную ей объектную программу. Исходная программа пишется на некотором исходном языке, объектная программа формируется на объектном языке. Выполнение программы самого транслятора происходит во время трансляции.
Если исходный язык является языком высокого уровня, например таким, как ФОРТРАН, АЛГОЛ и КОБОЛ, и если объектный язык — автокод или некоторый машинный язык, то транслятор называется компилятором. Машинный язык иногда называют кодом машины, поэтому и объектная программа иногда называется объектным кодом. Трансляция исходной программы в объектную происходит во время компиляции, а фактическое выполнение объектной программы происходит во время выполнения готовой программы.
Ассемблер — это программа, которая переводит исходную программу, написанную на автокоде, или на языке ассемблера, на язык вычислительной машины. Автокод очень близок к машинному языку; действительно, большинство автокодных инструкций является точным символическим представлением команд машины. Более того, автокодные инструкции обычно имеют фиксированный формат, что позволяет легко их анализировать. В автокоде, как правило, отсутствуют вложенные инструкции, блоки и т. п.
Интерпретатор для некоторого исходного языка принимает исходную программу, написанную на этом языке, как входную информацию и выполняет ее. Различие между компилятором и интерпретатором состоит в том, что интерпретатор не порождает объектную программу, которая затем должна выполняться, а непосредственно выполняет ее сам.
Для того чтобы выяснить, как осуществить выполнение инструкций исходной программы, чистый интерпретатор анализирует ее всякий раз, когда она должна быть выполнена. Конечно же, это не эффективно и используется не очень часто. При программировании интерпретатор обычно разделяют на две фазы. На первой фазе интерпретатор анализирует всю исходную программу, почти так же, как это делает компилятор, и транслирует ее в некоторое внутреннее представление. На второй фазе это внутреннее представление
18
ГЛАВА I
исходной программы интерпретируется или ^выполняется. Внутреннее представление исходной программы разрабатывается для того, чтобы свести к минимуму время, необходимое для расшифровки или анализа каждой инструкции при ее выполнении.
Как указывалось выше, сам компилятор — это не что иное, как программа, написанная на некотором языке, для которой входной информацией служит исходная программа, а результатом является эквивалентная ей объектная программа. Исторически сложилось так, что компиляторы писались на автокоде вручную. Во многих случаях это был единственный доступный язык! Однако существует тенденция писать компиляторы на языках высокого уровня, поскольку при этом уменьшается время, затрачиваемое на программирование и отладку, а также обеспечивается удобочитаемость программы компилятора, когда работа завершена. Кроме того, теперь мы имеем много языков, разработанных специально для составления компиляторов. Эти так называемые «компиляторы компиляторов» являются некоторым подмножеством в «системах построения трансляторов» (СПТ). Мы обсудим их кратко в гл. 20.
Эта книга познакомит вас с конструированием компиляторов. В ней также найдут отражение вопросы, связанные с интерпретаторами, хотя им будет уделено сравнительно немного места, так как большинство методов, используемых при конструировании компиляторов, применимы также и при составлении интерпретаторов. Мы не будем обсуждать ассемблеры, но любой, кто понимает, как конструировать компиляторы, без труда поймет, что делает ассемблер, и как он это делает.
Вам не удастся найти в книге полного описания какого-либо компилятора. Идея заключается не в том, чтобы показать, как я пишу один конкретный компилятор, а в том, чтобы научить вас писать свой собственный компилятор. В книге вы, конечно, встретите примеры и объяснения многих (но далеко не всех, и я даже не позволил себе сказать — большинства) приемов и методов, используемых при конструировании компилятора. Примеры будут запрограммированы на нестандартном АЛГОЛе, краткое описание которого приводится в приложении. Если вы используете эту книгу в качестве учебного пособия, то вы, несомненно, напишете свой собственный компилятор или интерпретатор на АЛГОЛе, ФОРТРАНе, PL/1 или другом языке высокого уровня; и это — наилучший способ научиться конструировать компиляторы.
1.2.	КРАТКИЙ ОБЗОР ПРОЦЕССА КОМПИЛЯЦИИ
Компилятор должен выполнить анализ исходной программы, а затем синтез объектной программы. Сначала исходная программа разлагается на ее составные части; затем из них строятся части эк-
ВВЕДЕНИЕ
19
Бивалентной объектной программы. Для этого на этапе анализа компилятор строит несколько таблиц, которые используются затем как при анализе, так и при синтезе. На рис. 1.1 весь процесс показан более подробно. Пунктирные стрелки изображают информа-
Р и с. 1.1. Логические части компилятора.
ционные потоки, тогда как сплошные стрелки указывают порядок работы программ. Теперь кратко опишем различные части компилятора.
20
ГЛАВА 1
Информационные таблицы
При анализе программы из описаний, заголовков процедур, заголовков циклов и т. д. извлекается информация и сохраняется для последующего использования. Эта информация обнаруживается в отдельных точках программы и организуется так, чтобы к ней можно было обратиться из любой части компилятора. Например, при каждом использовании идентификатора необходимо знать, как был описан этот идентификатор и как он использовался в других местах программы. Что конкретно следует хранить, зависит, конечно, от исходного языка, объектного языка и сложности компилятора. Но в каждом компиляторе в той или иной форме используется таблица символов (иногда ее называют списком идентификаторов или таблицей имен). Это таблица идентификаторов, встречающихся в исходной программе, вместе с их атрибутами. К атрибутам относятся тип идентификатора, его адрес в объектной программе и любая другая информация о нем, которая может понадобиться при генерации объектной программы.
Какую еще другую информацию следует собирать? Нам наверняка потребуется таблица констант, используемых в исходной программе. В эту таблицу будет включена сама константа и соответствующий ей адрес в объектной программе. Нам также может понадобиться таблица заголовков for-циклов, отображающая структуру вложений циклов и хранящая переменные циклов; понадобится информация об инструкциях EQUIVALENCE для языков, подобных ФОРТРАНу, и размеры объектных программ для каждой компилируемой процедуры. При разработке компилятора невозможно определить вид и содержание информации, которую следует собирать до тех пор, пока не будут достаточно обстоятельно продуманы команды объектной программы для каждой инструкции исходной программы и сама синтезирующая часть компилятора. Многое зависит от глубины задуманной оптимизации программы.
Сканер
Сканер — самая простая часть компилятора, иногда также называемая лексическим анализатором. Сканер просматривает литеры исходной программы слева направо и строит символы программы — целые числа, идентификаторы, служебные слова, двухлитерные символы, такие, как ** и // и т. д. (В литературе иногда вместо термина символ используют термины элемент и атом.) Символы передаются затем на обработку фактическому анализатору. На этой стадии может быть исключен комментарий. Сканер также может заносить идентификаторы в таблицу символов и выполнять другую простую работу, которая фактически не требует анализа исходной программы. Он может выполнить большую часть работы по макро
введение
21
генерации в тех случаях, когда требуется только текстовая подстановка.
Обычно сканер передает символы анализатору во внутренней форме. Каждый разделитель (служебное слово, знак операции или знак пунктуации) будет представлен целым числом. Идентификаторы или константы можно представить парой чисел. Первое число, отличное от любого целого числа, использующегося для представления разделителя, характеризует сам «идентификатор» или «константу»; второе число является адресом или индексом идентификатора или константы в некоторой таблице. Это позволяет в остальных частях компилятора работать эффективно, оперируя с символами фиксированной длины, а не с цепочками литер переменной длины.
Синтаксический и семантический анализаторы
Анализаторы выполняют действительно сложную работу по расчленению исходной программы на составные части, формированию ее внутреннего представления и занесению информации в таблицу символов и другие таблицы. При этом также выполняется полный синтаксический и семантический контроль программы.
Обычный анализатор представляет собой синтаксически управляемую программу. В действительности стремятся отделить синтаксис от семантики настолько, насколько это возможно. Когда синтаксический анализатор (распознаватель) узнает конструкцию исходного языка, он вызывает соответствующую семантическую процедуру. или семантическую программу, которая контролирует данную конструкцию с точки зрения семантики и затем запоминает информацию о ней в таблице символов или во внутреннем представлении программы. Например, когда распознается описание переменных, семантическая программа проверяет идентификаторы, указанные в этом описании, чтобы убедиться в том, что они не были описаны дважды, и заносит их вместе с атрибутами в таблицу символов.
Когда встречается инструкция присваивания вида (переменная >: = (выражение >
семантическая программа проверяет переменную и выражение на соответствие типов и затем заносит инструкцию присваивания в программу во внутреннем представлении.
Внутреннее представление исходной программы
Внутреннее представление исходной программы в значительной степени зависит от его дальнейшего использования. Это может быть дерево, отражающее синтаксис исходной программы. Это может быть исходная программа, в так называемой польской записи. Используется еще одна форма — список тетрад (оператор, операнд, one-
22
ГЛАВА 1
ранд, результат) в порядке их выполнения. Например, присваивание «А=В+С* D» будет представлено как
с, D, Т1 + , в, Tl, Т2 = ,Т2, А,
где Т1 и Т2 — временные переменные, образованные компилятором. Операндами в приведенном примере будут не сами символические имена, а указатели на те элементы (или их индексы) в таблице символов, в которых описаны эти операнды.
Подготовка к генерации команд
Перед генерацией команд обычно необходимо некоторым образом обработать и изменить внутреннюю программу. Кроме того, должна быть распределена память под переменные готовой программы. Если мы имеем дело с компилятором с ФОРТРАНа, то должны быть обработаны инструкции EQUIVALENCE и COMMON. Одним из важных моментов на этом этапе является оптимизация программы с целью уменьшения времени ее работы.
Генерация команд
По существу, на этом этапе происходит перевод внутреннего представления исходной программы на автокод или на машинный язык. Это, по-видимому, наиболее хлопотная и кропотливая часть компилятора, хотя и наиболее понятная. Предположим, что внутреннее представление имеет вид тетрад, какописано выше, и мы генерируем команды для каждой тетрады по порядку. На языке ассемблера IBM 360х) для трех приведенных выше тетрад можно сгенерировать следующие команды:
L	5, С	Загрузить С в регистр 5.
М	4, D	Результат умножения в регистрах 4, 5.
А	5, В	Прибавить В к результату умножения.
ST	5, А	Запомнить результат.
В интерпретаторе эта часть компилятора заменяется программой, которая фактически выполняет (или интерпретирует) внутреннее представление исходной программы. Причем само внутреннее представление в этом случае мало чем отличается от того, которое получается при компиляции.
На рис. 1.1 показаны скорее логические связи между отдельными частями компилятора, чем последовательность их работы. Все четы
х) Необходимые сведения о системе команд и языке ассемблера для машин IBM 360 читатель может найти в книге Джермейна [71].— Прим. ред.
ВВЕДЕНИЕ
23
ре логически последовательных процесса: сканирование, анализ, подготовку к генерации и генерацию команд — можно выполнять в том порядке, который показан на рис. 1.1, или их можно выполнять параллельно, с определенной взаимной синхронизацией. Одним из критериев, определяющих выбор в данном случае, является объем доступной памяти. Часто выгодно или даже необходимо иметь
Рис. 1.2. Однопроходный КО?> п [ЛЯТСр.
несколько проходов (т. е. несколько раз вводить информацию в память машины). К счастью, при этом «другую информацию» удается хранить в памяти и тем самым экономить время на ввод-вывод. Другие критерии определяются целями, которые преследуются при разработке компилятора. Насколько быстрым должен быть сам компилятор? Насколько быстрой должна быть готовая программа? Какие средства отладки должны быть предусмотрены в готовой программе? Еще одним фактором является количество людей, занятых в разработке. Чем больше людей, тем больше, по-видимому, будет проходов, чтобы каждый мог отвечать за отдельную и самостоятельную часть компилятора.
Справедливым является и то, что не все этапы должны быть обязательно осуществлены. В однопроходном компиляторе нет необходимости во внутреннем представлении программы, в то время как этапы подготовки и генерации команд растворяются в семантических программах. На рис. 1.2 представлена типичная однопроходная схема. Синтаксический анализатор вызывает сканер, когда необходим новый символ, и вызывает процедуру, когда конструкция распознана. Эта процедура осуществляет семантическую проверку, распределение памяти и генерирует команды, перед тем как возвратиться к разбору.
24
ГЛАВА 1
Однако не все языки имеют такую структуру, которая допускает однопроходную трансляцию.
Естественно, возникает вопрос: в чем заключаются главные трудности реализации компилятора? Сканер — весьма прост и хорошо изучен. Синтаксические анализаторы, если речь идет о простых формальных языках, также довольно хорошо изучены. В действительности эту часть можно в значительной степени автоматизировать. (С тех пор, как синтаксис был формализован, большинство исследований по созданию компиляторов касалось именно синтаксиса, а не семантики.) Наиболее трудными и запутанными частями компилятора являются семантический анализ, программы подготовки генерации и программы генерации команд. Эти три части взаимозависимы, должны в значительной степени разрабатываться совместно и могут коренным образом измениться при переходе с одного объектного языка на другой или с одной машины на другую.
После этого краткого введения мы готовы приступить к изложению нашей первой темы «Теория формальных языков и ее применение к конструированию компиляторов». Если у вас есть желание (это не является необходимым), вы можете просмотреть следующий раздел, в котором приведены несколько примеров существующих компиляторов, для того чтобы закрепить представленный в этой главе материал.
1.3.	ПРИМЕРЫ СТРУКТУР КОМПИЛЯТОРОВ
Мы приводим здесь четыре примера существующих компиляторов. Мы выбрали два компилятора с языка АЛГОЛ, чтобы показать, насколько радикально может повлиять на проект вычислительная машина, для которой создается компилятор. Затем мы описываем два различных компилятора с ФОРТРАНа для одной и той же вычислительной машины, чтобы показать, как влияют на проект цели, преследуемые при создании компилятора.
Компилятор ALCOR Illinois 7090
Это четырехпроходный компилятор, созданный для машин IBM 7090-7040 (см. Грис [68]); исходный язык компилятора—АЛГОЛ 60, из которого исключены собственные значения.
Компилятор тщательно оптимизирует вычисление индексов в циклах (описанный ниже компилятор с ФОРТРАНа выполняет гораздо более полную оптимизацию программы). Результатом трансляции является программа на языке машины 7090 в виде двоичной колоды, загружаемой в машину системным загрузчиком.
ВВЕДЕНИЕ
25
Проход	Работа
1	Лексический и частичный синтаксический анализ с целью получения таблицы символов для всех идентификаторов с учетом блочной структуры. Каждый символ заменяется целым числом фиксированной длины.
2	Полный синтаксический и семантический анализ. Подготовка для оптимизации переменных с индексами. Распределение памяти в готовой программе (это, по существу, уже синтез).
3	Оптимизация переменных с индексами и генерация команд.
4	Получение двоичной колоды и вывод сообщений об ошибках, если они есть.
Компилятор для машины Gier с АЛГОЛа
Компилятор создан для машины, располагающей памятью лишь в 1024 42-разрядных слова и магнитным барабаном на 128 000 слов (см. Наур [63 b]). Несмотря на малый размер машины, компилятор транслирует по существу полный АЛГОЛ. Легко понять, почему компилятор пришлось расчленить на 9 проходов, приведенных ниже. Проходы 1 и 2 осуществляют лексический анализ, проход 3 — синтаксический аналйз, проходы 4, 5 и 6 выполняют семантический анализ (надо заметить, что здесь же осуществляется часть синтеза — распределение памяти в готовой программе). Проходы 7 и 8 представляют синтезирующую часть компилятора. Проход 9 является специфической частью этого компилятора, он необходим для получения эффективной объектной программы.
Проход	Работа
1	Сканер, который переводит разделители (все символы, кроме констант и идентификаторов) в их внутреннее представление.
2	Замена всех идентификаторов в исходной программе целыми числами фиксированной длины.
3	Синтаксический анализ. Введение дополнительных разделителей и замена существующих для облегчения последующей обработки.
4	Построение таблицы символов. Для каждого блока идентификаторы, описанные или специфицированные в блоке, запоминаются в таблице вместе со своими атрибутами.
5	Распределение памяти для переменных в готовой программе. Кроме того, каждый идентификатор исходной
26
ГЛАВА 1
программы (представленный целым числом) заменяется четырьмя байтами, которые соответственно представляют тип и вид, номер блока, адрес в готовой программе, число параметров или индексов (если требуется).
6	Контроль типа и вида всех идентификаторов и других операндов. Преобразование исходной программы в польскую запись.
7	Генерация программы.
8	Заключительная работа по формированию адресов
в некоторых командах. Сегментация по трактам магнитного барабана. Получение окончательной программы.
9	Компоновка программных сегментов на магнитном барабане.
Компиляпгор/3&) WATFOR
WATFOR представляет собой некоторую систему, состоящую из подмонитора и компилятора, реализованную на машине IBM 360, для организации пакетной обработки программ, написанных на языке ФОРТРАН IV (см. Кресс и др.). Цель системы — обеспечить быстрый пропуск программ, написанных на ФОРТРАНе и имеющих относительно небольшой размер и малое время выполнения — такие программы характерны для учебных институтов. Поэтому система целиком размещается в оперативной памяти, и единственными операциями ввода-вывода являются ввод исходной программы и вывод результата компиляции. Минимум памяти, необходимый системе, составляет 128 000 байтов (32 000 слов).
Компилятор не проводит оптимизации и получает относительно неэффективную объектную программу на языке машины в абсолютных адресах, которую система тотчас выполняет. Именно в этом источник экономии — не теряется время на редактирование связей и загрузку.
Компилятор является по существу однопроходным. Сканер переводит одну инструкцию ФОРТРАНа во внутреннее представление, определяет тип инструкции и вызывает соответствующую семантическую программу для ее обработки. Эта программа анализирует синтаксис и семантику инструкции и сразу же генерирует для нее команды, т. е. анализ и синтез выполняются вместе. Для ФОРТРАНа окончательное распределение памяти в готовой программе не может быть осуществлено до тех пор, пока не будут обработаны инструкции COMMON и EQUIVALENCE. Поэтому адреса в командах программы не являются адресами памяти, а указывают на элементы таблицы символов для соответствующих переменных, описанных в
ВВЕДЕНИЕ
27
программе, или временных переменных. После того как завершится главный проход, вызывается второй, небольшой проход, который распределяет память для всех переменных и затем заменяет в объектной программе каждый указатель в таблицу символов на соответствующий адрес памяти.
Компилятор уровня Н для ФОРТРАНа IV на IBM 360
Задача компилятора (см. IBM (а)) состоит в получении максимально эффективной объектной программы, и с этой задачей он справляется блестяще. В связи с оптимизацией программы сам компилятор работает очень медленно. Компилятор имеет следующие проходы.
Проход	* Работа
1	Сканирование и синтаксический анализ, формирование таблицы символов и внутреннего представления исходной программы в виде пар (оператор—операнд).
2	(Фактически три отдельных прохода.)
а) Обработка инструкций COMMON и EQUIVALENCE, b) Замена внутреннего представления исходной программы на тетрады (оператор, операнд, операнд, результат).
с) Распределение памяти в готовой программе.
3	Оптимизация программы.
а)	Удаление лишних операций, вынесение, где возможно, операций из циклов и т. д.
Ь)	Оптимизация переходов.
4	Окончательная генерация объектной программы.
Глава 2
Г рамматики и языки
Для читателя, не знакомого с теорией формальных языков, эта глава может оказаться самой трудной в книге. Свыше сорока терминов, таких, как «фраза», «предложение», «язык» и «неоднозначность», которые мы часто употребляем неформально, приобретают точный смысл. Список терминов в порядке их определения приводится в конце книги. Когда вы прочтете всю главу, непременно просмотрите этот список и вернитесь к определениям, которыми вы недостаточно овладели. Полное понимание этой главы значительно упростит чтение последующего материала книги и позволит прочесть книгу с большей пользой.
2.1. ОБСУЖДЕНИЕ ГРАММАТИК
Рассмотрим предложение «The big elephant ate the peanut»1). Если мы знаем английский, то поймем, что это предложение английского языка. Его можно изобразить в виде схемы, которая представлена на рис. 2.1.
Схема предложения, такая, например, как на рис. 2.1, называется синтаксическим деревом. Оно описывает синтаксис, или структуру, предложения, разлагая его на составные части. Таким образом, мы видим, что <предложение> состоит из (подлежащее), за которым следует (сказуемое); (подлежащее) состоит из (артикль), за которым следует (прилагательное), за которым в свою очередь следует (существительное) и т. д.
Для того чтобы описать структуру, мы использовали новые символы — «синтаксические единицы» или «синтаксические классы», такие, как (предложение). Эти символы заключаются в угловые скобки ( и ), чтобы отличить их от слов самого языка.
Мы узнаем, что «The big elephant ate the peanut» является предложением, либо основываясь на интуиции, либо применяя соответст- I вующие правила грамматики, выученные в школе; конечно, каждый
х) «Большой слон съел орех».
ГРАММАТИКИ И ЯЗЫКИ
29
из нас мог бы составить неформальное синтаксическое дерево для любого простого английского предложения. Однако для того, чтобы иметь возможность механически разбирать предложения, мы должны дать точные формальные правила, которые описывают структуру предложений. Для этой цели мы создадим метаязык, т. е. некий язык, на котором можно описывать другие языки. В английских школах в курсе французского языка английский язык использует-
<предложение>
<подлежаш,ее>
<артиклъ>	<прилагате льное) ^уществительноО
the	big	elephant
(большой)	(слон)
<спазуемое>
---------.------------1
Спрямое дополнение>
<глагол> <артикль> <суцес!пеи)пелъное>
ate the	peanut
(съел)	(орех)
Рис. 2.1. Синтаксическое дерево.
ся при описании французского языка. Следовательно, английский играет роль метаязыка. В учебниках английского языка английский язык используется как метаязык для описания самого себя.
В настоящий момент нас в основном интересует описание синтаксиса (структуры) языков программирования, а не их семантики (смысла); схемы, такие, как на рис. 2.1, не передают смысла предложения. Такое синтаксическое описание мы называем грамматикой языка.
Метаязык, который мы будем использовать, впервые был предложен Хомским [561 для описания естественных языков. Конкретная система записи, которой мы воспользуемся, принадлежит Бэ-кусу [59]. По мнению Хомского, этот метаязык полезен лишь для описания небольших подмножеств, состоящих из простых предложений, и поэтому лингвисты обратились к более мощным метаязыкам. Он не совсем удовлетворителен даже для описания структуры довольно простых формальных языков, таких, как АЛГОЛ или ФОРТРАН. Однако мы все же воспользуемся им, поскольку в нем наилучшим образом сочетается мощность и целесообразность практического использования.
Еще раз рассмотрим предложение «The big elephant ate the peanut». Как мы отмечали, рис. 2.1 показывает, что (предложение> состоит из (подлежащее), за которым следует (сказуемое). Если сократить фразу «может состоять из», заменив ее символом :: —, то наша грамматика могла бы содержать, например, такие правила: (предложение)	(подлежащее) <сказуемое>
(подлежащее)	:: — (артикль) (прилагательное) (суще-
ствительное)
30
ГЛАВА 2
<артикль>
<прилагательное>
<сказуемое>
<глагол>
<прямое дополнение>
<существительное>
<существительное>
= the
= big
= <глагол> <прямое дополнение>
= ate
= <артикль> <существительное>
= peanut
= elephant
Заметим, что грамматика может содержать более одного правила, в котором описывается образование конкретной синтаксической единицы. Например, на рис. 2.1 есть два правила, которые показывают, из чего может состоять <существительное>.
Если имеется множество правил, то ими можно воспользоваться для того, чтобы вывести, или породить, предложение по следующей схеме. (По этой причине такие правила часто называют правилами вывода, или продукциями.) Начнем с синтаксической единицы <пред-ложение>, найдем правило, в котором <предложение> слева от : : = , и подставим вместо <предложение> цепочку, которая расположена справа от : : = , т. е.
<предложение> => <подлежащее> <сказуемое>
Таким образом, мы заменяем синтаксическую единицу на одну из цепочек, из которых она может состоять. Повторим процесс. Возьмем одну из синтаксических единиц в цепочке <подлежащее> <сказуемое>, например <подлежащее>; найдем правило, где <под-лежащее> находится слева от : : = , и заменим <подлежащее> в исходной цепочке на соответствующую цепочку, которая находится справа от : :=. Это дает
<подлежащее> <сказуемое> => <артикль> <прилагательное>
<существительное> <сказуемое>
Символ “=>” означает, что один символ слева от => в соответствии с правилом грамматики заменяется цепочкой, находящейся справа от =>. Полный вывод предложения будет таким:
<предложение> => <подлежащее> <сказуемое>
=> <артикль> <прилагательное> <существи-тельное> <сказуемое>
=> the <прилагательное> <существительное>
<сказуемое>
=> the big <существительное> <сказуемое>
=> the big elephant <сказуемое>
=> the big elephant <глагол> <прямое дополнен ие>
ГРАММАТИКИ И ЯЗЫКИ
31
=> the big elephant ate <прямое дополнение> => the big elephant ate <артикль> <существи-тельное>
=> the big elephant ate the <существительное> the big elephant ate the peanut
Этот вывод предложения запишем сокращенно, используя новый сим-ВОЛ —I
<предложение> =>+ the big elephant ate the peanut
На каждом шаге можно заменить любую синтаксическую единицу. В приведенном выше выводе мы всегда заменяли самую левую из них. Обратите также внимание на то, что такое правило, как <пред-ложение> : : = <подлежащее> <сказуемое>, можно использовать для описания многих различных предложений; для этого необходимо только иметь различные способы образования синтаксических единиц <подлежащее> и <сказуемое>.
Из семи правил
<предложение>	: <подлежащее> <подлежащее>	: <подлежащее>	: <сказуемое>	: <сказуемое>	: <сказуемое>	:	: = <подлежащее> <сказуемое> : =We : =Не : =1 : =гап : =sat : =ate
мы можем образовать целых девять предложений!
We ran Не ran I ran We ate He ate I ate We sat He sat I sat
Одно из назначений грамматики как раз и состоит в том, чтобы описать все предложения языка с помощью приемлемого числа правил. Это важно, если учесть тот факт, что обычно количество предложений в языке бесконечно.
После такого введения мы почти готовы описать формальные понятия грамматик и языков, но в следующем разделе нам все-таки придется прежде определить некоторые термины, которыми мы пока пользовались неформально.
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.1
1. Найдите слова «язык», «мета», «метаязык», «синтаксис» и «семантика» в хорошем словаре.
32
ГЛАВА 2
2. Нарисуйте синтаксические деревья для предложений “John ate the big peanut” (Джон съел большой орех) “John ate the big brown peanut” (Джон съел большой коричневый орех) “John ate the salted big brown roasted peanut” (Джон съел соленый большой коричневый жареный орех). В английском языке любое существительное может быть определено любым числом прилагательных. Теперь попытайтесь дать только два правила для нового синтаксического класса, скажем <группа существительного>, позволяющих вывести <существитель-ное>, которому предшествует любое количество прилагательных (в том числе ни одного).
2.2.	СИМВОЛЫ И ЦЕПОЧКИ
Мы неформально определяем язык как подмножество множества всех предложений из «слов» или символов некоторого основного словаря. И опять-таки нас не интересует смысл этих предложений. Например, английский язык состоит из предложений, которые являются последовательностями, составленными из слов (if, he, is и т. д.), и знаков пунктуации (например, запятые, точки, скобки). Язык программирования АЛГОЛ состоит из программ, которые являются последовательностями, составленными из таких символов, как if, begin, end, знаков пунктуации, букв и цифр. Язык четных целых чисел состоит из последовательностей, составленных из цифр О, 1....9, в которых последней цифрой должны быть 0, 2, 4, 6 или 8.
Алфавит — это непустое конечное множество элементов. Назовем элементы алфавита символами. Всякая конечная последовательность символов алфавита А называется цепочкой1). Вот несколько цепочек «в алфавите» А={а, b, с}: a, b, с, ab и ааса. Мы также допускаем существование пустой цепочки Л, т. е. цепочки, не содержащей ни одного символа. Важен порядок символов в цепочке; так, цепочка ab не то же самое, что Ьа, и abca отличается от aabc. Длина цепочки х (записывается как |х|) равна числу символов в цепочке. Таким образом,
|Л|=0, |а|=1, |abb|=3.
Заглавные буквы М, N, S, Т, U, ... используются как переменные или имена символов алфавита, в то время как строчные буквы t, u, v, w ... используются для обозначения цепочек символов. Таким образом, можно написать
x=STV, и это означает, что х является цепочкой, состоящей из символов S, Т и V именно в таком порядке.
*) Вместо термина цепочка (в английском языке — string) некоторые авторы используют термины строка или строчка.— Прим. ред.
ГРАММАТИКИ И ЯЗЫКИ
33
Если х и у — цепочки, то их катенацией1) ху является цепочка, полученная путем дописывания символов цепочки у вслед за символами цепочки X. Например, если x=XY, y=YZ, то xy=XYYZ и yx=YZXY. Поскольку Л — цепочка, не содержащая символов, в соответствии с правилом катенации для любой цепочки х мы можем написать
Лх=хЛ=х.
Если z=xy — цепочка, то х — голова, а у — хвост цепочки z. И, наконец, х — правильная голова, если у — не пустая цепочка (у не Л), у — правильный хвост, если х — не пустая цепочка. Таким образом, если x=abc, то Л, a, ab и abc суть головы х, и к тому же все они, кроме abc,— правильные головы.
Множества цепочек в алфавите обычно обозначаются заглавными буквами А, В....Произведение АВ двух множеств цепочек А и В
определяется как
АВ={ху|х€А, а у£В}
и читается как «множество цепочек ху, такое, что х из А, а у из В». Например, если А={а, Ь} и В={с, d}, то множество АВ={ас, ad, be, bd}. Поскольку Лх=хЛ=х справедливо для любой цепочки х, мы имеем
{Л}А=А{Л}=А.
Заметьте, что здесь символ Л заключен в фигурные скобки. Произведение определено для множеств, тогда как Л является символом, а не множеством. {Л} — это множество, состоящее из пустого символа Л.
Мы можем теперь определить степени цепочек. Если х — цепочка, то х° — пустая цепочка Л, х1=х, хг=хх, х3=ххх, и в общем случае хп определяется как
хххх ... хх
п раз
Для п>-0 имеем хп=ххп-1=(хп-1)х.
Так же можно определить степени алфавита А:
А°={Л}, А1=А, АП=ААП-1 для п>0.
Используя это, определим две последние операции в этом разделе — итерацию А* множества А и усеченную итерацию А+ множества А:
А+=А! U А8 и . . . и A11 U . . .,
А*=А° U А+.
*) Говорят также конкатенация.— Прим. ред.
2 д. Грио
34
ГЛАВА 2
Таким образом, если А={а, Ь), то А* включает цепочки
A, a, b, аа, ab, ba, bb, ааа, aab ....
Заметим, что А+=АА* = (А*)А.
Примеры:
Пусть z=abb. Тогда |z|=3. Головы z есть A, a, ab, abb. Правильные головы z есть A, a, ab. Хвосты z — это A, b, bb, abb. Правильные хвосты z — это A, b, bb.
Пусть х=а, z=abb. Тогда
zx=abba,	xz—aabb,
z°=A,	г1=аЬЬ,	z1 2=abbabb, z3 4=abbabbabb,
|z°|=0,	|z1|=3,	|z2|=6,	|z»|==9.
Пусть S={a, b, с}. Тогда
S+ = {a, b, c, aa, ab, ac, ba, bb, be, ca, cb, cc, aaa, . . .}.
S* = {A, a, b, c, aa, ab, ac, . . .}.
Иногда удобнее и, как правило, нагляднее писать х. . . вместо ху,
если нас не интересует у — остальная часть цепочки. Таким образом, три точки «. . .» обозначают любую возможную цепочку, включая и пустую. Наиболее часто встречаются следующие обозначения:
Обозначение	Смысл
Z — X...	x—голова цепочки z. Нам безразличен хвост.
z= .. .X	х — хвост цепочки z. Нам безразлична голова.
z= .. .х... z = S... z = .. .S z = .. .S...	х встречается где-то в цепочке z. Символ S—первый символ цепочки z. Символ S — последний символ цепочки z. Символ S встречается где-то в цепочке z.
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.2
1. Дайте определения терминов «цепочка» и «катенация».
2. Пусть А={$}. Пусть z=$. Выпишите следующие цепочки и их длины: z, zz, z2, z5, z°. Каким будет множество А*?
3. Пусть А={0, 1, 2}. Пусть х=01, у=2 и z=011. Выпишите следующие цепочки, их длины, головы и хвосты: ху, yz, xyz, х*, (х3)(у2), (ху)2, (ухх)3.
4. Пусть А={0, 1,2}. Напишите 7 самых коротких цепочек мно-
жества А+ и множества А*.
ГРАММАТИКИ И ЯЗЫКИ
35
2.3.	ФОРМАЛЬНОЕ ОПРЕДЕЛЕНИЕ ГРАММАТИКИ И ЯЗЫКА
Теперь мы в состоянии формализовать понятие правила, или, как иногда говорят, продукции, и абстрактно определить грамматику и язык, используя эти правила.
(2.3.1).	Определение. Продукцией или правилом подстановки называется упорядоченная пара (U, х), которая обычно записывается так:
U : :==х,
где U — символ, ах — непустая конечная цепочка символов.
U называется левой частью, ах — правой частью продукции. Вместо термина продукция в дальнейшем мы чаще будем пользоваться .более коротким термином — правило.
(2.3.2.). Определение. Грамматикой G [Z] называется конечное, непустое множество правил; Z — это символ, который должен встретиться в левой части по крайней мере одного правила. Он называется начальным символом х). Все символы, которые встречаются в левых и правых частях правил, образуют словарь N.
Если из контекста ясно, какой символ является начальным символом Z, мы будем писать G вместо G [Z].
(2.3.3).	Пример. Грамматика G1 [<число>] содержит следующие 13 правил:
(1) <число> :	: = <чс>
(2) <чс>	:	: = <чс> <цифра>
(3) <чс>	:	:=<цифра>
(4) <цифра>	: =0
(5) <цифра>	: =1
(6) <цифра> :	: =2
(7) <цифра> :	: =3
(8) <цифра> :	: =4
(9) <цифра> :	: =5
(10) <цифра> :	: =6
(11) <цифра> :	: =7
(12) <цифра> :	: =8
(13) <цифра> :	: =9
V = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, <цифра>, <чс>, <число>}.
(2.3.4).	Определение. В заданной грамматике Q символы, которые встречаются в левой части, правил, называются нетерми
х) Начальный символ называют также аксиомой или помеченным символом.— Прим. ред.
36
ГЛАВА 2
налами или синтаксическими единицами языка. Они образуют множество нетерминальных символов VN. Символы, которые не входят в множество VN, называются терминальными символами (или терминалами). Они образуют множество VT.
Таким образом, V=VN(jVT. Как правило, нетерминалы мы будем заключать в угловые скобки < и >, чтобы отличить их от терминалов. В грамматике G1 (пример 2.3.3) символы 0, 1,2, 3, 4, 5, 6, 7, 8, 9 — терминальные, а <число>, <чс> и <цифра> — нетерминальные. Мы будем пользоваться надстрочными литерами в том случае, когда нам надо отличить различные вхождения одного и того же нетерминала. Так, можно написать <чсх>: : = <чс2> <цифра> вместо <чс>: :=<чс><цифра>.
Множество правил U : :=х, U : :=у, . . ., U : :=z с одинаковыми левыми частями будем записывать сокращенно как
U : :=х|у|. . .|z
Например, грамматику G1 можно записать следующим образом:
<число> : :=<чс>
<чс> : :=<чс> <цифра> | <цифра>
<цифра> : :=0|1[2|3|4|5|6|7|8|9
Эта форма записи называется нормальной формой Бэкуса (сокращенно НФБ) или формой Бэкуса — Наура. Впервые она была разработана Бэкусом [59] для описания АЛГОЛа в сообщении о языке АЛГОЛ 60 (см. Наур [63а]). Наур был редактором этого сообщения. Существует несколько других способов записи для описания формальных языков. Мы их обсудим позднее.
Теперь, когда есть грамматика, как определить язык, соответствующий этой грамматике? Что является предложением этого языка? Для того чтобы ответить на эти вопросы, нам надо определить символы “=>” и “=>+”, которыми мы интуитивно пользовались в разд. 2.1 при выводе предложений. Неформально мы пишем v=>w, если можно вывести w из v, заменив нетерминальный символ в v на соответствующую правую часть некоторого правила.
(2.3.5).	Определение. Пусть G — грамматика. Мы говорим, что цепочка v непосредственно порождает цепочку w, и обозначаем это как
v w,
если для некоторых цепочек х и у можно написать v=xUy, w=xuy,
где U : :=и — правило грамматики G. Мы также говорим, что w непосредственно выводима из v или что w непосредственно приводится (редуцируется) к v.
ГРАММАТИКИ И ЯЗЫКИ
37
Цепочки х и у могут, конечно, быть пустыми. Следовательно, для любого правила U :: —и грамматики G имеет место U и. В следующей таблице даны некоторые примеры непосредственных выводов, при этом используется грамматика G1 (пример 2.3.3) и обозначения из предыдущего определения.
V	W	Использованные правила	X	У
<число>	=> <ЧС>	1	А	А
<чс>	=> <чс> <цифра>	2	А	А
<чс> <цифра>	=> <цифра> <цифра>	3	А	<цифра>
<цифра> <цифра>	=> 2 <цифра>	6	А	<цифра>
2 <цифра>	=> 22	6	2	А
Рис. 2.2. Примеры непосредственных выводов.
(2.3.6).	Определение. Говорят, v порождает w или w приводится к v, что записывается как v=>+ w, если существует последовательность непосредственных выводов
v=uO => ul => u2z>. . . => u[n]=w,
где n>0. Эта последовательность называется выводом длины п. Говорят также, что цепочка w является словом для v. И, наконец, пишут v=>* w, если v=>+ w или v=w.
Приведем пример вывода. Взгляните на первую строку рис. 2.2. Из нее видно, что <число>=>+ <чс>; длина вывода равна 1. Используя еще и строку 2 этой таблицы, получаем: <число> =>+<чс> <цифра>; длина вывода равна 2. Если мы просмотрим вниз все строки рис. 2.2, то увидим, что
<число> => <чс> => <чс> <цифра> => <цифра> <цифра> 2<цифра> ^22
Таким образом, <число> =>+ 22 и длина вывода равна 5.
Заметьте, что пока в цепочке есть хотя бы один нетерминал, из -нее можно вывести новую цепочку. Однако если нетерминальные символы отсутствуют, то вывод надо закончить. Поэтому называют «терминалом» (terminal — заключительный, конечный) символ, который не встречается в левой части ни одного из правил.
Каким будет язык, описанный грамматикой О[<число>]? В следующем определении утверждается, что этим конкретным языком является множество последовательностей из одной и более цифр.
(2.3.7).	Определение. Пусть G [Z] — грамматика. Цепочка х называется сентенциальной формой, если х выводима из начального символа Z, т. е. если Z=>*x. Предложение — это сентенциальная форма, состоящая только из терминальных символов. Язык
38
ГЛАВА 2
L (G[Z]) — это множество предложений:
L(G)={x|Z=>*x и x£VT+}.
Таким образом, язык — это просто подмножество множества всех терминальных цепочек, т. е. цепочек в VT. Структура предложения задается грамматикой. Несколько различных грамматик могут, однако, порождать один и тот же язык. Мы будем часто говорить «предложение грамматики» вместо «предложение языка, определенного грамматикой».
(2.3.8).	Пример. Определим грамматику для языка, содержащего вариации уже знакомого нам предложения “the big elephant ate the peanut”. Эта грамматика G2 1<предложение>) имеет вид
<предложение>	:: =	<подлежащее> <сказуемое>
<подлежащее>	:: =	<артикль> <прилагательное>
<существительное> | <местоимение>
<сказуемое>	::— <глагол> <прямое дополнение>
<прямое дополнение> :: — <артикль> <существительное> <местоимение>	::	=	he
<артикль>	::	=	the
<прилагательное>	::	=	big
<глагол>	::	=	ate
<существительное>	::	=	elephant | peanut
Множество терминальных символов есть {he, the, big, ate, elephant, peanut}. Язык L (G2) является множеством последовательностей этих терминальных символов, выводимых из начального символа <предложение>. Вот некоторые из таких предложений:
he ate the peanut
he ate the elephant
the big elephant ate the elephant
Ниже следует важное определение, и читатель не должен переходить к изучению дальнейшего материала до тех пор, пока смысл этого определения не будет ему полностью ясен.
(2.3.9).	Определение. Пусть G[Z1 — грамматика. И пусть w=xuy — сентенциальная форма. Тогда и называется фразой сентенциальной формы w для нетерминального символа U, если Z=>* xUy и U =>+ и; и далее, и называется простой фразой, если Z =>* xUy и U=>u.
Следует быть осторожным с термином фраза. Тотфакт, что U => + и, вовсе не означает, что и является фразой сентенциальной формы хиу; необходимо также иметь Z =>* xUy. В качестве иллюстрации
ГРАММАТИКИ И ЯЗЫКИ
39
рассмотрим сентенциальную форму <чс>1 грамматики G1 (пример 2.3.3). Значит ли, что <чс> является фразой, если существует правило <число>: :=<чс>? Конечно, нет, поскольку не возможен вывод цепочки <число> 1 из начального символа <число>. Каковы же фразы сентенциальной формы <чс>1? Имеет место вывод
<число> => <чс> => <чс><цифра> => <чс>1
Таким образом,
(1) <число>=>* <чс> и <ЧС> =>+ <ЧС>1
(2) <число> <чс><цифра> и <цифра> =>+ 1
Следовательно, <чс>1 и 1 — фразы. Простой же фразой будет только 1. В дальнейшем мы часто будем говорить о самой левой простой фразе сентенциальной формы. Поэтому введем
(2.3.10).	Определение. Основой всякой сентенциальной формы называется самая левая простая фраза.
Грамматика G1 примера 2.3.3 описывает бесконечный язык, т. е. язык, состоящий из бесконечного числа предложений. Это обусловлено тем, что правило <чс>: :=<чс><цифра> содержит <чс> и в левой, и в правой частях, т. е. в некотором смысле символ <чс> сам себя определяет. В общем случае, если U=>+. . .U. . ., мы говорим, что грамматика рекурсивна по отношению к U. Если U=>+U..., то имеет место левая рекурсия, если U=>+. . .U, то имеет место правая рекурсия. Правило называется лево (право) рекурсивным, если оно имеет вид U: :=U. . . (U: : = . . .U). Если язык бесконечен, то определяющая его грамматика должна быть рекурсивной.
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.3
1.	Пусть 0[<ид>] состоит из правил
<ид>:: =а | ы с I <ид> а |<ид>с|<ид>01 <ид> 1
Выпишите VT и VN. Выведите в тех случаях, когда это возможно, цепочки a, abO, aOcOl, 0а, 11, ааа.
2.	Каковы предложения языка L(G1) для G1 из примера 2.3.3?
3.	Опишите грамматику, язык которой состоит из множества четных целых чисел.
4.	Опишите грамматику, язык которой состоит из множества четных целых чисел, исключая числа с нулем вначале.
5.	Пусть G состоит из правил <А>: :=Ь<А>|сс. Докажите, что ее, bcc, bbbcc, ... принадлежат L(G).
6.	Постройте грамматику для языка
{abna|n=0, 1, 2, 3, . . .}.
40
ГЛАВА 2
7.	Постройте грамматику для языка
{апЬп|п=1, 2, 3,
8.	Следующая грамматика 03(<врж>] часто используется для описания арифметических выражении, в которых встречаются бинарные операторы (под i подразумевается «идентификатор»):
<врж> : :=<терм>|<врж>+<терм>|<врж> — <терм>
<терм> : :=<множ>|<терм>*<множ>|<терм>/<множ>
<множ>_: :=(<врж>)Ц
Выведите следующие арифметические выражения: i, (i), i*i, ,i*(i+i).
9.	Перечислите все фразы и простые фразы сентенциальной формы <врж>+<терм>*<множ> грамматики G3 (упражнение 8).
2.4.	СИНТАКСИЧЕСКИЕ ДЕРЕВЬЯ И НЕОДНОЗНАЧНОСТЬ
Синтаксические деревья помогают понять синтаксис предложений. На рис. 2.1 показано то, что мы называем синтаксическим деревом. В качестве иллюстрации построим дерево для следующего вывода предложения 22 грамматики G1 (пример 2.3.3):
(2.4.1)	<число> <чс> => <чс> <цифра>
=><цифра> <цифра>=>2 <цифра>=>22
Отправляясь от начального символа <число>, нарисуем его куст для того, чтобы указать первый непосредственный вывод (рис. 2.3, а). Куст узла — это множество подчиненных ему узлов (символов). Символы куста образуют цепочку, которая заменяет имя куста в первом непосредственном выводе <число>=><чс>.
<число>
<чс>
<шло>
<чс>
5зел
<чс> <цифро>
а	Ъ
Рис. 2.3. Синтаксические деревья для двух выводов.
Чтобы показать второй вывод, из узла, представляющего заменяемый символ, рисуется куст, узлы которого образуют цепочку, заменяющую этот символ (рис. 2.3, Ь). Поступая таким же образом, мы построим одну за другой три синтаксические диаграммы, показанные на рис. 2.4.
ГРАММАТИКИ И ЯЗЫКИ
41
Концевые (висячие) узлы синтаксического дерева — это узлы, не имеющие подчиненного куста. При чтении слева направо концевые узлы образуют цепочку, вывод которой представлен деревом. Таким образом, после третьего непосредственного вывода в (2.4.1) цепочка концевых узлов — суть <цифра> <цифра> (см. рис. 2.4, а).
Концевой куст — это куст, все узлы которого концевые. На рис. 2.3, b один концевой куст; его узлы <чс> и <цифра>. На рис. 2.4, с два концевых куста с узлами 2 и 2.
<число>
<чс>
, 1----1——1 к
<чс> <цифро>
<цифра>
а
<чисяо>
<чс>
1------1-----1
<чс> <цифра>
<цифра>
2
<число>
<ЧС> F-----1-----1
<чс>	<цифра>
<цифра> г
2
Р и е. 2.4. Синтаксические деревья для вывода (2.4.1).
Когда речь идет о деревьях, часто используется следующая терминология. Пусть N — узел дерева. Сыновьями N называются узлы куста, подчиненного N. N — их отец. Сыновья называются братьями > самым младшим считается самый левый из них. На рис. 2.4, b <чс> является единственным сыном узла <число>. Тот же самый <чс> имеет двух сыновей: <чс> и <цифра>. Отцом узла 2 является <цифра>.
Поддерево синтаксического дерева состоит из узла дерева (называемого корнем поддерева) вместе с той частью дерева (если она имеется), которая исходит из него. Поддеревья тесно связаны с фразами; концевые узлы образуют фразу для корня данного поддерева. Проследим это более подробно. Если U — корень поддерева и если и — цепочка из концевых узлов поддерева, то U г>+ и. Пусть х — цепочка концевых узлов слева от и, а у — цепочка концевых узлов справа от и, т. е., хиу — сентенциальная форма, заданная деревом. Тогда Z=>*xUy, а это означает, что и есть фраза для U в хиу.
Построение вывода по дереву
Можно восстановить вывод по синтаксическому дереву при помощи обратного процесса. Из рис. 2.4, с видно, что концевые узлы образуют цепочку 22. Самый правый концевой куст указывает не-, посредственный вывод:
2 <цифра> => 2g
42
ГЛАВА 2
Чтобы пройти по синтаксическому дереву до 2 <цифра>, мы отсекаем куст от дерева — удаляем его. Например, отсекание этого куста (на рис. 2.4, с) дает нам дерево на рис. 2.4, Ь. Этот процесс часто называют непосредственной редукцией.
Рассматривая рис. 2.4, Ь, мы видим, что последним здесь должен быть вывод <цифра> <цифра>=>2 <цифра>. Это нам дает
<цифра> <цифра> => 2 <цифра> => 22
Продолжаем процесс, всегда восстанавливая последний непосредственный вывод, на который указывает концевой куст синтаксического дерева, и затем отсекая этот куст.
Подводя итог, сформулируем следующие положения о синтаксических деревьях:
для каждого синтаксического дерева существует по крайней мере один вывод;
для каждого вывода есть соответствующее синтаксическое дерево (но несколько разных выводов могут иметь одно и то же дерево);
куст дерева указывает на непосредственный вывод, в котором имя куста заменяется узлами куста. Следовательно, в грамматике существует правило, левой частью которого является имя куста, а правой частью — цепочка из узлов куста;
концевые узлы дерева образуют выводимую сентенциальную форму;
пусть U — корень поддерева для сентенциальной формы w= = xuy, где и образует цепочку концевых узлов этого поддерева. Тогда и—фраза сентенциальной формы w для U. Она является простой фразой, если поддерево представлено единственным кустом.
Кроме вывода (2.4.1), есть другой вывод предложения 22 в G1:
(2.4.2)	<число> => <чс> =$> <чс> <цифра>
=> <цифра> <цифра> => <цифра> 2 => 22
у него то же самое синтаксическое дерево, что и у вывода (2.4.1). В действительности есть еще и третий вывод 22:,
(2.4.3)	<число>. => <чс> => <чс> <цифра> => <чс>2 => <цифра>2 => 22
Заметьте, что эти выводы отличаются лишь порядком применения правил и что синтаксическое дерево не определяет точный порядок, в соответствии с которым осуществляется вывод. На данном этапе это различие порядка в выводах для нас несущественно, и мы считаем, что выводы эквивалентны, если им соответствует одно и то же дерево.
ГРАММАТИКИ И ЯЗЫКИ
43
Неоднозначность
Гораздо более важным является вопрос неединственности синтаксического дерева. Рассмотрим грамматику, содержащую среди прочих правила:
<предложение>	:: =	<подлежащее> <сказуемое>
<предложение>	:: =	<сказуемое> <прямое дополнение>
<подлежащее>	:: =	<существительное>
<прямое дополнение>	:: =	<существительное>
<сказуемое>	::=	<глагол>
<существительное>	:: =	time | flies
<глагол>	:: =	time | flies
По этим правилам мы могли бы образовать предложение “time flies” двумя различными способами с различными синтаксическими деревьями (рис. 2.5).
<предложение>
<подлежацее> <сказуемое> х I	I
<суи|ествительное> <глаеол>
. I	I
time	flies
<предложение>
<спазуемое> <прямое йополненце>
<глагол>	<су(цестеительное>
time	flies
Рис. 2.5. Синтаксические деревья для неоднозначного предложения.
В английском языке каждое из этих предложений имеет смысл— либо “time flies by very quickly” (время летит очень быстро), либо “go find out how fast flies fly” (выясните, как быстро летают мухи). Дело в том, что без контекста мы не можем понять предложение, поскольку не можем его правильно разобрать; мы не можем однозначно разделить его на составные части. Поэтому, если компилятор должен уметь транслировать все правильные исходные программы, резонно потребовать, чтобы язык был однозначно определен. Теперь введем следующее
(2.4.4).	Определение. Предложение грамматики неоднозначно, если для его вывода существуют два синтаксических дерева. Грамматика неоднозначна, если она допускает неоднозначные предложения, в противном случае она однозначна.
Заметим, что мы называем неоднозначной грамматику, а не сам язык. Изменяя неоднозначную грамматику, но, конечно, не изменяя ее предложения, можно иногда получить однозначную грамматику для того же самого множества предложений. Однако есть языки,
44
ГЛАВА 2
для которых не существует однозначной грамматики. Такие языки называются существенно неоднозначными (разд. 2.10).
Не надо забывать, что в данный момент речь идет лишь о синтаксисе. Согласно определению выше, предложение может быть однозначным, но мы можем все же не понять, что оно означает из-за неоднозначного смысла слов.
К сожалению, было доказано, что проблема распознавания неоднозначности алгоритмически неразрешима. Это означает, что не существует алгоритма (т. е. нельзя составить такой алгоритм), который воспримет любую НФБ-грамматику и заведомо определит за конечное число шагов, является она однозначной или нет. Можно, однако, разработать (гл. 5 и 6) достаточно простые, но нетривиальные условия, такие, что если грамматика им удовлетворяет, то можно утверждать, что она однозначна. Эти условия являются достаточными, но не необходимыми.
Однозначная грамматика для арифметических выражений
Если сентенциальная форма неоднозначна, то она имеет более чем одно синтаксическое дерево и поэтому в общем случае более чем одну основу. Покажем это на примере, который в то же время предоставит нам очень полезную грамматику для арифметических вы-
<Е>
I---------1-----Г
<Е>	+	<£>
I----1----1
<Е>	*	<Е>
а
<Е>
<Е>	*	<Е>
I------1-------1
<Е>	+	<Е>
б
Рис. 2.6. Два синтаксических дерева для <Е> 4- <Е> * <Е>.
ражений. Рассмотрим следующую грамматику арифметических выражений, в которой используются единственный операнд (в качестве идентификатора), круглые скобки и бинарные операторы 4- и *: (2.4.5)	<Е> : :=<Е>+<Е>|<ЕЖЕ>|«Е» | i.
Сентенциальная форма <Е> + <Е>*<Е> имеет два синтаксических дерева (рис. 2.6) и две основы: <Е>4-<Е> и <Е>*<Е>. Если мы хотим разобрать эту сентенциальную форму, то с какой основы мы должны начать? Грамматика неоднозначна, и можно выбрать любую из основ. Таким образом, мы не можем сказать, что выполняется раньше: умножение или сложение.
ГРАММАТИКИ И ЯЗЫКИ
45
Рассмотрим теперь грамматику 03[<врж>] из упражнения 2.3.8:
(2.4.6)	<врж> : : = <терм>|<врж>+<терм>|<врж>—<терм> <терм> : : = <множ>|<терм>»<множ>|<терм>/<множ> <множ> : : = (<врж»Ц
Единственное дерево для выражения i+i*i показано ниже, и, таким образом, в соответствии с этой грамматикой предложение однозначно. В действительности все предложения G3 однозначны. Теперь определим, согласно G3, что в выражении i+i*i должно выполняться раньше: умножение или сложение. Операндами для +, согласно дереву, являются <врж>, из которого получается i, и <терм>, из которого в свою очередь получается i#i. Это означает, что умножение должно быть выполнено первым для того, чтобы образовать <терм> для сложения; следовательно, умножение предшествует сложению.
(врж)
I-----------1------------, -
(врж) + (терм)
I	I—----------1
(терм)	(терм) * (множ)
I	I	I
(МНОЖ)	(МНОЖ)	i
i	i
Грамматику G3 следует предпочесть грамматике (2.4.5) по двум причинам: она однозначна и указывает, что умножение предшествует сложению.
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.4
1.	Нарисуйте синтаксические деревья-для следующих выводов:
а)	<число> => <чс> => <чс> <цифра> => <чс> 3 => <чс> <цифра> 3 => <чс> 2 3 => <цифра> 2 3 => 1 2 3
Ь)	<число> => <чс> => <чс> <цифра> => <чс> 3
=> <чс> <цифра> 3 => <цифра> <цифра> 3
<цифра> 3^-123
Совпадают ли их синтаксические деревья?
2.	Нарисуйте синтаксические деревья для следующих выводов:
а)	<врж> => <терм> => <множ> =>i
b)	<врж> =><терм>^> <множ>=>(<врж>)=>(<терм>)=^(<множ>)=^(1) с) <врж> + <терм> => + <множ> -f-i
46
ГЛАВА 2
3. Постройте выводы (врЖ)
(терм) I " —4--------1
(терм)	*	(множ)
I	>
(МНОЖ)	1>
I
по следующим синтаксическим деревьям ' (врж)
(терм)
। " 1 I "	।
(терм) *	(множ)
(множ) ।........ I  — ।
|	(	(врж) )
I ।I।
(врж)	+	(терм)
I	I
(терм)	(множ)
I	!
(МНОЖ)	I
4.	Покажите, что следующая грамматика 0[<врж>] неоднозначна, построив 2 синтаксических дерева для каждого из предложений i+i*i и i~M+i:
<врж> : : =1|«врж>)|<врж> <оп> <врж>
<оп> : : =+|—1*|/
5.	Покажите, что предложения i-H*i и i+i-f-i грамматики G3 (2.4.6) однозначны. Какой оператор старше в предложении i+i*i? В предложении
2.5. ЗАДАЧА РАЗБОРА
Разбор сентенциальной формы означает построение вывода и, возможно, синтаксического дерева для нее. Программу разбора называют также распознавателем, так как она распознает только предложения рассматриваемой грамматики. Именно это и является нашей задачей в данный момент, поскольку мы хотим распознавать программы, написанные на языке программирования.
Все алгоритмы разбора, которые мы описываем, называются алгоритмами разбора слева направо ввиду того, что они обрабатывают сначала самые левые символы рассматриваемой цепочки и продвигаются по цепочке вправо только тогда, кбгда это необходимо. Мы могли бы подобным же образом определить разбор справа налево, но он менее естествен. Инструкции в программе выполняются слева направо, да и мы читаем тоже слева направо.
Различают две категории алгоритмов разбора: нисходящий (сверху вниз) и восходящий (снизу вверх) J). Эти термины соответствуют способу построения синтаксических деревьев. При нисходящем разборе дерево строится от корня (начального символа)
х) Нисходящий (top-down) и восходящий (bottom-up) разбор называют также соответственно разверткой и сверткой.— Прим. ред.
ГРАММАТИКИ И ЯЗЫКИ
47
вниз к концевым узлам. Рассмотрим предложение 3 5 в следующей грамматике целых чисел (последовательностей, состоящих из одной и более цифр):
(2.5.1)	N : : = D | N D
D:: = 0|l|2|3|4| 5| 6 | 7 | 8 | 9
На первом шаге непосредственный вывод N => N D будет строиться так, как это показано в первом дереве на рис. 2.7. На каждом последующем шаге самый левый нетерминал V текущей сентенциаль-
N
N	N	N	N
I—' ’	i	Г— 1 "I	г—-1 "*Л	Г~~
ND	ND	ND	ND
I	I	II
D	D	D 5
I	I
3	3
N D =>D D => 3 D =» 3 5
Рис. 2.7. Нисходящий разбор и построение вывода.
ной формы xVy заменяется на правую часть и правила V : : = и, в результате чего получается следующая сентенциальная форма. Этот процесс для предложения 3 5 представлен на рис. 2.7 в виде пяти деревьев. Фокус в том, конечно, что надо получить ту сентенциальную форму, которая совпадает с заданной цепочкой.
N
N *	N	N
D D	D D	D	О
’I	1	I	Ic	I	е
35	35	35	35
N =» N 0	=>	N	5	=> D 5	=>.3	5
Рис. 2.8. Восходящий разбор и построение вывода.
Метод восходящего разбора состоит в том, что, отправляясь от заданной цепочки, пытаются привести ее к начальному символу. На первом шаге при разборе предложения 3 5 терминал 3 приводится к D, в результате чего получается сентенциальная форма D 5. Таким образом, мы построили непосредственный вывод D 5 => 3 5, как это показано в самом правом частичном синтаксическом дереве на рис. 2.8. На следующем шаге D приводится к N, что показано во втором дереве справа. Этот процесс продолжается до тех пор, пока не будет получено первое дерево рис. 2.8. Мы расположили деревья
48
ГЛАВА 2
на рисунке справа налево, потому что такое расположение лучше иллюстрирует построенный вывод, который теперь читаем, как обычно, слева направо. Заметим, что выводы, произведенные двумя различными распознавателями, различны, но имеют одно и то же синтаксическое дерево.
Поскольку мы будем часто ссылаться на тип вывода, который получается при восходящем разборе слева направо, введем еще один новый термин. Заметим, что в таком разборе на каждом шаге редуцируется основа (самая левая простая фраза) текущей сентенциальной формы и поэтому цепочка справа от основы всегда содержит только терминальные символы.
(2.5.2).	Определение. Непосредственный вывод xUy => х и у называется каноническим и записывается xUy => х и у, если у содержит только терминалы. Вывод w =>+ v называется каноническим и записывается w =>+ v, если каждый непосредственный вывод в нем является каноническим.
Каждое предложение, но не каждая сентенциальная форма имеет канонический вывод. Рассмотрим в качестве примера сентенциальную форму 3D грамматики (2.5.1). Ее единственным выводом является
<число >=>ND=>DD=>3D
И второй, и третий непосредственные выводы не являются каноническими, но это не должно нас смущать, поскольку мы в основном интересуемся разбором (анализ) программ, которые являются предложениями языка программирования. Сентенциальная форма, которая имеет канонический вывод, называется канонической сентенциальной формой.
Мы, конечно, еще не касались главных проблем разбора.
1. Предположим, что при нисходящем разборе надо заменить самый левый нетерминал V, и пусть имеется п правил:
V : : = х, | х2 | . . . | хп.
Как узнать, какой цепочкой х, следует заменить V?
2. При восходящем разборе на каждом шаге редуцируется основа. Как найти основу и то, к чему она должна приводиться?
На эти вопросы не всегда легко ответить. Это будет хорошо видно при выполнении упражнения 2. В гл. 4 проблема рассматривается применительно к нисходящему разбору, тогда как в гл. 5 и 6 обсуждаются различные приемы, позволяющие решить проблему восходящего разбора.
Одним из решений является выбор некоторого из возможных вариантов наугад в предположении, что он верен. Если позднее обнаружится ошибка, то мы должны вернуться назад и попытаться применить другой вариант, Этот прием называется возвратом.
ГРАММАТИКИ И ЯЗЫКИ
49
Очевидно, что возвраты могут привести к чрезмерному расходу времени.
Другим решением является просмотр контекста вокруг обрабатываемой в данный момент подцепочки. Именно так мы поступаем, когда вычисляем выражение, подобное А + В * С. Мы задаем себе вопрос, „можно ли вначале вычислить А 4- В“, и отвечаем на него отрицательно, замечая, что за А + В следует * .
Один из первых алгоритмов разбора, который действительно использовался в компиляторе, был описан Айронсом (61а]. Алгоритм применялся в синтаксически ориентированном трансляторе Ингермана [66].
Метод, положенный в основу этого алгоритма, является смесью нисходящего и восходящего методов, и мы не будем обсуждать его подробнее.
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.5
1.	Найдите канонический разбор предложений 561, 0 и 0012 грамматики G1 (пример 2.3.3).
2.	Проведите разбор предложений (,) (*, i (, (+(, (+(i (и (+) (i (* i( грамматики
<цель>: : = VI
VI	: :	= V2 | VI i V2
V2	: :	= V3 | V2 + V3	| i V3
V3	: :	= ) VI * | (
3.	Дайте характеристику языка, порождаемого грамматикой V : : = aaV | Ьс.
2.6.	НЕКОТОРЫЕ ОТНОШЕНИЯ ПРИМЕНИТЕЛЬНО К ГРАММАТИКАМ
Отношения
Символы “=>” и “=>+”, определенные в разд. 2.3, являются примерами отношений между цепочками. Вообще говоря, (бинарным) отношением на множестве является любое свойство, которым обладают (или не обладают) любые два упорядоченных символа этого множества.
Другим примером является отношение МЕНЬШЕ ЧЕМ (<), определенное на множестве целых чисел: i С j тогда и только тогда, когда j — i является положительным ненулевым целым числом. Нематематическим является отношение РЕБЕНОК, определенное на множестве людей. Некто А либо является ребенком В; либо нет,
50
ГЛАВА 2
Мы используем инфиксные обозначения для отношений: если между элементами с и d некоторого множества имеет место отношение, то мы пишем cRd. (Заметим также, что важен порядок двух объектов: из eRd автоматически не следует dRc.) Можно также рассматривать отношение как множество упорядоченных пар, для которых данное отношение справедливо: (с, d) £ R тогда и только тогда, когда cRd.
Мы говорим, 5J0 отношение Р включает другое отношение R, если из (с, d) £ R следует (с, d) £ Р.
Отношение, обратное отношению R, записывается как R-1 и определяется следующим образом: с R-1 d тогда и только тогда, когда dRc.
Отношение, обратное отношению БОЛЬШЕ ЧЕМ, есть МЕНЬШЕ ЧЕМ. Отношение, обратное отношению РЕБЕНОК, есть РОДИТЕЛЬ.
Отношение R называется рефлексивным, если cRc справедливо для всех элементов с, принадлежащих множеству. Например, отношение МЕНЬШЕ ЧЕМ ИЛИ РАВНО (^) является рефлексивным (i i для всех действительных чисел i), тогда как отношение МЕНЬШЕ ЧЕМ таковым не является.
Отношение R называется транзитивным, если aRb и bRc влечет за собой a R с. Отношение МЕНЬШЕ ЧЕМ над целыми числами является транзитивным. Отношение РЕБЕНОК не является транзитивным; если Джон сын Джека и Джек сын Джила, то, очевидно, Джон не является сыном Джила.
Для любого отношения R определим новое отношение R+ и назовем его транзитивным замыканием R. Оно называется так потому, что включается в любое транзитивное отношение, которое включает в себя R. Другими словами, предположим, что Р — транзитивное отношение, которое включает в себя R : из (с, d) С R следует (с, d) С Р. Тогда Р также включает в себя R+.
Прежде всего для двух заданных отношений R и Р, определенных на одном и том же множестве, введем новое отношение RP, названное произведением R и Р : cRPd тогда и только тогда, когда существует такое е, что cRe иеРб. Умножение бинарных отношений ассоциативно: R (PQ) = (RP) Q для любых отношений R, Р и Q.
В качестве примера рассмотрим отношения R и Р над целыми числами, определенные следующим образом:
aRb тогда и только тогда, когда b = а + 1,
аРЬ тогда и только тогда, когда b = а + 2.
Очевидно, что aRPb, тогда и только тогда, когда есть такое с, что aRc и сРЬ, т.е. тогда и только тогда, когда b = а + 3.
Используя произведение, определим теперь степени отношения R следующим образом:
ГРАММАТИКИ И ЯЗЫКИ
51
R1 = R, R2 = RR, Rn = Rn-1 R = RRn-1 для n > 1. Определим R° как единичное отношение:
aR°b тогда и только тогда, когда а = Ь.
И, наконец, транзитивное замыкание R+ отношения R определяется следующим образом:
(2.6.1).	aR+b тогда и только тогда, когда aRnb для некоторого п > 0.
Очевидно, если aRb, то aR+b. Доказательство того, что R + действительно является транзитивным, предоставляется читателю в качестве упражнения. Ясно, почему для транзитивного замыкания выбрано обозначение R + ; когда отношения рассматриваются как множества упорядоченных пар, мы имеем
R + =RiU R2 U R3 • • • •
Определим рефлексивное транзитивное замыкание R* отношения R как
aR*b тогда и только тогда, когда а=Ь или aR+b.
Таким образом, R* = R° U R1 U R2 . . . .
Отношения применительно к грамматикам
Чем же вызван такой интерес к отношениям? Во-первых, теперь должно быть очевидно, что символ =>-Т (см. 2.3.6) обозначает не что иное, как транзитивное замыкание отношения =>, определенного в (2.3.5)! И, во-вторых, с грамматиками связано несколько важных символьных множеств, которые в дальнейшем нам придется построить. Эти множества легко определяются в терминах совсем простых отношений и их транзитивных замыканий. В разд. 2.7 мы дадим один несложный алгоритм, который позволит нам построить все необходимые множества из простых отношений. Единственное требование заключается в том, чтобы отношения были определены на конечном множестве элементов. Перед тем как приступить к этому алгоритму, рассмотрим те множества, которые нам будут необходимы.
Если задана грамматика и нетерминальный символ U, то нам необходимо знать множество головных символов в цепочках, выводимых из U. Таким образом, если U =>+ Sx, то цепочка Sx выводима из U и S принадлежит множеству. Назовем это множество голова (U) и определим его формально следующим образом:
(2.6.2).	голова(и) : : = {S | U =>+ S . . .}.
(Напомним, что тремя точками «. . .» обозначается цепочка (возможно, пустая), которая в данный момент нас не интересует.)
52
ГЛАВА 2
В большинстве случаев такое определение могло бы быть удовлетворительным. Заметим, однако, что в определении используется отношение заданное на бесконечном множестве цепочек в словаре V, и хотя само множество голова(11) конечно, при его построении могут возникнуть трудности. Поэтому мы переопределим множество голова(Ц), используя другое отношение, заданное на конечном множестве следующим образом. Определим отношение FIRST (первый) в конечном словаре V грамматики следующим образом:
(2.6.3).	U FIRST S тогда и только тогда, когда существует правило U : : = S . . . .
Затем, согласно определению транзитивного замыкания, имеем
(2.6.4).	U FIRST* S тогда и только тогда, когда существует непустая последовательность правил U : : = S1 , . ., S1 : : = S2 . . ., . . Sn : : = S . . . .
Из этого, очевидно, вытекает, что
U FIRST* S тогда и только тогда, когда U =>+ S . . . .
Заметим, что если отношение FIRST* рассматривать как множество, то оно полностью определяет множество голова(Ц) из (2.6.2) для всех символов U словаря V : голова (U) — ]S | (U, S)b FIRST*}. (2.6.5). Пример. Чтобы проиллюстрировать оба отношения FIRST и FIRST*, выпишем несколько правил и справа от каждого из них укажем отношение FIRST, выведенное из этого правила.
	Правило A:: =Af А:: =В В:: =DdC	Отношение A FIRST А A FIRST В В FIRST D
(2.6.6)	В:: =De	В FIRST D
	С::=е	С FIRST е
	D:: =Bf	D FIRST В
Таким образом, мы имеем следующие пары в FIRST* :
(А, А) (А, В) (A, D) (В, В) (В, D) (D, В) (D, D) (С, е)
Следовательно, головными символами цепочек, выводимых из А, будут А, В и D, из В — В и D, из С — е.
Есть еще три других множества, которые мы хотим здесь определить. Они могут быть определены тем же путем, что и FIRST*. Первое из них — это множество символов, которыми оканчиваются цепочки, выводимые из некоторого символа U. Оно определяется для всех символов U через отношение LAST*, являющееся транзитивным замыканием отношения LAST (последний):
ГРАММАТИКИ и языки	53
(2.6.7).	U LAST S тогда и только тогда, когда существует правило U	S.
Второе множество — это множество внутренних символов цепочек, выводимых из нетерминала U. Оно определяется через отношение WITHIN+, являющееся транзитивным замыканием отношения WITHIN (внутри).
(2.6.8).	U WITHIN S тогда и только тогда, когда существует правило U :: = ... S .. . .
И, наконец, определим множество символов S, которые выводимы из нетерминала U. Оно определяется через отношение SYMB + , являющееся транзитивным замыканием отношения SYMB (симв):
(2.6.9).	U SYMB S тогда и только тогда, когда существует правило U : : = S .
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.6
1.	Являются ли следующие отношения рефлексивными? Как выглядит транзитивное и рефлексивно-транзитивное замыкание каждого из них?
а)	НИЖЕ ЧЕМ (на множестве людей).
Ь)	> (БОЛЬШЕ ЧЕМ) на множестве действительных чисел.
с)	отношение R на множестве действительных чисел, определенное следующим образом: aRb тогда и только тогда, когда а = — Ь.
2.	Выпишите множества FIRST*, LAST* и SYMB* для грамматики G1 примера (2.3.3).
3.	Докажите, что умножение бинарных отношений ассоциативно.
4.	Покажите, что R* является транзитивным для любого отношения R.
5.	Пусть Р — любое транзитивное отношение, такое, что если cRb, то сРЬ. Доказать, что если cR*b, то сРЬ. (То есть R* является наименьшим транзитивным отношением, включающим R.)
6.	Каков смысл произведения отношения ОТЕЦ на самого себя?
2.7.	ПОСТРОЕНИЕ ТРАНЗИТИВНОГО ЗАМЫКАНИЯ ОТНОШЕНИЙ
Мы только что определили четыре важных множества, использовав при этом транзитивные замыкания для четырех очень простых отношений. Теперь вернемся к задаче построения транзитивного
54
ГЛАВА 2
замыкания для отношений, заданных в конечных множествах элементов.
Предположим, что для некоторого отношения R в множестве А и некоторого элемента U мы хотим построить множество
B = {S|UR+S},
где R+—транзитивное замыкание R. Если множество А бесконечно, то это может оказаться делом трудным (если вообще возможным), даже если множество В конечно. Трудность возникает из-за того, что нельзя ограничить число п так, чтобы отношение aRnb включало в себя отношение aR + b. Но если А конечно, то можно ограничить длину требуемой последовательности, что и показывает следующая теорема.
(2.7.1).	Теорема. Пусть в алфавите А имеется п символов, и пусть R — произвольное отношение в А. Если для двух символов Si и b мы имеем SjR + b, то существует положительное целое k п, такое, что SxRkb.
Доказательство. Так как S^R+ Ь, то существует целое р >• 0, такое, что Sx R₽b. Это означает, что существуют символы S2, Sa...Sp в А, такие, что
Si R S,. S2 R Sa, ..., Sp_x R Sp и Sp R b
(по определению Rp). Предположим, что наименьшее из р больше, чем п. Тогда для этого наименьшего р и двух целых чисел i и j, для которых справедливо i < j р, мы должны иметь Sj = Sj( так как в А только п символов. Но тогда отношения
Si R S2, . . ., Sj_i R Si,
Sj R Sj+1, . . ., Sp_x R Sp, Sp R b
показывают, что S2 Rk b, где k = p — (j — i), а это противоречит тому, что р было наименьшим. Таким образом, наше предположение, что р > п, ошибочно и теорема доказана.
Булевы матрицы и отношения
Теперь мы хотим представить отношения в алфавите в виде, удобном для машинной обработки. Для такого представления больше всего подходит булева матрица В. Это матрица, элементы которой В [i, j] могут принимать только значения 0 или 1 (ложь или истина). «Сложение» булевых матриц n-го порядка сводится к выполнению операции ИЛИ над соответствующими элементами матриц; элемент D [i, j] матрицы
D = В 4- С
определяется как
D [i, j]: = if В [i, j]=l then 1 else C £i, j]
ГРАММАТИКИ И ЯЗЫКИ
55
«Умножение» булевых матриц n-го порядка выполняется так же, как умножение над числовыми матрицами, за исключением того, что умножение заменяется операцией И, а сложение — операцией ИЛИ.
Таким образом, если В и С — две матрицы n-го порядка и D=BC, то элемент D [i, jl определяется как
D [i, j] : = В [l,j]4-B[i, 2]»С [2, j] + . . .+ В [i, nkC [n,j],
где » имеет тот же смысл, что и в следующем соотношении: а*Ь :: = if а=0 then 0 else b
Например, если то
R-Г00! и
В= 11 и
С=
1 01 10’
D1=B+C=[}°] и D2=BC= [?§].
Обратите, пожалуйста, внимание на то, что в этом контексте + и » имеют другой смысл.
Операция сложения булевых матриц ассоциативна (А+(В+С)= = (А + В) + С) и коммутативна (А+В — В+А), в то время как операция умножения только ассоциативна (А (ВС)=(АВ) С). Обе операции удовлетворяют также дистрибутивному закону (А (В + С) = АВ 4-АС).
Предположим, что отношение R определено на множестве S из и символов Sj..Sn. Для того чтобы представить это отношение,
построим булеву матрицу В n-го порядка с элементами В [i, j], равными 1 тогда и только тогда, когда Sj R Sj. Например, возьмем отношение FIRST, заданное в множестве {А, В, С, D, е, f) в (2.6.6). Матрица В для этого отношения изображена на рис. 2.9.
	S1=A	S2 = B	S3 = C	S4 = D	S5 = e	S6 = d	S7 = f
S1=A	1	1	0	0	0	0	0
S2 = B	0	0	0	1	0	0	0
S3=C	0	0	0	0	1	0	0
S4 = D	0	1	0	0	0	0	0
S5 =e	0	0	0	0	0	0	0
S6 = d	0	0	0	0	0	0	0
S7 = f	0	0	0	0	0	0	0
	Рис.	2.9. Матрица для		отношения	FIRST.		
Теперь мы можем сделать следующее очевидное утверждение. (2.7.2). Матрица для отношения R-1 получается путем транспонирования булевой матрицы, представляющей R.
56
ГЛАВА 2
Рассмотрим произведение D = ВС двух матриц В и С, которые представляют два отношения Р и Q в алфавите S. Если D [i, jl= 1, то из определения D [i, j] мы должны иметь для некоторого к
В П, к] = 1 и С [к, j] — 1,
т. е. S! Р Sk и Sk Q Sj, а это означает, что (Sb Sj) принадлежит произведению PQ двух отношений Р и Q.
Наоборот, если (S1( Sj) € PQ, то существует к, такое, что SjPSk и SkQSj, hD [i, j] должно быть равно 1. Это означает, что матрица D представляет отношение PQ. Следовательно, доказана следующая
(2.7.3). Теорема. Произведение двух отношений, заданных в одном и том же алфавите, представляется произведением булевых матриц этих отношений.
Поскольку в общем случае Rn определяется рекурсивно как R Rn-1 для n> 1, то из этого по индукции следует, что матрица Bn = ВВВ . . . В (п раз) представляет отношение Rn. Из определения R* и теорем 2.7.1 и 2.7.3 мы сразу выводим следующую теорему.
(2.7.4). Теорема. Пусть В — булева матрица n-го порядка, представляющая отношение R, заданное в алфавите S из п символов. Тогда матрица В+, определенная как
В+ = В + ВВ + ВВВ 4- . . . + Вп, представляет транзитивное замыкание R+ отношения R.
Например, на рис. 2.10 показаны матрицы В, В2, .... В’ и В + для матрицы В, представленной на рис. 2.9 (отношение FIRST грамматики (2.6.6)). Из В+ мы вновь получаем следующие пары в FIRST* :
(А, А) (А, В) (A, D) (В, В) (В, D) (D, В) (D, D) (С, е).
Использование отношений и теории булевых матриц обеспечило нас единым алгоритмом для получения ряда различных важных множеств. Эти множества в дальнейшем будут применяться при построении алгоритмов грамматического разбора. Замечательно то, что используемые принципы являются прстыми и легкими для понимания.
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.7
1. Воршалл [62] разработал следующий эффективный алгоритм для вычисления матрицы В+ =В+ВВ + . . . + Вп отматрицыВ. Докажите, что его алгоритм работает правильно,
ГРАММАТИКИ И ЯЗЫКИ
57
1.	Завести новую матрицу А — В.
2.	Установить i : = 1.
3.	Для всех j, если A [j, i] = 1, то для к — 1, п установить A [j, к] : = A [j, к] + А [i, к].
4.	Увеличить i на 1.
5.	Если i п, то перейти к шагу 3; в противном случае «останов».
2.8. ПРАКТИЧЕСКИЕ ОГРАНИЧЕНИЯ, НАЛАГАЕМЫЕ
НА ГРАММАТИКИ
В этом разделе мы снова рассмотрим грамматики, чтобы выявить некоторые ограничения, которые налагаются на них при их практическом использовании. Фактически эти условия не ограничивают множество языков, которые можно описать с помощью грамматик.
	"1	1	0	0	0	0	О'			~1	1	0	1	0	0	on
	0	0	0	1	0	0	0	В2	=	0	1	0	0	0-	0	0
	0	0	0	0	1	0	0			0	0	0	0	0	0	0
в =	0	1	0	0	0	0	0	В4	=	0	0	0	1	0	0	0
	0	0	0	0	0	0	0			0	0	0	0	0	0	0
	0	0	0	0	0	0	0	в*	=	0	0	0	0	0	0	0
	_0	0	0	0	0	0	0_		1	_0	0	0	0	0	0	0j
	“1	1	0	1	0	0	0~			Г1	1	0	1	0	0	0“1
В3 =	0	0	0	1	0	0	0			0	1	0	1	0	0	0
	0	0	0	0	0	0	0			0	0	0	0	1	0	0
В5 =	0	1	0	0	0	0	0	в+		0	1	0	1	0	0	0
	0	0	0	0	0	0	0			0	0	0	0	0	0	0
В’ =	0	0	0	0	0	0	0			0	0	0	0	0	0	0
।	[_о	0	0	0	0	0	0	1	1	l0	0	0	0	0	0	0
Рис. 2.10. Вычисление В+ от В.
Алгоритмы, осуществляющие контроль выполнения этих условий для грамматик, описаны в общих чертах. Этот раздел должен также помочь читателю еще лучше освоить технику манипулирования грамматиками до того, как мы перейдем к задаче грамматического разбора.
Правило, подобное U : : = U, очевидно, не является необходимым для грамматики; более того, оно приводит к неоднозначности грамматики. Поэтому далее в этой книге мы полагаем, что
(2.8.1)	грамматика не содержит правил вида U : : = U.
Грамматики могут также содержать лишние правила, которые невозможно использовать в выводе хотя бы одного предложения. Например, в следующей ниже грамматике G4 [Z] нетерминал
58
ГЛАВА 2
<d> не может быть использован в выводе какого-либо предложения, так как он не встречается ни в одной из правых частей правил:
Z : : = <Ь> е
<а> : : = <а> е |е
<Ь> : : = <с> е | <а> f
<с> : : == <с> f
<d> : : = f
Разбор предложений грамматики выполняется проще, если грамматика не содержит лишних правил. Можно с достаточным основанием утверждать, что если грамматика языка программирования содержит лишние правила, то она ошибочна. Следовательно, алгоритм, который обнаруживает лишние правила в грамматике, может оказаться полезным при проектировании языка.
Чтобы появиться в выводе какого-либо предложения, нетерминал U должен удовлетворять двум условиям. Во-первых, символ U должен встречаться в некоторой сентенциальной форме: _
(2.8.2)	Z =>* xUy для некоторых цепочек х и у,
где Z — начальный символ грамматики. Во-вторых, из U должна выводиться цепочка t, состоящая из терминальных символов:
(2.8.3)	U ^>+ t для некоторой t g VT+.	,
Совершенно ясно, что если нетерминальный символ U не удовлетворяет обоим этим условиям, то правила, содержащие U в левой части, не могут быть использованы ни в каком выводе. С другой стороны, если все нетерминалы удовлетворяют этим условиям, то грамматика не содержит лишних правил. Так, если U : : = и — правило, то, во-первых, Z =>* xUy => х и у. Во-вторых, так как мы можем вывести цепочку терминальных символов для каждого нетерминала, содержащегося в цепочке х и у, то
х и у =>* t для некоторой t g VT+.
В конечном итоге получаем Z =>+ t, т. е. вывод, в котором было использовано правило U : : = и.
В описанной выше грамматике G4 нетерминальный символ <с> не удовлетворяет условию (2.8.3), так как при непосредственном выводе он должен быть заменен на <с> f. Очевидно, что такая замена не может привести к цепочке из терминальных символов. Если отбросить все правила, которые нельзя использовать при порождении хотя бы одного предложения, то останутся правила
Z : : = <Ь> е, <а> : : = <а> е, <а> : : = е и <Ь> : : = <a>f.
(2.8.4).	Определение. Грамматика G называется приведенной, если каждый нетерминал U удовлетворяет условиям (2.8.2) и (2.8.3).
ГРАММАТИКИ и языки
59
Далее в этой книге будем полагать, что все грамматики только приведенные.
Теперь обратим внимание на то, что нетерминал U удовлетворяет условию (2.8.2) тогда и только тогда, когда Z WITHIN* U, где WITHIN — отношение, определенное в (2.6.8). Таким образом, можно использовать результаты предыдущего раздела, чтобы проверить грамматику на выполнение условия (2.8.2).
На рис. 2.11 приведена блок-схема алгоритма, который проверяет нетерминалы, встречающиеся в наборе правил, на выполнение
Детализация работы блока Pi i-счетчик нетерминалов
Предположим^ что существует набор правил { Щ	» п}
Рис. 2.11. Алгоритм проверки условия (2.8.3).
условия (2.8.3). Работа алгоритма начинается с того, что некоторым образом отмечаются те нетерминалы, для которых существует пра-вило и : : = t, где t € VT+ (первое выполнение блока Р1). Такие нетерминалы, очевидно, удовлетворяют условию (2.8.3). Далее в алгоритме проверяются все неотмеченные нетерминалы U и для
60
ГЛАВА 2
каждого из них делается попытка найти правило U : : = х, где х состоит только из терминальных символов или ранее отмеченных нетерминалов (повторное выполнение блока Р1). Символы, для которых такое правило существует, также, очевидно, удовлетворяют условию (2.8.3). Этот процесс продолжается до тех пор, пока либо все нетерминалы не будут отмечены, либо пока при выполнении блока Р1 не будет отмечено ни одного символа. Работа алгоритма завершена. Неотмеченные нетерминалы не удовлетворяют условию (2.8.3).
УПРАЖНЕНИЯ К РАЗДЕЛУ 2.8
1. Рассматривается грамматика
Z : : = Е + Т
Е : : = Е | S + F | Т
F : : = F J FP | Р
Р : : = G
G : : = G | GG | F
Т : : = T*i | i
Q : : = Е | Е + F | Т | S
S : : = i
Выполните следующие операции над этой грамматикой:
А. а) Постройте отношение WITHIN + .
b) Исключите все правила, содержащие (в левой или правой частях) нетерминалы, которые не удовлетворяют условию (2.8.2). В. а) Выполните алгоритм, показанный на рис. 2.11.
Ь) Исключите все правила, которые содержат нетерминалы, не удовлетворяющие условию (2.8.3).
с) Составьте список символов, которые до выполнения шага В считались нетерминальными, а теперь стали терминальными, d) Исключите все правила, содержащие символы, из списка, составленного в шаге с).
е) Повторяйте шаги с) и d) до тех пор, пока есть правила, которые следует исключить.
С.	Повторяйте шаги А и В до тех пор, пока есть правила, которые следует исключить.
D.	Грамматику, полученную в результате выполнения шага С, измените таким образом, чтобы она удовлетворяла условию (2.8.1). Опишите язык L(G) полученной грамматики.
2.9. ДРУГИЕ СПОСОБЫ ПРЕДСТАВЛЕНИЯ СИНТАКСИСА
В литературе для описания синтаксиса различных языков используются и некоторые другие способы записи. Мы хотели бы вкратце рассмотреть их. Эти способы обладают такой же мощно
ГРАММАТИКИ И ЯЗЫКИ
61
стью, что и НФБ, т. е. то, что может быть описано каким-либо из этих способов, может быть также описано и в НФБ. Но во многих отношениях они более наглядны, чем НФБ в чистом виде, и поэтому далее мы будем часто ими пользоваться.
Фигурные скобки
В НФБ только с помощью рекурсии можно задать список произвольного числа элементов. Таким образом, мы пишем
(2.9.1)	<спис ид> : : = <ид> | <спис ид>, <ид> и
(2.9.2)	<целое> : : = <цифра> | <целое> <цифра>
Предположим, что фигурные скобки { и } являются метасимволами и используются там, где необходимо указать, что цепочка, заключенная в фигурные скобки, может либо отсутствовать, либо повторяться любое число раз. Можно теперь переписать (2.9.1) и (2.9.2) как
(2.9.3)	<спис ид> : : = <ид> {, <ид>}
<целое> : : =<цифра> {<цифра>}
Оба способа записи считаются эквивалентными. Для того чтобы задать минимальное или максимальное число возможных повторений цепочки, после фигурных скобок используются надстрочные и/или подстрочные индексы. Например, в языке ФОРТРАН идентификаторы состоят не более чем из 6 буквенно-цифровых литер. Они могут быть описаны следующим образом:
(2.9.4)	<ид ФОРТРАНа> : : = <буква> { <бц»50
<бц>	: : = <буква> | <цифра>
Эта же запись в НФБ выглядит неуклюже:
<ид ФОРТРАНа> : : = <буква> | <буква> <бц спис>
<бц спис> : : = <бц> | <бц> <бц>
| <бц> <бц> <бц>
| <бц> <бц> <бц> <бц>
| <бц> <бц> <бц> <бц> <бц>
Чтобы запись была линейной (без надстрочных или подстрочных индексов), примем соглашение, что минимальное число вхождений цепочки, заключенной в фигурные скобки, равно 0, а максимальное число будет записываться сразу вслед за закрывающей фигурной скобкой — без пробела между ними. Следовательно, (2.9.4) мы можем написать в виде
<ид ФОРТРАН а > : : = <буква> { <бц>}5
62
ГЛАВА 2
Заметим, что эта запись отличается от
(ид ФОРТРАНа> : : = (буква > {<бц>}5 поскольку во втором случае есть пробел между фигурной скобкой и 5.
Фигурные скобки позволяют нам использовать метасимвол | внутри них для указания на возможный выбор: так, {х | у | z) означает, что каждая из цепочек х, у или z может входить в последовательность нуль или более раз. Воспользуемся этой возможностью и перепишем синтаксис идентификаторов ФОРТРАНа таким образом:
<ид ФОРТРАНа>:: = (буква> {(буква> | (цифра>}5
Квадратные скобки
Квадратные скобки [ и ] часто используют для того, чтобы указать факультативную цепочку. Следовательно, квадратные скобки эквивалентны фигурным скобкам, за которыми непосредственно следует 1. Предположим, что требуется описать простую инструкцию цикла в языке, подобном АЛГОЛу, в которой либо начальное значение, либо величина шага (либо и то и другое одновременно) могут быть опущены. Это можно сделать так:
(1ог-цикл> :: = FOR <переменная> [: = (выражение» [STEP (выражение»
UNTIL (выражение> DO (инструкция>
Круглые скобки как метасимволы
В правых частях правил оператор конкатенации предшествует оператору выбора. Поэтому АВ | С означает либо АВ, либо С. Когда необходимо отказаться от нормального предшествования, круглые скобки используются почти так же, как это делается в арифметических выражениях. Таким образом, А (В | С) означает либо АВ, либо АС. Круглые скобки как метасимволы будут полезны, как мы сейчас увидим, при факторизации в правых частях. Однако возникает проблема, когда круглые скобки используются как терминальные символы языка. В дальнейшем мы будем использовать круглые скобки как обычные терминальные символы, если не будет оговорено противное.
Факторизация
Если круглые скобки используются в качестве метасимволов, мы можем записать правила
U : : = ху | xw | .. . | xz
ГРАММАТИКИ И ЯЗЫКИ
63
как
U : : = х (у | w | ... | z), где общая для всех альтернатив голова х вынесена за скобки. Скобки могут быть вложенными на какую угодно глубину, точно так же как в арифметических выражениях. Например, пусть У = У1У2 и w = уху8. Тогда приведенное выше правило можно переписать как
U : : = х (у! (у. | у8) |. . . I z) .
Позднее мы увидим, что в некоторых случаях благодаря факторизации облегчается разбор.
Правила Е : : = Т | Е + Т, обозначающие по крайней мере одно вхождение Т, за которым следует сколько угодно (в том числе нуль) вхождений + Т, можно переписать как
Е : : = Т {+ Т).
Более того, Е :: = Т | Е + Т | Е — Т можно записать как
Е : : = Т {(+ | - ) Т}.
Такое преобразование, которое позволяет избавиться от левой рекурсии в правиле, будет удобным при нисходящем разборе.
Метасимволы как терминальные символы
Иногда нам придется описывать язык, содержащий терминалы, такие, как : : = , | или {, которые одновременно являются метасимволами. В этом случае терминал заключается в кавычки, чтобы отличить его от метасимвола. Например, правила НФБ могут быть частично описаны так:
<правило> : : = <лев. часть> ' : : = ' <прав. часть>
Ясно, что это условие ставит кавычки в положение метасимвола. Кавычку, если она используется как терминальный символ, следует представлять двумя кавычками, заключая их в кавычки. Следовательно, соотношение <кавычка)	описывает нетерми-
нальный символ <кавычка>; единственная цепочка, выводимая из него, состоит из одиночной кавычки.
2.10.	КРАТКИЙ ОБЗОР ТЕОРИИ ФОРМАЛЬНЫХ ЯЗЫКОВ
Теория формальных языков быстро прогрессировала с тех пор, как Хомский впервые описал формальный язык. Однако большинство из опубликованных работ не имеет непосредственного отношения к интересующей нас проблеме разбора предложений языка
64
ГЛАВА 2
программирования; эти работы скорее касаются вопросов математической теории языков и грамматик. В них определяются различные классы языков с теми или иными свойствами. Языки характеризуются в терминах грамматик, которые порождают языки лишь из некоторого определенного класса, и в терминах автоматов (машин), которые распознают языки лишь из определенного класса. Вот несколько типичных вопросов, которые рассматриваются в таких работах: если L1 и L2 — два языка из класса Т, то является ли объединение (пересечение) двух языков также языком из класса Т? Однозначна ли грамматика? Существует ли алгоритм, определяющий однозначность любой грамматики из класса Т? Эквивалентны ли две грамматики в некотором смысле? Хорошей работой на эту тему можно считать публикацию Хопкрофта и Уллмана 169]. Монография Гинзбурга [66] содержит больше материала, но весьма лаконична и трудна для чтения *). Много статей на эту тему публикуется в двух периодических журналах Information and Control и Journal of the ACM.
Хомский [56] определил четыре основных класса языков в терминах грамматик, являющихся упорядоченной четверкой (V, Т, Р, Z), где
1.	V — алфавит;
2.	Т = V — алфавит терминальных символов;
3.	Р — конечный набор правил подстановки;
4.	Z — начальный символ, принадлежащий множеству V — Т.
Язык, порождаемый грамматикой, — это множество терминальных цепочек, которые можно вывести из Z. Различие четырех типов грамматик заключается в форме правил подстановки, допустимых в Р. Говорят, что G — это (по Хомскому) грамматика типа 0 или грамматика с фразовой структурой, если правила имеют вид
(2	.10.1) и : : = v, где u € V+ и v £ V*.
То есть левая часть ц может быть тоже последовательностью символов, а правая часть может быть пустой. Грамматикам с фразовой структурой посвящено сравнительно немного работ. Если ввести ограничение на правила подстановки, то получится более интересный класс языков типа 1, называемых также контекстно-чувствительными или контекстно-зависимыми языками* 2). В этом случае правила подстановки имеют следующий вид:
*) Рекомендуем также читателю монографию Гладкого [73].— Прим. ред.
2) В отечественной литературе чаще используется термин грамматика непосредственно составляющих, или НС-грамматика, и соответственно НС-язык (см. Гладкий [73]).— Прим. ред.
ГРАММАТИКИ И ЯЗЫКИ
65
(2	.10.2) xUy : : = xuy, где U С V — Т, u € V+ и х, у Е V*.
Термин «контекстно-чувствительная» отражает тот факт, что можно заменить U на и лишь в контексте х . . . у. Дальнейшее ограничение дает класс грамматик, полностью подобный классу, который мы используем; грамматика называется контекстно-свободной1), если все ее правила имеют вид
(2	.10.3) U : : - и, где U С V — Т и u € V*.
Этот класс назван контекстно-свободным потому, что символ U можно заменить цепочкой и, не обращая внимания на контекст, в котором он встретился. В КС-грамматике может появиться правило вида U : = Л, где Л — пустая цепочка. Однако, чтобы не усложнять терминологию и доказательства, мы не допускаем таких правил в наших грамматиках. По заданной КС-грамматике G можно сконструировать К-свободную (или неукорачивающую) грамматику G1 (наш тип), такую, что L (Gl) = L (G) — {Л}. Более того, если G однозначна, то G1 также однозначна, поэтому фактически мы не вносим ограничений.
Если мы ограничим правила еще раз, приведя их к виду
(2	.10.4) U : : = N или U : : - WN, где N € Т, а и и W е V - т,
то получим грамматику типа 3, или регулярную грамматику 2). Регулярные грамматики играют основную роль как в теории языков, так и в теории автоматов. Множество цепочек, порождаемых регулярной грамматикой, «допускается» машиной, называемой автоматом с конечным числом состояний (которую мы определим в следующей главе), и наоборот. Таким образом, мы имеем характеристику этого класса грамматик в терминах автомата. Регулярные языки (те, что порождаются регулярными грамматиками), кроме того, называются регулярными множествами. Известно, что если Lt и Ь2 — регулярные множества, то таковыми же являются Lx и U L2, Lx П L2, Lx L2, Ll • L2 = {xy | x £ Lx и у £ L2) и Lx* ={Л} U Lx U L/ U Lx3 U .... 4
Вводя все большие ограничения, мы определили четыре класса грамматик. Таким образом, есть языки с фразовой структурой, которые не являются контекстно-чувствительными, контекстночувствительные языки, которые не являются контекстно-свободными, и контекстно-свободные, которые не являются регулярными.
2) А также бесконтекстной грамматикой, или КС-грамматикой.— Прим, ред.
2) Ее называют также автоматной грамматикой, или А-грамматикой.— Прим. ред.
3 д. Грис
66
ГЛАВА 2
В большинстве работ по теории формальных языков изучаются контекстно-свободные или регулярные языки, или их подмножества. Поэтому мы также ограничимся этими классами.
Один из основных вопросов, который возникает при рассмотрении грамматики, — это вопрос ее однозначности. К сожалению (или к счастью, в зависимости от того, как вы на это смотрите), было доказано, что эта проблема алгоритмически неразрешима для класса КС-грамматик. Это означает, что нельзя построить эффективный алгоритм (написать программу), который для любой заданной грамматики за конечное число шагов мог бы решить, является она однозначной или нет. Оказываются алгоритмически неразрешимыми и многие другие интересные вопросы, касающиеся контекстно-свободных языков (например, совпадают ли два языка, пересекаются ли два языка). В этом случае ищут интересные подклассы языков, для которых этот вопрос алгоритмически разрешим. Большинство доказательств теорем алгоритмической неразрешимости в теории формальных языков прямо или косвенно зависит от результатов теоремы Поста [46].
Доказано, что проблема однозначности для произвольной КС-грамматики алгоритмически неразрешима. Следующий вопрос, который может возникнуть: всегда ли существует однозначная грамматика для произвольного контекстно-свободного языка? Ответ отрицательный. Есть языки, для которых не существуют однозначные грамматики; первым это показал Парик [61]. Такие языки называются существенно-неоднозначными. Пример такого языка:
{а* b* d | i, j > 1} U {a1 bj cj | i, j 1).
Обширная литература посвящена интересующей нас задаче разбора предложений языка. Решение этой проблемы будет обсуждаться в последующих главах.
2.11.	РЕЗЮМЕ
Эта глава содержит базисный материал, необходимый для понимания следующих пяти глав, и читатель должен хорошо усвоить его, прежде чем продолжить чтение. Для того чтобы помочь читателю охватить материал, в конце главы приведен список терминов с указанием номеров страниц, где они определены.
метаязык (metalanguage)	29
алфавит (alphabet)	32
символ (symbol)	32
цепочка (string)	32
пустая цепочка (empty string)	32
ГРАММАТИКИ И ЯЗЫКИ
67
длина цепочки (length of a string)	32
катенация (catenation)	33
голова, хвост цепочки (head, tail, of a string)	33
правильная голова, хвост (proper head, tail)	33
произведение двух множеств (product of two sets)	33
степень цепочек (power of strings)	33
итерация множества (closure of a set)	33
усеченная итерация (positive closure)	33
продукция (production)	35
правило (rule)	35
левая часть правила (left part of a rule)	35
правая часть правила (right part of a rule)	35
грамматика (grammar)	35
начальный символ (distinguished symbol)	35
словарь (vocabulary)	35
нетерминал (nonterminal)	35
терминал (terminal)	36
НФБ (BNF)	36
непосредственный вывод => (direct derivation)	36
вывод =>+ (derivation)	36
длина вывода (length of a derivation)	37
сентенциальная форма (sentential form)	37
предложение (sentence)	37
язык (language)	37
фраза (phrase)	38
простая фраза (simple phrase)	38
основа (handle)	39
рекурсивная грамматика (recursive grammar)	39
рекурсивное правило (recursive rule)	39
правило с левой (правой) рекурсией [left (right) recursive rule] 39 синтаксическое дерево (syntax tree)	40
куст, узел (branch, node)	40
концевой узел (end node)	'	40
концевой куст (end branch)	41
узел — брат (brother of a node)	41
узел — отец (father of a node)	41
узел — сын (son of a node)	41
поддерево (subtree)	41
неоднозначное предложение (ambiguous sentence)	. 43
неоднозначная грамматика (ambiguous grammar)	43
существенная неоднозначность (inherently ambiguous)	44
разбор сентенциальной формы (parse of a sentential form)	46
нисходящий разбор (top-down parse)	46
восходящий разбор (bottom-up parse)	46
канонический разбор (canonical parse)	48
отношение (relation)	49
3*
68	ГЛАВА 2
рефлексивное отношение (reflexive relation)	50
транзитивное отношение (transitive relation)	50
транзитивное замыкание (transitive closure)	50
произведение двух отношений (product of two relations)	50
единичное отношение (identity relation)	51
приведенная грамматика (reduced grammar)-	58
Глава 3
Сканер
3.1.	ВВЕДЕНИЕ
Сканер представляет собой ту часть компилятора, которая читает литеры первоначальной исходной программы и строит слова, или иначе символы, исходной программы (идентификаторы, служебные слова, целые числа, одно- или двулитерные разделители, такие, как *, +, **,/*). Иногда символы называют атомами или лексемами. В чистом виде сканер выполняет простой лексический анализ исходной программы в отличие от синтаксического анализа, и поэтому сканер называют также лексическим анализатором.
Сразу же возникает вопрос, почему лексический анализ нельзя объединить с синтаксическим анализом. В конце концов для описания синтаксиса символов можно воспользоваться НФБ. Например, в ФОРТРАНе идентификатор можно описать следующим образом:
(3.1.1)	<идентификатор> : : = буква {буква | цифра}5
Однако есть несколько серьезных доводов в пользу отделения лексического анализа от синтаксического.
1.	Значительная часть времени компиляции тратится на сканирование литер. Выделение же позволяет нам сконцентрировать внимание на сокращении этого времени. Одним из способов сокращения времени является программирование части или всего сканера на автокоде, а это сделать легче, если сканер выделен. (Например, в IBM 360 для поиска литеры, отличной от пробела, в последовательности, содержащей от 1 до 256 литер, достаточно выполнить единственную команду TRT.) Конечно же, мы не рекомендуем пользоваться автокодом, если не предполагается широкое и массовое применение компилятора.
2.	Синтаксис символов можно описать в рамках очень простых грамматик. Если отделить сканирование от синтаксического распознавания, то можно разработать эффективную технику разбора, которая наилучшим образом учитывает особенности этих грамматик. Более того, тогда для конструирования сканеров можно разработать автоматические методы, в которых используется эта эффективная техника.
70
ГЛАВА 3
3.	Так как сканер выдает символы вместо литер, синтаксический анализ на каждом шаге получает больше информации о том, что надо делать. Более того, некоторые специфические проверки контекста, необходимые при разборе символов, проще и уместнее выполнить в сканере, чем в формальном синтаксическом анализаторе. Например, легко выяснить, что означает в ФОРТРАНе инструкция DO 101 = ... , установив, какой из символов или “(“ встречается раньше после знака равенства.
4.	Развитие языков высокого уровня требует внимательного отношения как к лексическим, так и к синтаксическим свойствам языка. Разделение этих двух свойств позволит нам исследовать их независимо друг от друга.
5.	Часто для одного и того же языка существует несколько различных внешних представлений. Например, в некоторых реализациях АЛГОЛа служебные слова заключены в кавычки и пробелы не играют никакой роли — они попросту игнорируются. В других же компиляторах служебные слова не могут использоваться как идентификаторы и смежные служебные слова и идентификаторы должны отделяться друг от друга по крайней мере одним пробелом. Внешние представления на перфоленте, картах и на телетайпе могут быть совершенно различными.
Выделение сканера позволит написать один синтаксический анализатор и несколько сканеров (которые написать проще и легче) по одному на каждое представление исходной программы и/или устройство ввода. При этом каждый сканер переводит символы в одинаковую внутреннюю форму, используемую синтаксическим анализатором.
Сканер можно запрограммировать как отдельный проход, на котором выполняется полный лексический анализ исходной программы и который выдает синтаксическому анализатору таблицу, содержащую исходную программу в форме внутренних символов. В другом варианте это может быть подпрограмма SCAN, к которой обращается синтаксический анализатор всякий раз, когда ему необходим новый символ (рис. 3.1). В ответ на каждый вызов SCAN распознает следующий символ исходной программы и отсылает его синтаксическому анализатору. Последний вариант, вообще говоря, лучше, поскольку нет необходимости конструировать целиком всю внутреннюю исходную программу и хранить ее в таком виде в памяти. В остальной части этой главы мы будем предполагать, что сканер должен быть реализован именно таким способом.
Повсюду в этой главе мы будем считать, что исходная программа пишется в формате, в котором границы полей не фиксируются, т. е. в виде непрерывной последовательности литер. Изменения и добавления, связанные с обработкой формата с фиксированными полями, а также случаи, когда конец карты означает конец инструкции (как в ФОРТРАНе), будут обсуждаться в конце разд. 3.3.
СКАНЕР
71
Иногда остается неясным различие между символами и конструкциями более высокого уровня. Например, можно рассматривать в качестве символов как целое, так и вещественное число вида
<целое). <целое>
или же можно рассматривать целое число как символ, а веществен-
Обращение
Рис. 3.1. Сканер как подпрограмма синтаксического анализа.
ное — как конструкцию более высокого уровня. В таких случаях мы будем делать произвольный выбор главным образом в интересах простоты изложения.
Использование регулярных грамматик
Большинство символов в языках программирования попадают в один из следующих классов:
идентификаторы;
служебные слова (которые являются подмножеством идентификаторов);
целые числа;
однолитерные разделители (+, —, (,),/ и т. д.);
двулитерные разделители (//, /*,	: = и т. д.).
Эти символы могут быть описаны следующими простыми правилами:
<идентификатор> :	: = буква | Идентификатор > буква | <иденти-фикатор> цифра
<целое>	: <разделитель>	: <разделитель>	:	: = цифра | <целое> цифра := +1-1(|)|/|... : = <SLASH>/I <SLASH> * 1 <AST> * I <COLON> =
<SLASH>	: <AST> <COLON>	:	* .. II .11 II
Конечно, правила могли бы быть еще проще, но мы записываем их так, чтобы каждое правило имело вид
U :: = Т или U :: = VT
72
ГЛАВА 3
где Т — терминал, а V — нетерминал. Как было отмечено в разд. 2.10, грамматика с такими правилами — это грамматика типа 3, или регулярная грамматика. Синтаксис символов большинства языков программирования можно задать в этой форме. Следовательно, целесообразно найти эффективный способ разбора предложений регулярной грамматики. Поэтому мы начнем с разработки эффективной схемы разбора.
3.2.	РЕГУЛЯРНЫЕ ВЫРАЖЕНИЯ И КОНЕЧНЫЕ
АВТОМАТЫ
Этот раздел содержит некоторые результаты, которые мы не будем формально доказывать. Нас главным образом интересует вопрос, как программировать сканеры, а для этого доказательства не потребуются. Подробные доказательства можно найти в книгах, на которые есть ссылки в разд. 3.6.
Диаграммы состояний
Рассмотрим регулярную грамматику GIZ]
Z : : = U0 | VI
U : : = Z1 | 1
V : : = Z0 | 0
Легко видеть, что порождаемый ею язык состоит из последовательностей, образуемых парами 01 или 10, т. е.
L (G) = {В" | п > 0}, где В = {01, 10}.
Чтобы облегчить распознавание предложений грамматики G, нарисуем диаграмму состояний (рис. 3.2, а). В этой диаграмме каждый нетерминал грамматики G представлен узлом или состоянием; кроме того, есть начальное состояние S (предполагается, что грамматика не содержит нетерминала S). Каждому правилу Q: Т в G соответствует дуга с пометкой Т, направленная от начального состояния S к состоянию Q. Каждому правилу Q: : = RT соответствует дуга с пометкой Т, направленная от состояния R к состоянию Q.
Мы используем диаграммы состояний, чтобы распознать или разобрать цепочку х следующим образом:
1. Первым текущим состоянием считать начальное состояние S. Начать с самой левой литеры в цепочке х и повторять шаг 2 до тех пор, пока не будет достигнут правый конец х.
2. Сканировать следующую литеру х, продвинуться по дуге, помеченной этой литерой, переходя к следующему текущему состоянию.
СКАНЕР
73
Если при каком-то повторении шага 2 такой дуги не оказывается, то цепочка х не является предложением и происходит останов. Если мы достигаем конца х, то х — предложение тогда и только тогда, когда последнее текущее состояние есть Z.
Рис. 3.2. Диаграмма состояний.
Читатель узнает в этих действиях восходящий разбор. На-каждом шаге (кроме первого) основой является имя текущего состояния, за которым следует входной символ. Символ, к которому приводится основа, будет именем следующего состояния. В качестве примера
шаа	Текущее	Остаток
	состояние	цепочки X
1	S	101001
2	и	01 001
3	Z	1001
4	и	001
5	Z	01
6	V	1
7	Z	
а	Ъ
Рис. 3.3. Разбор и синтаксическое дерево для цепочки 101001.
а — разбор; b — синтаксическое дерево.
проведем разбор предложения 101001. Каждая строка на рис. 3.3,а отражает состояние разбора перед началом выполнения шага 2.
В этом примере разбор выглядит столь простым благодаря простому характеру правил. Так как нетерминалы встречаются лишь как первые символы правой части, на первом шаге первый символ предложения всегда приводится к нетерминалу. На каждом последующем шаге первые два символа UT сентенциальной формы
74
ГЛАВА 3
UTt приводятся к нетерминалу V, при этом используется правило V: :=ит. При выполнении этой редукции имя текущего состояния— U, а имя следующего текущего состояния — V. Так как каждая правая часть единственна, то единственным оказывается и символ, к которому она приводится. Синтаксические деревья для предложений регулярных грамматик всегда имеют вид, подобный изображенному на рис. 3.3,Ь.
Чтобы избавиться от проверки на каждом шаге, есть ли дуга с соответствующей пометкой, можно добавить еще одно состояние, называемое F (НЕУДАЧА), и добавить все необходимые дуги от всех состояний к F. Добавляется также дуга, помеченная всеми возможными литерами и ведущая из F обратно в F. В результате диаграмма, показанная на рис. 3.2,а изменится и станет такой, как на рис. 3.2, Ь.
Детерминированный конечный автомат
Чтобы иметь возможность легко манипулировать с диаграммами состояний, нам необходима дальнейшая формализация концепции в терминах состояний, входных литер, начального состояния S, «отображения» М, которое по заданному текущему состоянию Q и входной литере Т указывает следующее текущее состояние, и заключительных состояний, аналогичных состоянию Z в приведенном выше примере.
(3.2.1).	Определение. (Детерминированный) автомат с конечным числом состояний (КА) — это пятерка (К, VT, М, S, Z), где
1)	К — алфавит элементов, называемых состояниями;
2)	VT — алфавит, называемый входным алфавитом (литеры, которые могут встретиться в цепочке или предложении);
3)	М — отображение (или функция) множества KxVT во множество К (если М (Q, T)=R, то это означает, что из состояния Q при входной литере Т происходит переключение в состояние R);
4)	S (£ К) — начальное состояние;
5)	Z — непустое множество заключительных состояний, каждое из которых принадлежит К.
Мы можем также формально определить, как работает КА (или диаграмма состояний) с входной цепочкой t. Сделаем это, расширив понятие отображения, которое указывает нам, как переключаются состояния в зависимости от входной литеры.
Определим
М (Q, A)=Q при любом состоянии Q;
М (Q, Tt)=M (М (Q, Т), t) для любых t£VT* и TgVT.
СКАНЕР
75
Первая строка означает, что если на входе пустой символ, то состояние остается прежним. Вторая строка показывает, что в состоянии Q и при входной цепочке Tt мы применяем М, чтобы перейти в состояние P=M(Q, Т) с входной цепочкой t, и затем применяем отображение М (Р, t). Наконец, говорят, что КА допускает цепочку t (цепочка t считается допускаемой), если М (S, t)=P, где состояние Р принадлежит множеству заключительных состояний Z.
Такие автоматы называются детерминированными, так как на каждом шаге входная литера однозначно определяет следующее текущее состояние.
(3.2.2).	Пример. Диаграмме состояний, показанной на рис. 3.2, соответствует КА ({S, Z, U, V, F}, {0, 1}, М, S, {Z}), где
м (S, 0)=V	М (S, 1)=и
М (V, 0)=F	М (V, 1)=Z
М (U, 0)=Z	M(U, 1)=F
М (Z, 0)=V	M(Z, 1)=U
М (F, 0)=F	M(F, 1)=F
Теперь остановимся на некоторое время и посмотрим, чего же мы достигли. Мы начали с рассмотрения регулярной грамматики, которая имела единственные правые части. Для нее мы смогли построить диаграмму состояний. Эта диаграмма фактически была неформальным представлением КА. Легко видеть, что если предложение х принадлежит грамматике G, то оно также допускается КА, соответствующим грамматике G. Несколько труднее показать, что для' любого КА существует грамматика G, порождающая только те предложения, которые являются цепочками, допускаемыми КА. Мы не будем здесь приводить доказательство этого утверждения, поскольку результат доказательства далее нам не потребуется.
Представление в ЭВМ
КА с состояниями Sx,..., Sn и входными литерами Тх,. . . , Тт можно представить матрицей В, состоящей из nxm элементов. Элемент В [i, j] содержит число к — номер состояния Sk, такого, что М {Sb Tj]=Sk. Можно условиться, что состояние Si — начальное, а список заключительных состояний представлен вектором. Такая матрица иногда называется матрицей переходов, поскольку она указывает, каким образом происходит переключение из одного состояния в другое.
Другим способом представления может быть списочная структура. Представление каждого состояния с к дугами, исходящими из него, занимает 2*к+2 слов. Первое слово — имя состояния, вто
76
ГЛАВА 3
рое — значение к. Каждая последующая пара слов содержит терминальный символ из входного алфавита и указатель на начало представления состояния, в которое надо перейти по этому символу.
Недетерминированный КА
Мы попадаем в затруднительное положение при построении КА, если G содержит два правила
V : :=UT и W : :=UT
с одинаковыми правыми частями. Это означает, что в диаграмме состояний есть две дуги, помеченные Т и исходящие из U, и отображение М оказывается неоднозначным! Автомат, построенный по такой диаграмме, называется недетерминировайным КА и определяется следующим образом:
(3.2.3).	Определение. Недетерминированным КА или НКА называется пятерка (К, VT, М, S, Z) где
1)	К — алфавит состояний;
2)	VT — входной алфавит;
3)	М — отображение множества KxVT в подмножества множества К;
4)	SsK — множество начальных состояний;
5)	Z=K — множество заключительных состояний.
И опять важным отличием является то, что отображение М дает не единственное состояние, а. (возможно, пустое) множество состояний.
Второе отличие состоит в том, что может быть несколько начальных состояний. Как и ранее, расширим отображение М до KxVT*, определяя
M(Q, A) = {Q) и
М (Q, Tt) — как объединение множеств М (Р, t), где P€M(Q, Т)
для каждого Т € VT и t С VT+. Продолжим расширение, определяя М ({Pi, Р2). . ., Pn}, t) как объединение множеств М (Pb t) для i = l....п.
Цепочка t допускается автоматом, если найдется состояние Р, которое принадлежит одновременно М (S, t) и множеству заключительных состояний Z.
В качестве примера рассмотрим регулярную грамматику G [Z1:
Z U1 | V0 | Z0 | Z1
U Q1 | 1
V ::= Q0 | О
Q ::= Q0 | Q1 | 0 | 1
СКАНЕР
77
Краткое рассмотрение показывает, что язык L (G) представляет собой множество последовательностей из 0 и 1, содержащих по крайней мере два смежных 0 или две смежные 1. (Чтобы убедиться в этом, начните вывод предложений из Z.) На рис. 3.4,а приводится
а
FA = ({S, Q, V, U, Z}, Ю, 1}, М ф, {Z})
M(S, 0) = {V, Q}
M(S, 1) = {U, Q}
M(V, 0) = {Z}
M(V, 1) = 0
M(Q, 0) = {V, Q}
M(Q, 1) = {U, Q}
M(U, O) = 0
M(U, 1) = {Z}
M(Z, 0) = {Z}
M(Z, 1) = {Z} b
Рис. 3.4. Диаграмма состояний и ее НКА.
соответствующая диаграмма состояний, а на рис. 3.4,b — НКА. Состояние НЕУДАЧА представлено здесь подмножеством 0, не содержащим символов.
Проблема состоит в том, что теперь на каждом шаге может быть более одной дуги, помеченной следующей входной литерой, так что мы не знаем, какой путь выбрать. На каждом шаге известна основа текущей сентенциальной формы, но не известно, к чему она приводится. Можно показать, что цепочка 01001 допускается определенной выше машиной, если на каждом шаге выбирается правильный путь. Вначале мы находимся в состоянии S. Прочитав первый 0, мы можем переключиться либо в состояние V, либо в Q; выберем Q. Так как следующий символ 1, то третье состояние либо U, либо Q; вновь выберем Q. На следующей схеме показан полный разбор:
Шаг	Текущее состояние	Остаток входной цепочки	Возможные приемники	Наш выбор
1	S	01001	V, Q	Q
2	Q	1001	U, Q	Q
3	Q	001	V, Q	V
4	V	01	Z	Z
5	Z	1	Z	Z
Теперь заметим, что для любой регулярной грамматики G можно построить диаграмму состояний и, следовательно, НКА. Диаграмма состояний может иметь любое число начальных состояний. Совер
78
Глава а
шенно очевидно, что любое предложение грамматики G допускается этим НКА; говоря иначе, при работе НКА выполняется восходящий разбор. Как и в случае детерминированного автомата, для любого НКА можно найти регулярную грамматику G, предложения которой будут именно теми цепочками, которые допускаются НКА. Доказывать это мы не будем.
Построение К.А из НК.А
Итак, при работе НКА возникает проблема, которая заключается в следующем: мы не знаем какую выбрать дугу на каждом шаге, если существует несколько дуг с одинаковой пометкой. Теперь покажем, как из НКА построить КА, который как бы параллельно проверяет все возможные пути разбора и отбрасывает тупиковые. То есть если в НКА имеется, к примеру, выбор из трех состояний X, Y и Z, то в КА будет одно состояние [XYZ], которое представляет все три. Напомним, что возможные состояния на каждом шаге работы НКА — это подмножество полного множества состояний и что число различных подмножеств конечно.
(3.2.4).	Теорема. Пусть НКА F=(K, VT, М, S, Z) допускает множество цепочек L. Определим КА F'=(K', VT, М', S', Z') следующим образом:
1.	Алфавит состояний К' состоит из всех подмножеств множества К. Обозначим элемент множества К' через [S^ S2........ SJ, где
Si, S2,.. Sj — состояния из К. (Допустим впредь, что состояния
Si, S2,...,Siрасположены в некотором каноническом порядке таким образом, что, например, состояние из К' для подмножества {Si, S2}={S2, SJ — суть [Si, SJ.)
2.	Множество входных литер VT для F и F' одно и то же.
3.	Отображение М' определим как
М' ([Si, S2.. SJ, T)=[Rlt R2,..., RJ,
где M ({Sn S2... SJ, T)={Ri, R2,..., RJ.
4.	Пусть S={Si,..., Sn). Тогда S'=lSi,..., SJ.
5.	Пусть Z={Sj, Sk,..., SJ. Тогда Z'=[Si,Sk,..., SJ.
Утверждается, что F и F'допускают одно и то же множество цепочек. Доказательство этой теоремы предоставляется читателю.
(3.2.5). Пример. На следующем рисунке слева приводится диаграмма состояний НКА с начальными состояниями S и Р и одним заключительным состоянием Z. Справа показана диаграмма состояний соответствующего КА, у которого начальное состояние [S, Р], а множество заключительных состояний |[Z], [S, Z], [Р, Z], [S, Р, Z]}.
СКАНЕР
79
Заметим, что состояния [S] и [Р, Z] можно исключить, так как нет путей, ведущих к ним. Таким образом, построенный КА не
является минимальным — возможен автомат с меньшим числом со-
стояний. Существует алгоритм, позволяющий сконструировать минимальный КА; ссылки на литературу по этому вопросу см. в разд. 3.6.
Нам состояния: S.P Закл. состояние; Z
Регулярные выражения
Оставшаяся часть этого раздела потребуется лишь для понимания разд. 3.5, в котором описывается система AED RWORD.
В разд. 2.9 речь шла о применении символов и метасимволов |, (,), { и } в правых частях правил, соответствующих одному нетерминалу. Существует тесная связь между записанными таким образом правыми частями правил и регулярными выражениями.
Пусть S={Si..... Sn}, тогда
1) 0, Л, Si,..., Sn — регулярные выражения в множестве S;
2) если е1 и е2 — регулярные выражения в S, то
(е1), {е1}, {e^n, eJe2 и е1! е2 также регулярные выражения.
Как и ранее, А — пустой символ, а 0 — пустое множество. Регулярное выражение {е)п для некоторого целого числа п эквивалентно выражению
Л|е|(е)(е)|. . . |(е) . . . (е)
где в последнем выборе (е) повторяется п раз. Мы не будем обсуждать это в дальнейшем.
Значение регулярного выражения е (обозначим его через |е[) — это некоторое множество над S, определенное следующим образом:
1.	|0|=0, пустое множество
2.	|А|={Л} (множество, состоящее из пустого символа)
80
ГЛАВА 3
3.	|SiH{Si} для i=l,. . ., n
4.	|(e)Hie|
5.	|eTe21= le11 |e21={xy |x |ex | и у |e2|}
6.	|ex|e2|= le1! u |e2|
7.	|{е}|=итерация |e|==|e|*
Если исключить символ 0, то по смыслу регулярное выражение очень напоминает множество правых частей правил для некоторого нетерминала. Например, мы пишем Е :: =0 { + 1}, чтобы указать, что синтаксическая единица Е производит символ 0, за которым следует произвольное количество цепочек +1. Регулярное выражение 0 { + 1} определяет то же самое множество цепочек. Регулярное выражение 0 можно задать с помощью грамматики, состоящей из единственного правила V ::=V, так как нет ни одной цепочки, которая принадлежала бы соответствующему языку.
Регулярные выражения оказываются удобным инструментом для описания лексических символов. Превосходной иллюстрацией этому может служить (3.1.1). Кроме того, мы покажем, как построить КА, который допускает множество |е|, соответствующее некоторому регулярному выражению е. Это означает, что язык, порождаемый любой регулярной грамматикой, можно определить с помощью одного регулярного выражения, и наоборот. Более важным, однако, является тот факт, что КА можно применить для разбора любой цепочки из множества |е|.
Построение КА по регулярному выражению
Можно построить КА по регулярному выражению за 4 шага: 1. Построить систему переходов (определяется ниже) для е. 2. Построить диаграмму состояний по системе переходов.
3.	Построить НКА по диаграмме состояний.
4.	Построить КА по НКА.
Шаги 3 и 4 уже были описаны, пояснения необходимы лишь к шагам 1 и 2. Использование системы переходов упрощает процесс комбинирования в одно целое диаграмм, представляющих несколько регулярных выражений.
(3.2.6).	Определение. Система переходов — это диаграмма с одним начальным состоянием S и одним заключительным состоянием Z. Состояние S не имеет входящих дуг, a Z — исходящих. Кроме того, дуга может быть помечена пустым символом А.
На рис. 3.5, Ь изображена система переходов. Она допускает то же самое множество цепочек, что и соответствующая диаграмма на рис. 3.5, а. Общий принцип «работы» в системе переходов тот же, что и в диаграмме состояний. Нам необходимо только объяснить смысл дуг, помеченных А. Если А — текущее состояние и дуга, помеченная Л, ведет в состояние В, то мы полагаем, что текущим
СКАНЕР
81
состоянием является либо А, либо В, т. е. следующее состояние может быть преемником либо А, либо В.
(3.2.7).	Теорема. Существует система переходов для любой диаграммы состояний.
Построение. Пусть S — диаграмма состояний с начальными состояниями Si, S2,..., Sn и заключительными состояниями
а	ъ
Рис. 3.5/ Диаграмма состояний и соответствующая ей система переходов.
Zn Z2,..., Zm- Добавим новое состояние S, единственное теперь начальное состояние, с дугами, помеченными Л и ведущими в Si, S2,..., Sn. Добавим новое состояние Z, заключительное состояние, с дугами, помеченными Л, ведущими в него из Zlt Z2,..., Zm- На рис. 3.5, b изображена система переходов, соответствующая диаграмме состояний на рис. 3.5, а.
(3.2.8).	Теорема. Для всякого регулярного выражения е существует система переходов, которая допускает |е|.
Построение. Системы переходов для 0, Л, Sj будут соответственно
Система переходов для (е) — та же, что для е. Предположим, что имеются системы переходов для е1 и е2 и они изображаются как
82
ГЛАВА 3
где S — начальное, a Z — заключительное состояния. Тогда системами переходов для ех |е2 и еге2 будут соответственно
В системе переходов для заключительное состояние ег было отождествлено с начальным состоянием е2. В случае ег |е2 попарно отождествляются начальные и заключительные состояния. На следующей диаграмме изображена система переходов для регулярного выражения {ех}. Мы предоставляем читателю возможность убедиться в том, что сконструированная система переходов действительно является искомой системой.
Последний шаг — построение диаграммы состояний по системе переходов. Необходимо избавиться от дуг, помеченных А. Следующий алгоритм исключает по крайней мере одну такую дугу, не изменяя при этом множество цепочек, допускаемых системой (при этом может возрасти число начальных или конечных состояний). Если мы повторим этот алгоритм достаточное число раз, то все дуги, помеченные А, будут исключены.
1.	Найти состояние, из которого ведет дуга, помеченная А, пройти по этой дуге в следующее состояние и идти по дугам, помеченным А, до тех пор, пока не встретится ситуация
где из В не исходит дуг, помеченных А. Если такой ситуации не окажется, то выполнить шаг 3; в противном случае выполнить шаг 2.
2.	Если существует последовательность дуг, помеченных А и ведущих из начального состояния в А (а значит, и в В), то сделать В начальным состоянием. Если В — заключительное состояние, то сделать А заключительным состоянием. Исключить дугу, помеченную А и направленную от А к В. В дополнение к каждой дуге, помеченной St и ведущей из В в состояние С, завести дугу, поме
СКАНЕР
83
ченную S1 и ведущую из А в С. Действия этого шага иллюстрируются на рис. 3.6.
3.	Если не оказалось ситуации, описанной в шаге 1, то должна существовать последовательность состояний А1( Аа,..., Ап, Аь такая, что есть дуга с пометкой Л, ведущая из каждого Aj в Ai+, для i = l,..., п — 1, и, кроме того, есть дуга с пометкой Л, ведущая
а	ь	с
Рис. 3.6. Исключение дуг, с пометкой X (шаг 2).
а — нач. состояние S, закл. состояние Z;
Ъ — нач. состояния S,Z, закл. состояния Z.X; с—нач. состояния S,Z,X, закл. состояния Z,X,S.
из Апв At. (Доказательство предоставляется читателю.) Отождествить все эти состояния, т. е. заменить их одним состоянием. Исключить все упомянутые дуги с пометкой Л.
а,	Ъ
Рис. 3.7. Исключение дуг, с пометкой Л (шаг 3). а—нач. состояние S, закл. состояние Z; b — нач. состояние S', закл. состояние Z.
Если какое-либо из состояний A t было начальным (заключительным) состоянием, то новое состояние сделать начальным (заключительным). Действия этого шага иллюстрируются на рис. 3.7.
84
ГЛАВА 3
УПРАЖНЕНИЯ К РАЗДЕЛУ 3.2
1.	Постройте автомат для следующей грамматики G [ZJ. Детерминированный ли этот автомат? Какой язык ему соответствует?
Z ::=А0 A ::=AO|Z1|O
2.	Постройте КА, который допускает все цепочки из 0 и 1, такие, что за каждой 1 непосредственно следует 0. Постройте регулярную грамматику для этого языка.
3.	Постройте КА из НКА, представленного на рис. 3.4.
4.	Постройте КА из НКА ({X, Y, Z}, {0, 1}, М, {X}, {Z}), где
М (X, 0)= {Z} М (Y, 0)={Х, Y} М (Z, 0)={Х, Z),
М(Х, 1)={Х} М (Y, 1)=0 M(Z, 1)={Y}.
5.	Постройте НКА для следующих регулярных выражений:
a) ((A|B)|{A|C})|DEM; b) {{0|1}|(1 1)}
6.	Докажите теорему (3.2.4).
3.3. ПРОГРАММИРОВАНИЕ СКАНЕРА
Покажем, как программируется сканер для небольшого и простого языка. Собственно, нас интересуют лишь символы языка, поскольку именно их построение является целью лексического анализа. Мы не будем формально применять ранее изложенный теоретический материал, но именно теория служит основой для понимания интересующей нас проблемы и будет направлять нас в работе над ней.
Символы исходного языка
Символами в языке являются разделители или операторы (/, + ,—>*,(,) и //), служебные слова (BEGIN, ABS и END), идентификаторы (которые не могут быть служебными словами) и целые числа. По крайней мере один пробел должен отделять смежные идентификаторы, целые числа и/или служебные слова. Ни одного пробела не должно появляться между литерами в слове. Идентификаторы и целые числа имеют вид
буква {буква|цифра} и цифра {цифра}
В добавление ко всему сканер должен распознавать и исключать комментарий. Комментарий начинается с двулитерного символа /* и заканчивается при первом появлении двулитерного символа */. Следующая строка является примером одного комментария:
/» Это * / / один комментарий */
СКАНЕР
85
Результат работы сканера
Сканер строит внутреннее представление для каждого символа. В большинстве случаев это целое число фиксированной длины (байт, полуслово, слово и т. д.). В других частях компилятора гораздо эффективнее обрабатываются эти целые числа, чем цепочки переменной длины, которыми фактически представляются символы.
Во внутреннем представлении некоторый номер обозначает «идентификатор», другой номер — «целое». Таким образом, во
Внутреннее представление	Символ	Мнемоническое имя
0	Не определен	SUND
1	Идентификатор	SID
2	Целое	SINT
3	BEGIN	SBEGIN
4	ABS	SABS
5	END	SEND
6	/	SSLASH
7	+	SPLUS
8	—	SMINUS
9	*	SSTAR
10	(	SLPAR
11	)	SR PAR
12	//	SSLSL
Рис. 3.8. Внутреннее представление символов.
внутреннем представлении все идентификаторы обозначаются одним и тем же числом. Это естественно, поскольку «идентификатор» для синтаксического анализатора является терминальным символом, и поэтому безразлично, какой идентификатор встречается в каждом конкретном случае. Однако при семантическом анализе приходится иметь дело с самим идентификатором, поэтому его необходимо сохранить. Вопрос исчерпан, если сканером выдается две величины: первая — внутреннее представление, вторая — фактический символ или ссылка на него. (Иногда проще, если сканер содержит таблицу всех различных символов и в качестве второй величины выдает целое число фиксированной длины — индекс позиции в этой таблице.)
Для остальной части этой главы договоримся о внутреннем представлении символов (рис. 3.8). Заметим, что всем символам при
86
ГЛАВА 3
писываются мнемонические имена; с ними нам и придется оперировать. В том языке программирования, на котором пишется сам сканер, знак $ относится к буквам и может быть использован в идентификаторах. Имя можно связать с числом либо через макроопределение, либо используя переменную, которой присваивается это число при инициализации. В программировании этот прием имеет особую ценность. Благодаря ему облегчается внесение изменений в программу; программа становится более наглядной.
Рассмотрим, например, сегмент программы
BEGIN А+/ВС// /* COMMENT ++*/END 11
Сканер передаст вызывающей его программе следующее:		
Шаг	Результат	Смысл
1	3, 'BEGIN'	BEGIN
2	1, 'А'	Идентификатор А
3	7, ' + '	+
4	6, 'Г	/
5	1, 'ВС'	Идентификатор ВС
6	12, '//'	И
7	5, 'END'	END
8	2, '11'	Целое 11
Диаграмма состояний
Начнем реализацию сканера с того, что нарисуем диаграмму состояний, показывающую, как ведется разбор символа (рис. 3.9). Здесь есть несколько моментов, которые стоит обсудить.
Метка D используется вместо любой из меток 0, 1, 2,..., 9, т. е. D представляет класс цифр. Это делается для упрощения диаграммы. Аналогично метка L представляет класс буквы А, В,..., Z, а DELIM представляет класс разделителей, состоящих из одной литеры + , —,*, (и ). Заметим, что литера / не принадлежит классу разделителей, так как она должна обрабатываться особым образом.
Некоторые дуги не помечены. Эти дуги будут выбраны, если сканируемая (обозреваемая) литера не совпадает ни с одной из литер, которыми помечены другие дуги. Например, если мы находимся в состоянии INT, то будем оставаться в этом состоянии до тех пор, пока сканируются цифры. Если же сканируется не цифра, то проходим по дуге, ведущей в OUT.
Дуги, ведущиев OUT и ERROR, являются еще одной особенностью диаграммы состояний. Они говорят лишь о том, что обнаружен кэнец символа и необходимо покинуть сканер. Одна из проблем, которая возникает при переходе в OUT, состоит в том, что в этот момент не всегда сканируется литера, следующая за распознанным
СКАНЕР
87
символом. Так, литера выбрана при переходе в OUT из INT, но она еще не выбрана, если только что распознан разделитель (DELIM). Когда потребуется следующий символ, необходимо знать, сканировалась уже его первая литера или нет. Это можно сделать, используя, например, переключатель. Мы же в дальнейшем будем считать, что перед выходом из сканера следующая литера всегда выбрана.
Еще один момент. Мы составили детерминированную диаграмму. Таким образом, мы позаботились о проблеме детерминированности,
Рис. 3.9. Диаграмма состояний для сканирования символов.
т. е. мы сами определяем,— что начинается с литеры “/”:$SLASH, $SLSL или комментарий,— поскольку всегда идем в общее для них состояние SLA.
Эта неформальная диаграмма состояний помогла нам составить представление о том, как выполняется разбор символов. Теперь посмотрим, как добавить «семантику», т. е. добавить информацию, которая скажет нам, что надо делать с тем или иным символом в процессе его построения.
Глобальные переменные и необходимые подпрограммы
Для работы сканера требуются следующие переменные и подпрограммы:
1.	CHARACTER CHAR, где CHAR — глобальная переменная, значением которой всегда будет сканируемая литера исходной программы.
2.	INTEGER CLASS, где CLASS содержит целое число, которое характеризует класс литеры, находящейся в CHAR. Будем считать,
88
ГЛАВА 3
что D (цифра)=1, L (буква)=2, класс, содержащий литеру /, =3 и класс DELIM=4.
3.	STRING А — ячейка, которая будет содержать цепочку (строку) литер, составляющих символ.
4.	GETCHAR — процедура, задача которой состоит в том, чтобы выбрать следующую литеру исходной программы и поместить ее в CHAR, а класс литеры — в CLASS. Кроме того, GETCHAR, когда это необходимо, будет читать и печатать следующую строку исходной программы и выполнять другие подобные мелочи.
Гораздо предпочтительнее использовать для таких целей отдельную подпрограмму, чем повторять группу команд в тех местах, где необходимо сканировать следующую литеру.
5.	GETNONBLANK. Эта программа проверяет содержимое CHAR, и если там пробел, то повторно вызывается GETCHAR до тех пор, пока в CHAR не окажется литера, отличная от пробела.
Добавление семантики в диаграмму состояний
Теперь, когда мы имеем общее представление о разборе символов, добавим команды, которые обеспечат построение символов. Как это сделать, показано на рис. 3.10. По существу диаграмма осталась той же, что и на рис. 3.9, но под каждой дугой появились команды, которые надлежит выполнить. К тому же мы явно ввели команду GC, сокращенно обозначив таким образом GETCHAR. Неявно это было сделано и на рис. 3.9. Мы также вынуждены были несколько расширить блок-схему в той части, где распознается конец идентификатора, чтобы проверить, не является ли набранный идентификатор служебным словом. Выражение OUT (С, D) означает возврат к программе, которая вызывала сканер, с двумя величинами С и D в качестве результата.
Под первой дугой, ведущей к состоянию S, записана команда INIT, которая указывает на необходимость выполнения подготовительных действий и начальных установок (инициализации). В данном случае выполняются команды
GETNONBLANK; А:='
Тем самым в CHAR заносится литера, отличная от пробела, и производится начальная установка ячейки А, в которой будет храниться символ. Команда ADD означает, что литера из CHAR добавляется к символу в А следующим образом:
А : =А CAT CHAR;
Команда LOOKUP осуществляет поиск символа, набранного в А, по таблице служебных слов. Если символ является служебным словом, то соответствующий индекс заносится в глобальную переменную J, в противном случае туда записывается 0.
СКАНЕР
89
Обратите внимание на тот факт, что при обнаружении комментария или ошибки мы не выходим из сканера, а начинаем сканировать следующий символ.
Проследим за разбором символа //. Начнем с инициализации (INIT). Затем переходим в состояние S. Проверяем CHAR, находим,
Рис. 3.10. Диаграмма состояний с семантическими процедурами.
что там /, и по соответствующей дуге переходим в состояние SLA. В момент перехода добавляем / в А (команда ADD) и вызываем GETCHAR, чтобы сканировать следующую литеру. В состоянии SLA мы проверяем новую литеру в CHAR и выбираем дугу, помеченную /. В момент перехода по дуге добавляем литеру из CHAR к цепочке в А и сканируем следующую литеру (GC). Заканчиваем работу возвратом к программе, которая вызвала сканер, с парой величин (SSLSL, 0). Заметим, что к моменту выхода литера следующего символа находится в CHAR.
Рекомендуем читателю самостоятельно проследить разбор следующих символов: +, 235, BEGIM, BEGIN, /«-COMMENT*/ и /.
Программа
Ниже приводится процедура, запрограммированная непосредственно по диаграмме состояний, которая изображена на рис. 3.10. Процедура имеет два параметра: первый параметр — внутреннее
90
ГЛАВА 3
представление получаемого символа; второй — цепочка литер, составляющих символ. В другом варианте SYN и SEM могли бы быть глобальными ячейками для сканера и для всех тех мест компилятора, в которых вызывается сканер. Этот вариант, вообще говоря, обеспечивает более высокую эффективность, и его следует рекомендовать.
В процедуре используется инструкция CASE, которая по номеру класса литеры, содержащейся в CHAR, определяет, какую надо выбрать дугу, ведущую из начального состояния S.
PROCEDURE SCAN (INTEGER SYN; STRING SEM)
START:GETNONBLANK; A:=' CASE CLASS OF
BEGIN
BEGIN WHILE CL ASS =1 DO BEGIN A:-A CAT CHAR;
GETCHAR;
END; SYN:=§INT;
END;
BEGIN WHILE CLASS <2 DO
BEGIN A: =A CAT CHAR; GETCHAR;
END; SYN:~$ID; LOOKUP(A);
IF J * 0 THEN SYN: = J;
END;
BEGIN A : =CHAR; GETCHAR; IF CHAR ='*' THEN
BEGIN B: GETCHAR;
C:IF CHAR # V THEN GOTO B;
GETCHAR;
IF CHAR # ,/t THEN GOTO C;
GETCHAR; GO TO START END;
Строка инициализации.
Инструкция CASE используется для перехода по дуге из состояния S в INT, ID, SLA и т. д.
CLASS=1 означает цифру. Состояние INT. Сканировать литеры и добавлять их в А до тех пор, пока будет обнаружена не цифра. Затем сформировать параметр SYN.
Состояние ID — в него приходят, если в CHAR — буква.
Сканировать литеры до тех пор, пока не будут обнаружены L или D.
Проверить, является ли идентификатор служебным словом.
Если это так, то сформировать SYN.
Состояние SLA.
Если следующая литера *, то — комментарий. Сканировать до тех пор, пока не встретится другая *.
Следующая литера может быть / (конец комментария).
Следующая литера / — комментарий кончился. Сканировать следующую литеру и начать формирование нового символа.
СКАНЕР
91
IF CHAR ='/' THEN
BEGIN А : = A CAT CHAR; GETCHAR; SYN := $SLST; END; ELSE SYN := «SLASH END;	Дуга помечена / — выход из состояния SLA. Это непомеченная дуга из состояния SLA.
BEGIN LOOKUP(A); SYN : = J; GETCHAR; END;	Найти номер разделителя и занести его в SYN.
BEGIN ERROR; GETCHAR; GO TO START; END	Встретилась недопустимая литера. Исключить ее и продолжить работу. Конец инструкции CASE.
END;	SEM	A;	Конец работы сканера.
Обсуждение
Есть несколько моментов, которые стоит обсудить. Прежде всего заметим, что сканер всегда строит по возможности наиболее длинный символ. Следовательно, АВС12 — это простой идентификатор, а не идентификатор, за которым следует целое число. Такой принцип вполне оправдан в большинстве случаев, но не во всех. Известным примером является инструкция ФОРТРАНа DO10I = 1,20, где DO10I — не идентификатор, а ключевое слово DO, за которым следует номер инструкции 10, за которым в свою очередь следует идентификатор I. Очевидно, что для ФОРТРАНа наш метод не будет идеальным. Обычно в таких случаях довольно легко написать небольшую программу, которая «просмотрит вперед» исходную программу и вынесет для таких случаев соответствующее решение.
Язык ФОРТРАН к тому же имеет фиксированные поля, т. е. для поля метки отводятся колонки 1—5, а для поля инструкции — колонки 7—72. Изменения, которые необходимы для представления языков с фиксированными полями, весьма просты, и мы не будем здесь о них говорить.
ФОРТРАН и многие другие языки допускают только одну инструкцию на карте. Сканер для ФОРТРАНа должен также следить за картами продолжения, которые отмечены в шестой колонке литерой, отличной от пробела. Проще всего эту проблему решить в программе GETCHAR. Когда GETCHAR достигает конца карты, читается следующая карта и проверяется литера в шестой колонке. Если там не пробел, то программа продолжает пересылать литеры из колонок 7, 8...; если пробел, то в сканер отсылается специальная литера «конец карты», которая затем передается синтаксическому анализатору как специальный внутренний символ. Эти проблемы удобно выделить и решать в одном наиболее подходящем мес
92
ГЛАВА 3
те, а не предусматривать их проверку на протяжении всей программы.
В некоторых представлениях исходных языков пробелы полностью игнорируются (исключение составляют строки). Эта возможность легко реализуется, если подпрограмма GETCHAR пропускает «игнорируемые» литеры. Другой вариант — при каждом состоянии иметь дугу (помеченную пробелом), которая ведет из этого состояния обратно в это же состояние. Подобное решение можно предпочесть в том случае, когда программа GETCHAR используется несколькими различными сканерами. Это и будет нашей следующей темой.
Некоторые языки программирования могут быть разбиты на несколько частей, каждая из которых имеет совершенно различные правила для образования символов. Так, в ФОРТРАНе обычные инструкции отличаются от инструкции FORMAT. В АЛГОЛе пробелы имеют смысл внутри строк, тогда как вне строк они полностью игнорируются. Некоторые языки позволяют включать в программу автокодные команды.
В таких случаях целесообразно иметь разные сканеры для каждой части языка и иметь команды (вызовы процедур), которые позволяют переключиться с одного сканера на другой. Осуществление этого — довольно простая задача, и она не нуждается в дальнейших обсуждениях.
Класс (CLASS) литеры в программе GETCHAR вычисляется совсем просто и эффективно, если имеется вектор С. Тогда, используя код литеры (EBCDIC, ASCII или BCD) как целочисленный индекс i, находим элемент С (i), который содержит номер класса литеры i. При этом, конечно, предполагается, что каждая литера принадлежит только одному классу. Чтобы просмотреть текст вперед до первой литеры, отличной от пробела, и занести ее класс в регистр, можно воспользоваться командами, такими, например, как команда TRT в IBM 360.
УПРАЖНЕНИЯ К РАЗДЕЛУ 3.3
1. Напишите процедуры GETCHAR и GETNONBLANK на нестандартном АЛГОЛе.
2. Составьте программу полного сканера на некотором языке высокого уровня и отладьте ее.
3.4.	КОНСТРУИРОВАНИЕ СКАНЕРОВ
В большинстве языков программирования общая структура символов достаточно одинакова, и поэтому можно иметь один общий алгоритм сканирования для всех языков. Алгоритм сканирования
СКАНЕР
93
будет использовать несколько таблиц, и именно эти таблицы будут изменяться в зависимости от языка. Следовательно, можно составить программу-конструктор, которая по описанию символов языка программирования готовит таблицы, соответствующие этому языку (рис. 3.11). Как только конструктор построил таблицы, сам он
Рис. 3.11. Конструктор и сканер.
становится ненужным. Поэтому можно отбросить все, что на рисунке расположено слева от двойной линии.
Мы начнем с описания общей структуры символов (3.4.1). Затем (3.4.2) нарисуем диаграмму состояний алгоритма сканирования, а также опишем таблицы, необходимые для этого алгоритма. И, наконец, покажем, как будет выглядеть входная информация для конструктора.
3.4.1.	Структура символов
Нам необходимо разбирать следующие типы символов:
1.	Идентификаторы и служебные слова вида «(начальная литера) {<другие литеры)}.
В общем случае начальная литера должна быть буквой, в то время как другие литеры могут быть либо буквами, либо цифрами. Некоторые языки допускают присутствие в идентификаторах $,__, —
и других литер. Кроме того, иногда ограничивается максимальное число литер в идентификаторах.
2.	Целые числа вида <цифра> {<цифра>}.
Цифры вообще — это 0,1,..., 9, но для восьмеричной системы счисления — это лишь 0, 1, ..., 7, а для шестнадцатеричной 0, 1, ..., 9, А, ..., F.
3.	Однолитерные разделители, такие, как /, —, +.
4.	Двулитерные разделители //, /*.
94
ГЛАВА 3
5.	«Ключевые слова», такие, как 'BEGIN' в АЛГОЛе или .EQ. и .AND. в некоторых реализациях ФОРТРАНа IV. В некоторых реализациях «ключевые слова» не могут использоваться как идентификаторы, что намного упрощает сканирование и грамматический разбор.
Помимо этого, мы должны лишь определить значение, которое имеют пробелы и литеры, подобные им. Игнорируются ли они полностью или указывают на окончание других символов, или же они относятся к обычным литерам? В нашем сканере не будет реализован разбор комментария, так как допускается возможность переключения между различными сканерами. Таким образом, когда обнаруживается символ начала комментария, можно переключиться для его обработки на другой сканер.
3.4.2.	Алгоритм сканирования
Классы литер
Исследование типов символов, которые будут подвергаться разбору, указывает на необходимость выделения следующих классов литер:
1. DIGIT (цифра)	—литеры, которые могут появиться в символе «целое» (INTEGER).
2. IDBEG (иднач)	— литеры, которые могут появиться в качестве первой литеры «идентификатора».
3. IDCHAR (идлит)	—литеры, которые могут появиться на месте второй, третьей, четвертой, ..., литеры «идентификатора».
4. IGNORE (игнор)	—литеры, которые сканер полностью игнорирует или исключает (такие, как пробел в некоторых реализациях АЛГОЛа).
5. INVTERMIN (консим)	—литеры, которые сигнализируют о конце формируемого символа, но которые во всех других случаях игнорируются, и сканер должен их исключать (подобно пробелу в некоторых реализациях АЛГОЛа).
6. DELIM (разд)	—литеры, которые сами являются символами, такие, как +, —, /, ( и ). Они, конечно, могут затем использоваться для образования двулитерных разделителей или для выделения ключевых слов.
СКАНЕР
95
Большинство этих классов не должны пересекаться, т. е. никакая литера не может принадлежать двум классам. Перекрытие допустимо только для классов DIGIT и IDCHAR. В нашем случае, исходя из целей эффективности, мы фактически разобьем разделители DELIM на четыре следующих класса:
6.	Те разделители, с которых не начинаются двулитерные символы или ключевые слова.
7.	Те разделители, с которых начинается по крайней мере одно ключевое слово, но ни один двулитерный символ.
8.	Те разделители, с которых начинается по крайней мере один двулитерный символ, но ни одно ключевое слово.
9.	Те разделители, с которых начинаются и ключевое слово, и двулитерный символ.
Диаграмма состояний
Используя эти классы, мы теперь можем нарисовать диаграмму состояний, как на рис. 3.12. Эта диаграмма имеет одну дополнительную особенность — возврат, которую нам до этого не приходилось использовать. Предположим, что на вход поступают литеры .ED., причем с начинается по крайней мере одно ключевое слово. Возникает вопрос, является ли .ED. одним символом или это три символа ., ED и .. Мы не узнаем этого до тех пор, пока не просмотрим список символов. Если .ED. не является символом, мы попросим программу GETCHAR «взять назад» литеры с тем, чтобы мы могли начать сканирование сначала. С этой целью позиция начальной литеры некоторым образом отмечается (MARK), и затем, когда возникает необходимость, происходит возврат (BACKUP) и программа GETCHAR начинает снова с последней отмеченной литеры. Заметим, что мы возвращаемся назад также и через игнорируемые литеры. И дело не только в том, что пришлось бы копировать «хорошие» литеры, которые должны повторно «сканироваться». Обусловлено это тем, что при распознавании символа в любое время может произойти переключение на другой сканер и, таким образом, изменится смысл литер.
Поскольку в нашем случае характер возврата строго определен, необходимо помнить лишь о последней отмеченной литере в течение ограниченного промежутка времени. Как только мы выдаем символ, нам не нужно дальше хранить отметку.
Таблицы для алгоритма сканирования
Конкретное представление необходимой информации может очень сильно зависеть от машины и языка, на которых реализуется алгоритм сканирования. Тем не менее можно дать некоторые общие рекомендации.
96
ГЛАВА 3
Прежде всего необходима таблица всех символов языка вместе с их внутренним представлением. Она включает все разделители, служебные и ключевые слова. Таблица используется в команде
Рис. 3.12. Алгоритм сканирования.
LOOKUP для поиска внутреннего представления символа. Так как это случается очень часто, следует, по-видимому, предпочесть хеш-таблицу (см. гл. 9).
Вторая таблица используется для того, чтобы выяснить класс литеры, когда сканер находится в состоянии S. Эту таблицу лучше всего организовать как вектор V, а код литеры использовать в качестве индекса: для литеры С V(C) содержит класс. Заметим, что
СКАНЕР
97
в данном случае не важно, принадлежит литера классу IDCHAR или нет. Здесь нас интересуют только классы IGNORE, INV-TERMIN, DIGIT, IDBEG и DELIM.
Нам потребуется и третья таблица, которая используется в состояниях ID и KY2 для того, чтобы различать классы IG, IDCHAR и другие.
3.4.3.	Входная информация для конструктора таблиц
Конструктору надо задать только распределение литер по классам и все служебные слова вместе с их внутренним представлением. Работа самого конструктора заключается в том, чтобы прочитать
определение сканера	и построить внутренние таблицы, которые
использует алгоритм сканера следующий:	сканирования. Синтаксис для определения
<сканер>	:: =	- BEGIN Идентификатор г> [Идентификатор 2>] {<опр. класса>} {<опр. символа>} END
<опр. класса> <имя класса>	:: =	= Имя класса> <список литер> = DIGIT | IDBEG | IDCHAR | IGNORE | INVTERMIN | DELIM
<список литер> <опр. символа>	:: = <символ>	::	— [ALLBUT] <литера> |<литера>} = RES <целое> <символ> {<символ>} = <литера> <литера>}| “последовательность литер EBCDIC, отличных от пробела”
<литера>	:: =	= “EBCDIC литера, отличная от пробела” |<16-теричная цифра> <16-теричная цифра>
<16-теричная цифра> ::	= 0|l|2|3|4|5|6|7|8|9|A|B|C|D]E|F
Определяемый сканер получит имя <идентификатор 1>. Это имя программист будет использовать в тех местах системы, где есть обращение к сканеру. <Идентификатор2> — имя подпрограммы, назначение которой будет объяснено позднее.
Л итеры
Чтобы можно было использовать любые внутренние литеры, мы разрешаем задавать <литера>либо как литеру в коде EBCDIC, либо как ее шестнадцатеричное представление. Пробел должен всегда задаваться шестнадцатеричным представлением в коде EBCDIC: Х'40'.
Определение классов
В Определение класса) указывается имя класса и описываются литеры этого класса. Каждая <литера) должна отделяться по крайней мере одним пробелом от других литер. Использование ALLBUT (все, кроме) указывает на то, что классу принадлежат все литеры, кроме тех, что перечислены в списке, и тех, которых нет в
Д. Грис
98
ГЛАВА 3
других классах. ALLBUT можно использовать только однажды в определении сканера.
Вот какими должны быть, например, определения классов для языка, описанного в разд.
DIGIT IDBEG
3.3:
0123456789
ABC DEFGH I JKLM NOPQRSTUVWXYZ ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789 40
*/ + 0 ( )
IDCHAR
INVTERMIN DELIM
Предположим, что мы хотим определить сканер для комментария, который будет использоваться всякий раз, когда другой сканер распознает символ начала комментария. Таким образом, нам необходимо сканировать лишь до тех пор, пока не будет обнаружен символ конца. Предположим, что это Определения классов такие:
IGNORE ALLBUT; DELIM;
Определение символов
Определение символа имеет следующий формат:
RES (целое) (символ) {(символ)}
В этом определении (целое) соответствует внутреннему представлению первого (символ) в списке; с каждым последующим (символ) сопоставляется следующее большее целое число. Могут встретиться несколько определений символов. По крайней мере один пробел должен отделять RES от (целое), (целое) от первого (символ) и каждый (символ) от соседнего. Различным символам могут соответствовать одинаковые внутренние представления.
Любой (символ) может быть представлен последовательностью литер в коде EBCDIC, кроме пробела (исключение составляют “RES”, и “END”), или последовательностью пар шестнадцатеричных цифр. Для того чтобы использовать последние в качестве символов, мы применяем “|” как знак катенации или используем последовательность из двух (16-теричное число) для того, чтобы обозначить внутреннее представление. Например, мы можем определить символ “RES” как 40 | R | Е | S, а символ “ ’” — как 7D.
Необходимо добавить, что каждый символ должен иметь один из следующих форматов:
DELIM [DELIM]
DIGIT {DIGIT} IDBEG {IDCHAR}
DELIM IDBEG {IDCHAR} DELIM
СКАНЕР
99
Ниже мы приводим определения символов для простого языка, описанного в разд. 3.3:
RES 3 BEGIN ABS Е |N|D
RES 6 /4-----* ( )
RES 12 //
Вызов подпрограммы
Для того чтобы сделать всю систему более гибкой, мы разрешаем указать имя подпрограммы ^идентификатор 2», которая вызывается всякий раз, когда прочитана новая строка исходной программы. Цель состоит в том, чтобы допустить предварительный просмотр строки и внесение в нее любых необходимых изменений. Этим можно воспользоваться для перехода от формата с фиксированными полями к свободным полям с включением в строку некоторых внутренних символов или для обнаружения карты продолжения или карты комментария. Конечно, на такую подпрограмму не должна возлагаться слишком большая обработка; это свело бы на нет все усилия, направленные на создание быстрого, автоматического сканера.
Сама программа имеет два строковых параметра. Первый указывает входную строку, второй — имя переменной, содержащей фактическую (выходную) строку, которую должен обрабатывать сканер. Подпрограмма, если она вызывается, должна присвоить строку, которую надлежит сканировать, переменной, обозначенной вторым параметром.
Приведем пример подпрограммы предварительной обработки для ФОРТРАНа. Если карта не является ни картой комментария, ни картой продолжения, то первой в выходную строку помещается внутренняя литера Х'ОЗ'. Она определяется как разделитель и используется в качестве признака конца инструкции на предыдущей карте. Затем содержимое 6-й колонки заменяется литерой Х'04', которая будет служить признаком конца поля метки. Комментарий (Св 1-й колонке) исключается, и, таким образом, не требуется его сканирование. На картах продолжения используются только колонки с 7-й по 72-ю.
PROCEDURE FORT (STRING IN, OUT);
BEGIN
IF SUBSTR (IN, 0, 1) = 'C' THEN OUT ;= X'03'
ELSE IF SUBSTR (IN, 5, 1) # " THEN OUT := SUBSTR (IN, 6, 66)
ELSE BEGIN OUT :=X'O3'
Процедура предпроцессора для карты ФОРТРАНа.
Если на карте комментарий, то конец предыдущей карты.
Если карта продолжения, выбираем колонки 7—72, в противном случае завершаем предыдущую инструкцию.
4*
100
ГЛАВА 3
CAT SUBSTR (IN, 0, 72); SUBSTR (OUT, 5, 1)
X'04';
END
END
Передаем на обработку колонки 1—72 и записываем признак конца поля метки.
УПРАЖНЕНИЕ К РАЗДЕЛУ 3.4
1.	Запрограммируйте и отладьте общий алгоритм сканирования на некотором удобном для вас языке.
3.5.	СИСТЕМА AED1) RWORD
В предыдущем разделе был рассмотрен сканер и конструктор для сканирования символов, структура которых ограничена четырьмя форматами:
DIGIT {DIGIT}
IDBEG {IDCHAR}
DELIM [DELIM]
DELIM IDBEG {IDCHAR} DELIM
Синтаксис символов был ограничен так, чтобы алгоритм сканирования был простым и допускал быструю и эффективную реализацию.
В системе AED RWORD (см. Джонсон и др. [68]), которая является существенной частью системы AED-1, предназначенной для генерации компиляторов, интерпретаторов, операционных систем и т. д., принят другой подход. Система конструирует сканер, подобно тому как это делает предшествующая система, но она является гораздо более общей. В ней может быть определено любое число классов и в терминах этих классов можно описать любую структуру символов, которая поддается разбору с помощью КА. Кроме того, к программе, составленной пользователем, можно обращаться не только в момент, когда считана новая карта, а всякий раз, когда построен символ определенного типа. Эта общность позволяет использовать систему и для многих других целей, помимо конструирования сканеров.
Не следует думать, что из-за универсальности система непременно будет неэффективной. Согласно Джонсону, сканер, сконструированный с помощью RWORD на IBM 7094, обрабатывал 3000 литер
l) AED расшифровывают как Automated Engineering Design (автоматизированное инженерное проектирование) или как ALGOL Extended for Design (расширенный АЛГОЛ для проектирования).— Прим. ред.
СКАНЕР
101
в секунду, в то время как программа анализатора для той же самой цели, тщательно составленная вручную, работала со скоростью 8000 литер в секунду. В настоящий момент к системе добавляются новые методы обработки наиболее общих часто встречающихся символов (таких, как идентификаторы), чтобы еще более сократить расход времени на сканирование.
Обсуждение RWORD мы начнем с рассмотрения входной информации для конструктора. Примите к сведению, что в интересах однородности изложения мы внесли некоторые изменения в систему RWORD, касающиеся исключительно обозначений.
Синтаксис для определения сканера
Определение сканера имеет вид
BEGIN {(описание классов литер>} END
BEGIN {(описание символов)} END
FINI
Эта форма очень похожа на форму, которая использовалась в системе, описанной в разд. 3.4. Общность достигается главным образом за счет описаний символов и классов литер.
Описание классов литер
Класс можно описать двумя способами. Первый способ позволяет указать лишь литеры в коде EBCDIC, которые принадлежат классу. Второй используется тогда, когда хотят показать фактическое внутреннее представление литеры. Приведем два примера первого способа описания класса:
LET=/ABCDEFGHIJKLMNOPQRSTUVWXYZ/ и
PUNCTUATION=A.,;+ — =() $ /А
Первый пример говорит о том, что класс LET состоит из всех букв (латинского алфавита) во втором примере в класс PUNCTUATION (знаки пунктуации) включены литеры: . ,; +—— ( ) $ и /.
В обоих примерах первая отличная от пробела литера, которая следует за знаком играет роль разделителя для литер этого класса. Каждая следующая литера (включая пробел) вплоть до нового появления первой литеры принадлежит классу. В первом примере в качестве разделителя используется /; во втором — А. Ясно, что сам разделитель не может принадлежать классу. Этот прием не лишен остроумия.
Пример второго способа описания класса:
XX = $ 21,22,25 $
102
ГЛАВА 3
Пример показывает, что класс XX состоит из таких литер, внутреннее представление которых есть 21, 22 и 25. Использование разделителя $ всегда означает задание внутреннего представления литер. Таким образом, знак доллара нельзя использовать в качестве разделителя в первом способе.
Имя класса может быть любой последовательностью букв, кроме BEGIN, END и FINI. Представляет интерес класс литер со специальным именем IGNORE. Он состоит из всех литер, которые должны пропускаться или игнорироваться сканером, в том случае если они встречаются во входной информации. Этот класс соответствует классу IGnore из предыдущего раздела.
Описания символов
Любое описание класса символов имеет вид
<идентификатор> (<целое»= регулярное выражение> $
где регулярное выражение определяется, как в разд. 3.2. Переменными в выражении могут быть:
1)	любая литера в коде EBCDIC, кроме |, (,$ и пробела;
2)	пустая цепочка Л;
3)	имя класса литер;
4)	апостроф ', за которым следует любая литера в коде EBCDIC, кроме пробела. (Это механизм авторегистра, который позволяет использовать литеры, запрещенные в п. 1. За апострофом следует требуемая литера.)
Приведем три примера:
PUNCT (5)=PUNCTUATION $
BEG (3)="В Е G I N" $
ID (1)=LET {LET | DIG}5 $
Первый пример говорит о том, что символы в PUNCT, внутреннее представление которых 5, являются литерами из класса PUNCTUATION. Второй пример показывает, что символ 'BEGIN' имеет внутреннее представление 3, а третий — что символы, состоящие из буквы, за которой следуют от 0 до 5 букв или цифр, имеют внутреннее представление 1. Любой символ, который принадлежит специальному классу IGNORE, никогда не передается пользователю, а полностью игнорируется. Поскольку нет необходимости во внутреннем представлении, определение для такого класса имеет вид
IGNORE= регулярное выражение> $
Обратите внимание на различие между классом литер IGNORE и классом символов IGNORE. Предположим, что входная инфор
СКАНЕР
103
мация такова:
BEGIN IGNORE-// LET—/АВ/ END
BEGIN ID (l)-LET {LET}$ END FINI
Тогда входная цепочка “AB А” будет разобрана, как один идентификатор АВА, поскольку пробелы игнорируются. Предположим теперь, что входная информация задана по-другому:
BEGIN SPACE-// LET-/AB/END
BEGIN IGNORE—SPACER ID (l)-LET {LET} $ END FINI Тогда цепочка “AB А” будет разобрана, как два идентификатора АВ и А, так как пробел — символ, который в действительности будет сконструирован, но не будет передан пользователю.
Осталось обсудить возможность вызова подпрограммы, когда в процессе сканирования построен символ. Если мы описываем класс символов, используя формат (идентификатор) ((целое), (имя подпр.»= (регулярное выражение) $
то всякий раз, когда будет сформирован символ класса (идентификатор), произойдет вызов подпрограммы (имя подпр.). Эта подпрограмма позволяет выполнить вообще любую обработку. Например, подпрограмма, связанная с идентификаторами, будет выяснять, является идентификатор служебным словом или нет; это уже не делается автоматически, как в ранее описанной системе.
Во многих случаях разбор входной цепочки можно выполнить несколькими различными спосрбами. RWORD всегда пытается сопоставить символу самую длинную цепочку литер; так, цепочка Х123 будет идентификатором; она не будет разбита на идентификатор X, за которым следует целое 123. Есть случаи, когда система RWORD не может разрешить неоднозначность. Когда это происходит, система печатает сообщение и прекращает работу.
Примеры определений сканеров
Первый пример иллюстрирует, как определяется сканер, описанный в разд. 3.3:
BEGIN SPACE = //
LET = /ABCDEFGHIJKLMNOPQRSTUVWXYZ/
DIG = /0 12345678 9/
END
BEGIN ID (1, CHECKRES) = LET {LET | DIG} $
INT (2) = DIG {DIG} $
SSL (6) = /$ SPLUS(7)=+$ SMINUS (8)=$
SSTAR (9) = *$ SLPAR (№) = ($ SRPAR(11) = )$ SSLSL (12) = / /$ SSLST (13, COMMENT) = M IGNORE = SPACE $ END FINI
104
ГЛАВА 3
Предположим, что CHECKRES проверяет идентификатор на совпадение со служебными словами ABS, BEGIN или END и выполняет некоторые сопутствующие действия. Процедура COMMENT вызывает SCAN, используя другой сканер (он определяется ниже) для того, чтобы пропустить комментарий, и затем возвращает управление. Можно было иначе определить класс литер PUNC=A/+ —*() А и затем класс символов PUNCT (6)=PUNC $. Но тогда у всех разделителей было бы одинаковое внутреннее представление и пользователю пришлось бы самому разбираться в том, какой это разделитель.
Сканер для комментария определяется следующим образом:
BEGIN IGNCHAR = $00, 01, 02, 03, ... , АО, Al, ... , FF$ END BEGIN IGNORE = IGNCHAR $
NOEND (1)= *|/$
TERMIN (2)= */$
END FINI
Конструктор
Система RWORD интересна потому, что в ней непосредственно применяется теория конечных автоматов, изложенная в разд. 3.2. Мы не будем разбираться во всех деталях конструктора. В некотором смысле, конструктор — это тот же компилятор, который переводит входную информацию, содержащую описания классов литер и символов, в программу для распознавания символов.
Конструктор выполняет работу в два этапа, названных RWORD ЧАСТЬ I и RWORD ЧАСТЬ II. Общая блок-схема RWORD ЧАСТЬ I приведена на рис. 3.13 На первой фазе переводятся во
Рис. 3.13. Часть I системы RWORD.
внутреннюю форму и запоминаются классы литер, а затем осуществляется разбор регулярных выражений и строится НКА для каждого класса символов. На второй фазе объединяются все НКА и строится один детерминированный КА. Детерминированный КА выдается в виде программы, записанной на языке AED-0. Программа состоит из таблиц и обращений к подпрограммам (с соответствующими аргументами). На каждое состояние КА приходится одно обращение;
СКАНЕР
105
аргументы, по существу, описывают отображение данного состояния на другие в зависимости от литер во входной цепочке.
После компиляции и связывания с подпрограммами полученную программу можно выполнить, и она будет сканировать и строить символы определённого языка. Но она окажется неэффективной из-за обращений к многочисленным подпрограммам, а таблицы займут значительное место в памяти. Поэтому программа проходит дополнительную обработку в RWORD ЧАСТЬ II.
Если говорить коротко, RWORD ЧАСТЬ II необходима для того, чтобы получить эффективный сканер' как в отношении времени его работы, так и в отношении занимаемой памяти. Мы, однако, не будем останавливаться на том, какая оптимизация в данном случае применяется. Кроме того, на этом этапе можно связать вместе несколько сканеров (если они используются в одной и той же работе) и получить один большой объединенный сканер. Результатом работы RWORD ЧАСТЬ II являются таблицы в виде автокодных макровызовов, т. е. полученную программу необходимо еще транслировать, используя библиотеку предварительно написанных макроопределений. Это создает относительную независимость от машины, так как макроопределения в любое время можно переписать, чтобы получить таблицы для другой машины.
3.6.	ИСТОРИЧЕСКИЕ ЗАМЕЧАНИЯ
Само понятие устройства, с конечным числом состояний связано с именами Маккалока и Питтса [43]. Формализацию мы использовали аналогичную той, что предложена Муром^ [56]. Регулярные выражения и их связь с автоматом были описаны Клини [56]. Мы лишь слегка коснулись теории автоматов, поэтому заинтересованного читателя отсылаем к работам Гилла [62], Гинзбурга [62] и Хопкрофта и Улмана [69] х).
Рассмотренный в разд. 3.4 конструктор следует рассматривать как развитие конструктора, использованного в FSL (система построения компиляторов) Фелдманом [66]. Система AED RWORD, функционирующая с 1966 г. (см. Джонсон и др. [68]), является первой, в которой формальная теория конечных автоматов была применена к задаче лексического анализа.
г) Рекомендуем также монографии Глушкова [62] и Трахтенброта и Бард-зиня [70].— Прим. ред.
Глава 4
Нисходящие распознаватели
Обсуждение методов разбора для контекстно-свободных грамматик мы начнем со знакомства с нисходящими распознавателями или, как их иногда называют, прогнозирующими или целенаправленными распознавателями. В этих названиях нашел отражение способ работы таких распознавателей и способ построения синтаксического дерева. В разд. 4.1 рассматривается в самом общем виде метод, приводящий в результате к схеме, которая в принципе не эффективна из-за неизбежных и многочисленных возвратов. Затем приводится другой алгоритм, позволяющий сократить количество возвратов для некоторых грамматик. В разд. 4.2 говорится о проблемах, свойственных нисходящим методам, и рассматривается вопрос о том, как можно реорганизовать правила и как использовать другие способы записи правил, чтобы обойти трудности. Там же обсуждается представление грамматик в машине. Наконец, в разд. 4.3 говорится об использовании рекурсивных процедур при программировании нисходящего метода.
4.1.	НИСХОДЯЩИЙ РАЗБОР С ВОЗВРАТАМИ
Алгоритм нисходящего разбора строит синтаксическое дерево, начиная с корня, постепенно спускаясь до уровня предложения, как было показано на рис. 2.7 в разд. 2.5. Этот пример демонстрирует простоту и наглядность основной идеи. Описание усложняется главным образом из-за вспомогательных операций, которые необходимы для того, чтобы выполнять возвраты с твердой уверенностью, что все возможные попытки построения дерева были предприняты. Чтобы свести осложнения к минимуму, опишем этот алгоритм образно. Вообразим, что на любом этапе разбора, в каждом узле уже построенной части дерева, находится по одному человеку. Люди, находящиеся в терминальных узлах, занимают места, соответствующие символам предложения.
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
107
Некоему человеку предстоит провести разбор предложения х. Прежде всего ему необходимо отыскать вывод Z=>+x, где Z — начальный символ; следовательно, первым непосредственным выводом должен быть вывод Z=>y, где Z: :=у — правило. Пусть для Z существуют правила
Z : : = ХД2. . . Xn | YXY2. . . Ym | ZXZ2. . . Z.
Сначала человек пытается применить правило Z : : = ХхХ2. . . Хп. Если нельзя построить дерево, используя это правило, он делает попытку применить второе правило Z : :=YXY2. . . Ym. В случае неудачи он переходит к следующему правилу и т. д.
Как ему определить, правильно ли он выбрал непосредственный вывод Z => ХхХ2. . . Хп? Заметим, что если вывод правилен, то для некоторых цепочек хх будет иметь место х=ххх2. . . хп, где Xj=>*Xi для i = l,. . п. Прежде всего человек, выполняющий разбор, возьмет себе приемного сынах) Мх, который должен найти вывод Хх=>*хх для любого хх, такого, что х^=хх....Если сыну Мх уда-
лось найти такой вывод, он (и любой из его сыновей, внуков и т. д.) закрывает цепочку хх в предложении х и сообщает своему отцу об успехе. Тогда его отец усыновит М2, чтобы тот нашел вывод Х2=>*х2, где х=ххх2. . . , и ждет ответа от него и т. д. Как только сообщил об успехе сын М^, он усыновит еще и Мь чтобы тот нашел вывод X 1=>*хь Сообщение об успехе, пришедшее от сына Мп, означает, что разбор предложения закончен.
Как быть, если сыну Mt не удается найти вывод X 1=>*хр В этом случае Mi сообщает о неудаче своему отцу; тот от.него отрекается и дает старшему братуМь Mi;_x такое распоряжение: «Ты уже нашел вывод, но этот вывод неверен. Найди-ка мне другой». Если Mj„x сумеет найти другой вывод, он вновь сообщит об успехе, и все продолжится по-прежнему. Если же сообщит о неудаче, отец отречется и от него,* и тогда уже его старшего брата, Mj_2, попросят предпринять еще одну попытку. Если придется отречься даже отМь значит непосредственный вывод Z=>XxX2. . . Хп был неверен, и человек, начинавший разбор, попытается воспользоваться другим выводом Z=>YX. . . Ym.
Как же действует каждый из Положим, его целью является терминал ХР Входная цепочка имеет вид х=ххх2. . . Xi_xT. . ., где символы в хх, х2, . . . , xt_x уже закрыты другими людьми. Mj проверяет, совпадает ли очередной незакрытый символ Т с его целью Хь Если это так, он закрывает этот символ и сообщает об успехе. Если нет, сообщает о неудаче.
Если цель Mj — нетерминал X ь то Mi поступает точно так же, как и его отец. Он начинает проверять правые части правил, относящихся к нетерминалу, и, если необходимо, тоже усыновляет или
1} См. определения терминов в разд. 2.4.— Прим. ред.
108
ГЛАВА 4
отрекается от сыновей. Если все его сыновья сообщают об успехе, то Mi в свою очередь сообщает об успехе своему отцу. Если отец просит Mi найти другой вывод, а целью является терминальный символ, то Mj сообщает о неудаче, так как другого такого вывода не существует. В противном случае М} просит своего младшего сына найти другой вывод и реагирует на его ответ так же, как и раньше. Если все сыновья сообщат о неудаче, он сообщит о неудаче своему отцу.
Теперь уже вам, наверное, понятно, почему этот метод называется прогнозирующим или целенаправленным. Используется и название «нисходящий» из-за способа построения синтаксического
z
т I F
i + l * г
Рис. 4.1. Частичный нисходящий разбор предложения i+i*i.
дерева. При разборе отправляются от начального символа и нисходят к предложению (рис. 4.1).
Привлекательность этого метода (и его представления) в том и состоит, что каждый человек должен помнить лишь о своей цели, о своем отце, о своих сыновьях, а также о своем месте в грамматике и во входной цепочке. И никому не нужны точные сведения о том, что происходит в других местах. Это как раз и есть то, к чему мы вообще стремимся в программировании: в каждом сегменте программы или в подпрограмме необходимо заботиться о собственной входной и выходной информации и ни о чем более.
Для имитации усыновления и отречения от сыновей в программе используется стек типа LIFO (последний — в — первый — из), или, как его иногда называют, «магазин». Стек—основной механизм, используемый почти во всех типах распознавателей. Действительно, всякий раз, когда говорят о рекурсии, говорят о стеках. Стек — запоминающее устройство, в котором хранятся данные. Однако новые данные можно занести в стек только «сверху», проталкивая при этом вниз данные, уже находящиеся в нем. Соответственно можно ссылаться только на верхний или на несколько верхних элементов, и только их можно изменять. Если верхние элементы больше не нужны, их исключают, «поднимая» при этом вверх элементы, находившиеся ниже.
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
109
Пример стека из повседневной жизни — монетодержатель. Когда вкладывается новая монета, те, что уже были внутри, проталкиваются вниз; достать можно только верхнюю монету.
Обычно для реализации стека используются массив S и счетчик v. При v=0 стек пуст. При v=n, где п>0, в стеке находятся элементы S (1), S (2),. . ., S (п), где S (п) — верхний элемент стека.
Мы можем описать алгоритм в более явном виде, воспользовавшись нестандартным вариантом АЛГОЛа. Положим, во-первых, что грамматика задана списком в одномерном массиве GRAMMAR таким образом, что каждое множество правил U : :=х | у | . . * |z представлено как Ux|y|...|z|$. То есть каждый символ занимает одну ячейку, за каждой правой частью U следует вертикальная черта “I”, а за последней правой частью U следует Таким образом, грамматика
Z:: = E#
(4.1.1)	Е : : = Т+Е | Т
Т : :=F#T ( F
F : :=(Е) | i
будет выглядеть как
(4.1.2)	ZE# | $ET+E|T|$TF*T|F|$F (E)|i|$
Каждый элемент стека соответствует одному человеку и состоит из пяти компонент
(GOAL, i, FAT, SON, BRO)
которые означают следующее:
1.	GOAL — цель, т. е. символ, который человек ищет. Таким образом, в незакрытой в данный момент части предложения ему предстоит найти такую голову, которая приводится к GOAL, и закрыть ее. GOAL передается ему отцом.
2.	i — индекс в массиве GRAMMAR, указывающий на тот символ в правой части правила для GOAL, с которым человек работает в данный момент.
3.	FAT — имя отца (номер элемента стека, соответствующего отцу).
по
ГЛАВА 4
4.	SON — имя самого последнего (младшего) из сыновей.
5.	BRO — имя его старшего брата.
Нуль в любом из полей означает, что данная величина отсутствует. В программе значение переменной v равно количеству участвующих в разборе людей (количеству элементов в стеке в текущий момент), с — имя (номер элемента в стеке) человека, работающего в данный момент. Остальные ожидают конца его работы. Индекс j
СТЕК	ЦЕЛЬ	г	FAT	SON	BRO
Z	1	Z	4	0	15	0
1	1	г	2	Е	10	1	7	0
Е	*	3	Т	20	2	4	0
।—f	.	4	F	28	3	5	0
т 1 Е	5	i	0	4	0	0
1+1	6	+	0	2	0	3
F	Т	7	Е	12	2	8	6
1	Н—1	8	Т	18	7	12	0
i	F * Т	9	F	28	8	10	0
1	|	10	г	0	9	0	0
IF	11	*	0	8	0	9
I	12	Т	20	8	13	11
1	13	F	28	12	14	0
14	г	0	13	0	0
15	#	0	1	0	2
а.			Ь		
ZE#|$ET+E|T|$TF*T) F|$F(E) |i,|$
Рис. 4.2. Стек после нисходящего разбора i+i*i. а—синтаксическое дерево; стек после разбора.
относится к самому левому (незакрытому) символу входной цепочки INPUT (1), . . . , INPUT (n).
Если в программе встречаются обозначения GOAL, i, FAT, SON, BRO и нет других спецификаций, то считается, что они ссылаются на компоненты, относящиеся к тому человеку, который в данный момент работает (его имя с). Об этом следует помнить при чтении программы. Например, чтобы обратиться к полю GOAL элемента S (с), мы должны вместо GOAL написать S (с).GOAL. Такая сокращенная запись вводится, чтобы не загромождать программу.
Ранее упоминалось о том, что каждый человек должен хранить информацию о своих сыновьях. Можно хранить имена всех своих сыновей в собственном элементе стека, но в таком случае число полей элемента станет переменным. Мы поступим иначе и будем использовать поле SON для хранения ссылки на последнего (младшего) сына. Тогда поле BRO элемента, соответствующего этому сыну, укажет на его старшего брата и т. д.
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
111
В качестве иллюстрации рассмотрим изображенное на рис. 4.2, а синтаксическое дерево для предложения i+i*i грамматики (4.1.1). Состояние стека после окончания работы алгоритма разбора показано на рис. 4.2, &. Теперь у человека 2 (S (2)) есть цельЕ; предполагается, что он в соответствии с синтаксическим деревом использует правило Е : :=Т+Е. Таким образом, ему для того, чтобы найти символы Т, + и Е, потребуются три сына. Значение поля S (2) . SON=7, так что его младшим сыном является человек с номером 7, цель которого Е. Имя среднего сына — число 6, определяется значением поля S (7). BRO; цель этого сына — символ +. Имя старшего сына находится в поле BRO человека 6 и равно 3.
Вы видите, что у нас имеется список сыновей каждого человека и элементы этого списка в стеке «связаны» между собой. Использование таких связанных списков широко распространено в программировании, и читатель, который еще не вполне разобрался в этом примере, должен изучать его до тех пор, пока не почувствует, что понял все досконально. Должно стать ясным, что стек в его окончательном виде не что иное, как внутренняя форма синтаксического дерева.
Отметим, как в конце каждого предложения в соответствии с грамматикой (4.1.1) используется разделитель. По нему можно судить о том, что окончен разбор всего предложения, а не только какой-то из его голов.
Далее следует алгоритм нисходящего разбора. Он разбит на шесть частей для того, чтобы выделить разные функции. Там, где второстепенные детали могут затенить смысл алгоритма, мы даем некоторые пояснения.
НАЧАЛЬНАЯ УСТАНОВКА
S(l) :== (Z, 0, 0, 0, 0); с:=1;
v:=l;	1; GO ТО НОВЫЙ
ЧЕЛОВЕК
НОВЫЙ ЧЕЛОВЕК
IF GOAL терминал THEN
IF INPUT (j) = GOAL THEN BEGIN j:=j+l; GO TO УСПЕХ
EISE GO TO НЕУДАЧА i:= индекс в GRAMMAR правой части для GOAL;
GO TO ЦИКЛ
ЦИКЛ
IF GRAMMAR (i)=“|”
THEN IF FAT # 0
Первое усыновление. Цель усыновленного — начальный символ Z.
Новый человек изучает свою цель. Цель — терминал.
Если GOAL совпадает с символом из предложения, человек закрывает этот символ и сообщает об успехе.
Не совпадает — сообщает о неудаче. Цель нового человека — нетерминал. Подготовка к просмотру правых частей в правилах для GOAL.
Просмотр правой части.
112
ГЛАВА 4
THEN GO TO УСПЕХ
ELSE STOP — предложение;
IF GRAMMAR (i)=±=“F
THEN IF FAT #0
THEN GO TO НЕУДАЧА ELSE STOP —не предложение;
v:=v+ 1:
S(v) := (GRAMMAR (i), 0, c, 0, SON);
SON :=v; c:=v;
GO TO НОВЫЙ ЧЕЛОВЕК
УСПЕХ
c:—FAT;
i: =i+ 1; GO TO ЦИКЛ;
НЕУДАЧА
c: = FAT;
v: = v— 1;
SON := S (SON)-BRO;
GO TO ЕЩЕ РАЗ
ЕЩЕ РАЗ
IF SON = 0 THEN
BEGIN WHILE GRAMMAR
(i) + “I”
DO i:= i + 1;
i:= i+1; GO TO ЦИКЛ END;
i:— i — 1; c: = SON;
IF GOAL нетерминал
THEN GO TO ЕЩЕ РАЗ
j:= j —1;
GO TO НЕУДАЧА
Вы достигли конца правой части, поэтому сообщите об успехе. Если нет отца, то останов — окончен разбор предложения.
Нет больше правых частей, которые можно было бы попробовать, поэтому сообщите о неудаче или, если нет отца, остановитесь, не распознав предложения.
GRAMMAR (i) — другая цель, которую можно попытаться найти. Возьмите сына. Вы его отец, а его старший брат — тот, который до этого был вашим младшим сыном.
Переключить внимание на нового сына и ждать от него ответа.
Сообщить об успехе своему отцу; он предпримет следующий шаг.
Сообщить о неудаче своему отцу. Он от вас отречется и попросит вашего старшего брата предпринять еще одну попытку.
Есть у вас сын, который может предпринять еще одну попытку? Нет. Тогда пропустите эту правую часть — это не та, которая нужна — перейдите к следующей.
У вас есть сын. Попросите его повторить попытку. Его цель — нетерминал, так что он попытается еще раз добиться успеха.
Его цель — терминал. Попытка не приведет к успеху. Поэтому он открывает свой символ и сообщает о неудаче.
Несмотря на то что этот алгоритм может показаться удачным, в нем есть серьезный недостаток, смысл которого будет разъяснен в следующем разделе. Во-первых, давайте так модифицируем алгоритм, чтобы во время его работы было поменьше возвратов. Заметьте, что когда кто-либо сообщает о своей неудаче, мы отрека
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
113
емся от него и просим его старшего брата «попытаться еще раз», т. е. поискать другую подцепочку, которая приводится к желаемой цели. Поступая таким образом, мы показываем, как мало полагаемся мы на своего сына. Если бы прежде чем его усыновлять, мы делали тщательную проверку и указывали ему правильный путь, можно было бы доверять ему в гораздо большей степени. Будем теперь считать, что он сразу делает все верно. Тогда не придется просить его повторить попытку. Если кого-нибудь постигнет неудача, останется только предположить, что мы на ложном пути, и поискать совсем другую альтернативу (правую часть правила).
Для этого необходимо упорядочить правила, относящиеся к каждому нетерминалу, чтобы сразу находить верное правило. Такую операцию удается произвести с большинством языков программирования, но способа, позволяющего сделать это автоматически, нет. Можно помещать первой самую длинную из альтернатив. Грамматика (4.1.1) уже задана в такой форме. Таким образом, если у человека есть цель Е, он первым делом ищет Т+Е. Если есть Т, а символа + нет, он не просит своего первого сына найти другое Т, а отказывается от всех сыновей и переходит к следующему правилу Е : :=Т.
После таких изменений становится ненужной компонента стека BRO, зато приходится внимательнее следить за тем, какая часть цепочки уже закрыта. Теперь элемент стека выглядит так:
(GOAL, i, FAT, SON, j)
где GOAL, i, FAT и SON имеют прежний смысл, a j содержит индекс, указывающий то место во входной цепочке INPUT, которое мы в данный момент йзучаем или о котором ждем сообщения. Поскольку вся задача в целом стала проще, мы можем, находясь в данном месте, сами проверить терминальные символы в правых частях; при этом отпадает необходимость в усыновлении человека, которому пришлось бы выполнять это задание.
Еще раз заметим, что алгоритм работает правильно только при условии, что правила упорядочены так, что сразу же делается попытка применить верное правило.
НАЧАЛЬНАЯ УСТАНОВКА
S(l) := (Z, 0, 0, 0, 1); с:= 1; v:= 1; GO ТО НОВЫЙ ЧЕЛОВЕК
НОВЫЙ ЧЕЛОВЕК
i:= индекс, указывающий на первую правую часть для GOAL в грамматике;
GO ТО ЦИКЛ
Поставить перед новым человеком цель Z и начать с первого символа.
Усыновление происходит лишь в случае нетерминала. Сын находит относящиеся к нему правила и начинает работать.
114
ГЛАВА 4
ЦИКЛ
IF GRAMMAR (i) = „|“
THEN IF FAT # 0
THEN GO TO УСПЕХ ELSE STOP — предложение;
IF GRAMMAR =
THEN IF FAT 0
THEN GO TO НЕУДАЧА
ELSE STOP — не предложение
IF GRAMMAR (i) терминал
THEN IF INPUT(j) = GRAMMAR (i) THEN
BEGIN ]:==] +1;
GO TO ЦИКЛ
END
ELSE GO TO РАССМОТРИ АЛЬТЕРНАТИВУ
v:= v + 1;
S (v) : = (GRAMMAR (i), 0, c, 0, j);
SON := v; c:=v;
GO TO НОВЫЙ ЧЕЛОВЕК
УСПЕХ
c:—FAT; j:=S(SON)*j;
i:=i+1; j:=j + l;
GO TO ЦИКЛ
НЕУДАЧА
c:—FAT;
v:=c; SON:=0;
GO TO РАССМОТРИ АЛЬТЕРНАТИВУ
РАССМОТРИ АЛЬТЕРНАТИВУ
IF FAT ф 0
THEN j: =S(FAT).j ELSE j : = 1;
WHILE i:=i + l;
i:	= i+1; GO TO ЦИКЛ
Просмотр правой части.
Вы достигли ее конца, так что сообщите об успехе, или, если отца нет, закончите работу; предложение распознано.
Нет больше правых частей, которые можно было бы рассмотреть, так что сообщите о неудаче, или, если отца нет, остановитесь.
Предложение не распознано.
Если терминал подходит, мы закрываем его и переходим к следующему символу.
Терминал не подходит, поэтому перейдите к правой части.
Возьмите еще одного сына, укажите ему цель — нетерминал.
Его отец — вы. Переключитесь на него и ожидайте от него сообщения.
Сообщите отцу, какая часть цепочки закрыта. Тогда он продолжит свою работу.
Сообщите о неудаче своему отцу.
Он отречется от вас и от всех остальных сыновей, а после этого рассмотрит другую альтернативу.
Узнайте у отца, с какого места продолжить обработку.
Найдите начало следующей альтернативы, если такая есть, и начните все снова.
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
115
4.2.	ПРОБЛЕМЫ НИСХОДЯЩЕГО РАЗБОРА И ИХ РЕШЕНИЕ
Прямая левосторонняя рекурсия
В алгоритме, описанном в разд. 4.1, есть серьезный недостаток, который проявляется, когда цель определена с использованием левосторонней рекурсии. Если X — наша цель, а первое же правило для X имеет вид X : :=Х. . . , то мы незамедлительно усыновляем того, кто будет искать X. Он в свою очередь немедленно «заведет» себе сына, чтобы тот искал X. Таким образом, каждый будет сваливать на своего сына ответственность, и для решения этой задачи не хватит всего населения Китая.
По этой причине правила грамматики (4.1.1) написаны с применением правосторонней рекурсии вместо более привычной левосторонней. Лучший способ избавиться от прямой левосторонней рекурсии — записывать правила, используя итеративные и факультативные обозначения, которые обсуждались в разд. 2.9. Запишем правила
(4.2.1)	Е : :=Е+Т | Т как Е : :=Т {+Т}
и
Т: :=T*F|T/F|F как Т: :=F { *F|/F}
Сейчас будут сформулированы два принципа, на основании которых правила языка, включающие прямую левостороннюю рекурсию, преобразуются в эквивалентные правила, использующие итерацию.
(4.2.2)	Факторизация. Если существуют правила вида U : :=ху | xw | . . . | xz, то их надо заменить на U: :=x(y|w|. . . |z), где скобки являются метасимволами.
Допустима факторизация и в более общей форме, такая, как в арифметических выражениях. Например, если в (4.2.2) y=yiy2 и w=y!W2, мы могли бы заменить U : :=х (y|w|. . . |z) на
U : :=х (у! (y2|w2)|. . . |z).
Заметьте, что исходные правила U : :=х | ху мы преобразуем к виду U ::=х (у|А), где Л — пустая цепочка. Когда бы ни использовалось подобное преобразование, Л всегда помещается как последняя альтернатива, так как мы принимаем условие, что если цель — Л, то эта цель всегда сопоставляется.
Помимо того что факторизация позволяет нам исключить прямую рекурсию, использование этого приема сокращает размеры грамматики и позволяет проводить разбор более эффективно. В этом мы убедимся позже.
После факторизации (4.2.2) в грамматике останется не более одной правой части с прямой левосторонней рекурсией для каждого
116
ГЛАВА 4
из нетерминалов. Если такая правая часть есть, мы делаем следующее:
(4.2.3)	Пусть U ::=х|у|. . . |z]Uv — правила, у которых осталась леворекурсивная правая часть. Эти правила означают, что членом синтаксического класса U является х, у или z, за которыми либо ничего не следует, либо следует сколько-то v. Тогда преобразуем эти правила к виду U :: = (х|у| . . . |z) {v}.
Мы использовали (4.2.3), чтобы сделать преобразование в (4.2.1), позволяющее избавиться от ненужных скобок, заключающих Т. В качестве другого примера преобразуем A ::=BC|BCD|Axz|Axy.
z
Ь
Рис. 4.3. Деревья, использующие рекурсию и итерацию.
Применив правило (4.2.2), получим А ::=ВС (D|A)|Ax (z|y); применив (4.2.3), получим А :: = (ВС (D|A)){x(z|y)}. Можно избавиться от одной пары скобок, после чего получим A ::=BC(D|A){x(z|y)}.
После таких изменений мы, конечно, должны изменить и наш алгоритм нисходящего разбора. Теперь алгоритм должен уметь обрабатывать альтернативы не только во всей правой части, но и в ее подцепочках, должен учитывать в своей работе существование пустой цепочки А, должен уметь обрабатывать итерацию. Внесение этих изменений в алгоритм предоставляется читателю.
Использование итерации вместо рекурсии отчасти меняет и структуру деревьев. Таким образом, рис. 4.3, а должен был бы походить на рис. 4.3, Ь. Мы утверждаем, что эти два дерева следует рассматривать как эквивалентные; операторы «плюс» должны выполняться слева направо.
Общая левосторонняя рекурсия
Мы не решили всей проблемы левосторонней рекурсии: с прямой левосторонней рекурсией покончено, но общая левосторонняя рекурсия еще осталась. Таким образом, правила
U ::=Vx и V ::=Uy|v
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
117
дают вывод U=>+Uyx. Избавиться от этого не так просто, но обнаружить такую ситуацию можно. Исключим из исходной грамматики все правила с прямой левосторонней рекурсией. Символ U, получившейся в результате этих преобразований грамматики, может быть леворекурсивным тогда и только тогда, когда U FIRST4" U. Как проверить это отношение, нам уже известно.
Представление грамматики в оперативной памяти
Одной из проблем, возникающих при реализации нисходящих методов, является представление грамматики в вычислительной машине. Одно из возможных представлений мы уже использовали
Рис. 4.4. Синтаксический граф для грамматики (4.2.4).
в разд. 4.1. Очевидно, что оно неудачно из-за объема работы, необходимой для поиска правил, соответствующих каждому нетерминалу. Речь пойдет о другом представлении. Прежде чем начать изложение, мы только упомянем о том, что написать конструктор, который воспринимает грамматику, проводит любые из преобразований, о которых только что говорилось, проверяет, не являются ли правила леворекурсивными, и составляет таблицы для грамма
118
ГЛАВА 4
тики в одной из описываемых далее форм — довольно легко, и мы опять предоставляем это читателю.
Для представления грамматики используется списочная структура, называемая синтаксическим графом. Каждый узел представляет символ S из правой части и состоит из четырех компонент: ИМЯ, ОПРЕДЕЛЕНИЕ (ОПР), АЛЬТЕРНАТИВА (АЛТ) и ПРЕЕМНИК (ПРЕМ), где
1.	ИМЯ — это сам символ S в некоторой внутренней форме.
2.	ОПРЕДЕЛЕНИЕ равно 0, если S — терминал; в противном случае эта компонента указывает на узел, соответствующий первому символу в первой правой части для S.
3.	АЛЬТЕРНАТИВА указывает на первый символ той альтернативы правой части, которая следует за правой частью, содержащей данный узел (0, если такой правой части нет). Это только для первых символов в правых частях.
4.	ПРЕЕМНИК указывает на следующий символ правой части (О, если такого символа нет).
Кроме того, каждый нетерминальный символ представлен узлом, состоящим из одной компоненты, которая указывает на первый символ в первой правой части, относящейся к этому символу. Примером может служить рис. 4.4, на котором изображен синтаксический граф грамматики (4.2.4). Компоненты каждого узла располагаются так:
ИМЯ		
ОПР	АЛТ	ПРЕМ
(4.2.4)	Е ::=Е<аоп>Т| Т <аоп> : :=+ | — Т ::~T<Mon>F|F <моп> : :=* |/ F ::=(E)|i
Синтаксический граф — удобная форма записи для проведения преобразований (4.2.2) и (4.2.3). На рис. 4.5 изображен модифицированный синтаксический граф грамматики (4.2.4). Для указания на итерацию в узлах имеются дополнительные символы. ST в узле означает, что данный символ начинает цепочку, заключенную в фигурные скобки, в то время как звездочка указывает на то, что символ оканчивает такую строку.
Будьте здесь внимательны. В зависимости от того, как работает анализатор, для правой части вида {{X, Y}Z} может получиться так, что в узел X нужно занести ST2, чтобы указать, что X начинает две вложенные друг в друга итерации.
Мы также можем отождествлять между собой узлы графа с идентичными компонентами. Например, на рис. 4.4 два узла с именем F
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
119
одинаковы. Если алгоритм продолжает работать и после того, как мы провели отождествление, исходная грамматика все более утрачивает свой первоначальный вид, и поэтому по семантическим соображениям (которые будут понятны гораздо позже) эта идея неудачна.
Рис. 4.5. Модифицированный синтаксический граф.
Если программирование ведется на языке, не допускающем указательных переменных, можно пользоваться одномерными массивами; указателям будут соответствовать индексы массива.
Разбор без возвратов
Программа разбора в компиляторе ни в коем случае не должна прибегать к возвратам. Мы должны иметь уверенность в том, что каждая предполагаемая цель верна. Это необходимо потому, что
120
ГЛАВА 4
нам предстоит связать семантику с синтаксисом, и по мере того, как мы будем прогнозировать и находить цели, эти символы будут обрабатываться семантически. Вот некоторые примеры «обработки»: 1) при обработке описаний переменных идентификаторы помещаются в таблицу символов; 2) при обработке арифметических выражений проверяют, совместимы ли типы операндов.
Если возврат произошел из-за того, что прогнозируемая цель неверна, придется уничтожить результаты семантической обработки, проделанной во время поисков этой цели. Сделать это не так-то просто, поэтому постараемся провести грамматический разбор без возвратов.
Для того чтобы избавиться от возвратов, в компиляторах в качестве контекста обычно используется следующий «незакрытый» символ исходной программы. Тогда на грамматику налагается следующее требование: если есть альтернативы х | у | . . . |z, то множества символов, которыми могут начинаться выводимые из х, у, . . . , z слова, должны быть попарно различны. То есть если xz>*Au и y=>*Bv, то А=#В. Если это требование выполнено, можно довольно просто определить, какая из альтернатив х, у или z — наша цель. Заметим, что факторизация оказывает здесь большую помощь. Если есть правило U : :=ху | xz, то преобразование этого правила к виду U :~х (у | z) помогает сделать множества первых символов для разных альтернатив непересекающимися.
Здесь не приводится более подробного описания этого метода, хотя в следующем разделе мы вновь встретимся с его применением.
4.3.	РЕКУРСИВНЫЙ СПУСК
В некоторых компиляторах синтаксический анализатор содержит по одной рекурсивной процедуре для каждого нетерминала U. Каждая такая процедура осуществляет разбор фраз, выводимых из U. Процедуре сообщается, с какого места данной программы следует начинать поиск фразы, выводимой из U. Следовательно, такая процедура — целенаправленная, или прогнозирующая. Мы предполагаем, что сможем найти такую фразу. Процедура ищет эту фразу, сравнивая программу в указанном месте с правыми частями правил для U и вызывая по мере необходимости другие процедуры для распознавания промежуточных целей.
В действительности во время этого разбора дерево строится точно так же, как в алгоритме разбора, описанном в разд. 4.1 (без возвратов). Отличается только форма записи самого алгоритма.
Чтобы проиллюстрировать этот способ, запишем процедуры для
НИСХОДЯ ЩИЕ РАСПОЗНАВАТЕЛИ
121
нетерминальных символов такой грамматики:
<инстр>
(4.3.1)
<перем>
<выр>
<терм>
<множ>
<перем>: = <выр>
| IF <выр> THEN <инстр>
| IF <выр> THEN <инстр> ELSE <инстр> i |i «выр»
<терм> | <выр> 4- <терм>
<множ> | <терм> * <множ>
<перем> | «выр»
Чтобы удобнее было работать, перепишем грамматику так:
<	инстр>:: = <перем>: = <выр>
| IF <выр> THEN <инстр> [ELSE <инстр>] (4.3.2) <перем>:: =i [«выр»]
<	выр> ::=<терм> { + <терм»
<	терм> :: =<множ> {*<множ>}
<	множ> :: =<перем> | «выр»
Мы полагаем, что сможем провести разбор без возвратов. Для того чтобы возвратов не было, в качестве контекста используется единственный символ, следующий за уже разобранной частью фразы. Мы записываем процедуры на нестандартном АЛГОЛе с соблюдением следующих условий:
1.	Глобальная переменная NXTSYMB всегда содержит тот символ исходной программы, который будет обрабатываться следующим. При вызове процедуры для поиска новой цели первый символ, который она должна исследовать, уже находится в NXTSYMB.
2.	Подобно этому, перед тем как выйти из процедуры с сообщением об успехе, символ, следующий за уже разработанной подцепочкой, помещается в NXTSYMB.
3.	Процедура SCAN готовит очередной символ исходной программы и помещает его в NXTSYMB.
4.	Программа ERROR вызывается в тех случаях, когда обнаружена ошибка. Она печатает сообщение и передает управление обратно. После возврата мы продолжим работу так, как будто бы никакой ошибки не было (см. соображения, следующие за описанием процедур).
5.	Для того чтобы начать синтаксический анализ инструкции, мы обращаемся к программе SCAN, которая поместит первый символ в NXTSYMB, а затем вызываем процедуру STATE.
122
ГЛАВА 4
PROCEDURE STATE;
IF NXTSYMB = “IF”
THEN
BEGIN SCAN; EXPR;
IF NXTSYMB # “THEN” THEN ERROR ELSE
BEGIN SCAN; STATE;
IF NXTSYMB / “ELSE” THEN
BEGIN SCAN;
STATE END
END
END
ELSE
BEGIN VAR;
IF NXTSYMB / THEN ERROR ELSE BEGIN SCAN; EXPR END
END;
PROCEDURE VAR;
IF NXTSYMB #“i”THEN ERROR
ELSE BEGIN SCAN;
IF NXTSYMB = “(” THEN BEGIN SCAN; EXPR;
IF NXTSYMB “)”
THEN ERROR
ELSE SCAN END
END;
Подпрограмма для <инстр>. Мы обычно полагаем, что можно определить вид инструкции по первому символу-
Взять первый символ выражения и вызвать процедуру для обработки вы. ражения. Следующим символом должен быть «THEN», затем — инструкция. Рекурсивный вызов. Если встретится символ «ELSE», произвести его анализ.
Это не условная инструкция. Должна быть инструкция присваивания. Перейти к разбору переменной, проверить, есть ли символ : = , и затем анализировать выражение.
Конец процедуры <инстр>.
Подпрограмма для <перем>.
Переменная должна начинаться с i.
Взять символ, следующий за 1. В том и только в том случае, когда это открывающая скобка, перейти к разбору выражения и проверить, следует ли за выражением закрывающая скобка. Занести в NXTSYMB символ, следующий за обработанной конструкцией.
PROCEDURE EXPR;
BEGIN TERM;
WHILE NXTSYMB = “+” DO
BEGIN SCAN ;TERM END
END
Подпрограмма для <выр>.
Выражение должно начинаться термом. После этого ожидается любое количество конструкций вида «+<терм>». Возврат, если следующий символ не «+».
НИСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
123
PROCEDURE TERM;
Подпрограмма для <терм>.
BEGIN FACTOR;
WHILE NXTSYMB—DO BEGIN SCAN; FACTOR END
END
PROCEDURE FACTOR;
IF NXTSYMB = “(” THEN BEGIN SCAN; EXPR;
IF NXTSYMB “)”
THEN ERROR;
ELSE SCAN END
ELSE VAR;
Процедура аналогична процедуре для <выр> и не требует объяснений.
Подпрограмма для <множ>.
По первому символу выбирается альтернатива. Это выражение, заключенное в скобки. Проверить наличие закрывающей скобки и сканировать следующий символ.
Это переменная.
Во всех этих процедурах необычно то, что они не требуют локальных переменных. Фактически единственная используемая переменная— это NXTSYMB. По существу, мы просто используем обычный стековый механизм, связывающий процедуры во время счета для того, чтобы имитировать стек, используемый в разд. 4.1.
Преимущества этого метода совершенно очевидны. Программируя, можно реорганизовать правила так, чтобы они согласовывались с процедурами. Предполагается, что автор компилятора настолько хорошо знаком с исходным языком, что может произвести реорганизацию, которая избавляет от возвратов. Метод сохраняет свою гибкость и по отношению к семантической обработке. С этой целью в любое место процедуры можно включить группу команд, не откладывая семантическую обработку до тех пор, пока будет обнаружена вся фраза. Мы познакомимся с этим подробнее, когда речь пойдет о семантике.
Основной недостаток состоит в том, что на программирование и отладку затрачивается больше усилий, чем в частично автоматизированных системах. Тем не менее это разумный метод и используется он во многих компиляторах.
Большинство компиляторов после обнаружения ошибки просто исключают весь путь вплоть до уровня инструкции. На этом уровне пропускается участок исходной программы до ближайшего символа BEGIN, END или до точки с запятой и лишь затем возобновляется обработка. Такой подход, конечно, довольно примитивен. К этой теме мы еще вернемся в гл. 15.
Несложные размышления, несомненно, убедят читателя в том, что можно написать такую программу, которая воспринимала бы правила подходящей грамматики и порождала бы рекурсивные
124
ГЛАВА 4
процедуры (написанные на каком-нибудь языке) для этой грамматики. Конечно, на грамматику пришлось бы наложить некоторые ограничения.
УПРАЖНЕНИЯ К РАЗДЕЛУ 4.3
1. Выполните вручную написанные выше процедуры для проведения синтаксического анализа следующих инструкций:
a) i: = i; b) i:=i (i); c) IF i THEN i:~i. Начните с выполнения инструкций SCAN; STATE.
4.4. ИСТОРИЧЕСКИЕ ЗАМЕЧАНИЯ
Одна из первых статей, в которых рассматривался фиксированный алгоритм разбора с описывающими язык таблицами, принадлежит Айронсу [61а]. Его метод — это не нисходящий разбор в чистом виде, как описано здесь, а некоторое смешение нисходящих и восходящих методов. Второй метод будет описан дальше. Куно и Уттингер использовали нисходящий способ в исследованиях, касающихся естественных языков. Алгоритм, приведенный в разд. 4.1, взят из обзора Флойда [64Ь]. Способ изложения принципа нисходящего разбора на примере людей позаимствован из этой же работы.
Обстоятельный доклад об использовании прогнозирующих методов при компилировании был сделан на весенней конференции (SJCC) Читэмом и Сэттли [64]. Гро [61] также описывает компилятор, в котором используются рекурсивные процедуры.
Способы, позволяющие избавиться от возвратов, описаны Унгером [68], в то время как Льюис и Стирнз [68] и Розенкранц и Стирнз [69] исследовали LL (к) грамматики и языки, в которых для проведения нисходящего разбора без возвратов достаточно на каждом шаге анализировать все уже обработанные символы и еще к символов справа.
В языке символьной обработки COGENT (Рейнольдс [65]) используется нисходящая схема разбора, причем разбор альтернатив производится параллельно и всякий раз исключаются те из них, которые оказались полностью неподходящими. Таким образом, можно обработать неоднозначные языки; при этом для неоднозначного предложения порождаются все возможные варианты разбора. Нисходящий разбор — излюбленный метод в системе написания компиляторов МЕТА (см. Шорр [64]) и используется в самом старом компиляторе компиляторов (Брукер и др. [63]). Его описание приводится в работе Розена [64]. Среди компиляторов, в которых используется рекурсивный спуск,— компилятор с расширенного языка АЛГОЛ фирмы Burroughs и компилятор с АЛГОЛа для машины 7090, разработанный группой SHARE.11 *
См. также статью Вирта [71].— Прим, ред.
Глава 5
Грамматики простого предшествования
5.1.	ОТНОШЕНИЯ ПРЕДШЕСТВОВАНИЯ
И ИХ ИСПОЛЬЗОВАНИЕ
В этой главе описывается схема разбора, в которой применяется восходящий метод. Напомним, что при использовании этого метода в текущей сентенциальной форме повторяется поиск основы (самой левой простой фразы и), которая в соответствии с правилом U : :=и приводится к нетерминалу U. При применении любого из методов нисходящего разбора возникает вопрос — как найти основу и выяснить, к какому нетерминалу нужно ее приводить. В настоящей главе этот вопрос решается для определенного класса грамматик, называемых грамматиками (простого) предшествования. Для всех грамматик это, конечно, проблемы не решает. Поскольку мы будем двигаться слева направо и снизу вверх, то все сентенциальные формы и весь вывод будут каноническими. В этой главе термин «сентенциальная форма» означает «каноническая сентенциальная форма».
Как же найти основу, если задана сентенциальная форма х? Хотелось бы, двигаясь слева направо и рассматривая только два соседних символа одновременно, суметь определить, нашли ли мы хвост основы. А затем, продвигаясь назад к левому концу сентенциальной формы, найти голову основы, опять-таки принимая каждый раз решение только по двум соседним символам. То есть мы сталкиваемся с такой проблемой: если задана цепочка . . .RS. . . , то всегда ли R является хвостом основы, или RS вместе могут встретиться в основе, или возможны другие варианты? Хотелось бы, не приступая еще к разбору, исследовать грамматику и принять решение относительно каждой пары символов R и S.
Рассмотрим теперь два символа R и S из словаря V грамматики G. Предположим, что существует (каноническая) сентенциальная форма . . .RS. ... На некотором этапе канонического разбора либо R, либо S (или оба символа одновременно) должны войти в основу. При этом возникают следующие три возможности:
1.	R — часть основы, a S нет (рис. 5.1, а). Эту ситуацию мы записываем как R *>S и говорим, что R больше S или что R предшествует S, поскольку символ R будет редуцирован раньше, чем S.
126
ГЛАВА 5
Заметим, что R должен быть последним (хвостовым) символом в правой части некоторого правила U . . .R. Заметим еще, что, поскольку основа находится слева otS, S должен быть терминалом.
2.	Оба символа R и S входят в основу (рис. 5.1, Ь). Запишем это как R=LS. У них одинаковое значение предшествования, и они
Рис. 5.1. Примеры отношений предшествования.
должны редуцироваться одновременно. Очевидно, в грамматике должно быть правило U . . .RS. . . .
3.	S — часть основы, a R нет (рис. 5.1, с). Отношение между ними записывается как R<£S; можно говорить, что R меньше, чем S. Символ S должен быть первым (головным) в правой части некоторого правила U ::=S. . . .
Если (канонической) сентенциальной формы . . .RS. . . не существует, мы считаем, что между упорядоченной парой символов (R, S) не определено никакое отношение. Заметим, что ни одно из трех определенных выше отношений предшествования <•,= и •> не является симметричным. Например, из R<»S вовсе не следует S<R
Рассмотрим в качестве примера грамматику G5 (Z)
(5.1.1)	Z :: =ЬМЬ
М :: =(L|a L=Ма)
Языку L (G5) принадлежат такие цепочки, как bab, b (аа)Ь, b ((аа)а) b и b (((аа)а)а)Ь. В каждом столбце приведенной ниже таблицы показана сентенциальная форма, ее синтаксическое дерево, основа дерева и отношения, которые можно из него получить.
На рис. 5.2 представлена матрица предшествования для грамматики G5 — матрица, в которой указываются все отношения предшествования. Элемент этой матрицы В [i, j] содержит отношение между парой символов (Sb Sj). Пустой элемент матрицы свидетельствует о том, что между соответствующими двумя символами отношение предшествования не определено.
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
127
b ( М а ) Ь
Сентенциальная b а Ь
форма,: Синтаксическое	Z
дерево:	।-----1----1
b М	b
I а
b ( L b
1—I—-I М а )
Основа!.	а
Отношения»	b <• а
определяемые	а •> Ь
деревом:
( U
Ь	<•	(
(	=	L
L	•>	Ь
М ) а
( <• М М ± а а = ) ) •> b
Z .—I—, ь м ь
На первый взгляд может показаться, что трудно найти все отношения предшествования между символами грамматики. Складывается впечатление, что придется просмотреть довольно много синтаксических деревьев, чтобы найти все отношения. Да и как гарантировать, что все отношения найдены? В следующем разделе мы переопределим отношения так, что их можно будет без особого труда построить для любой грамматики.
Рис. 5.2. Матрица предшествования для грамматики G5.
Как же воспользоваться отношениями предшествования при разборе предложений? Если между какой-либо парой символов (R, S) определено более чем одно отношение, они бесполезны. Если же между любой парой символов определено не более одного отношения, отношения предшествования позволят найти основу любой сентенциальной формы. Тогда можно сказать, что основой любой сентенциальной формы Si . . . Sn является самая левая подцепочка Sj . . . Sb такая, что
Sj-t^Sj
(5.1.2)	S3iSJ+1iSJ+2=r. .
Si Л> SU1
128
ГЛАВА 5
(Чтобы учесть и тот случай, когда символ Sj (Si) основы является самым первым (последним) символом сентенциальной формы, последнее утверждение придется слегка изменить; мы это сделаем позже.) Из определения отношений с очевидностью вытекает, что основа Sj ... Sj удовлетворяет (5.1.2). Не столь очевидным пред-
Шаг	Сентенциальная форма	Основа	Привести основу к	Построенный непосредственный вывод
1 b	( а а ) b	а	М	b (Ma) b b (aa) b
2 b	( М а ) b	М а)	L	b (Lb b (Ma) b
3 b	( L b	(L	М	bMb => b (Lb
4 b	М b	ьмь	Z	Zz>bMb
Рис. 5.3. Разбор сентенциальной формы b (аа) Ь.
ставляется тот факт, что самая левая подцепочка, удовлетворяющая (5.1.2), является основой. Это мы докажем в следующем разделе. А пока примем это на веру и завершим раздел примером канонического разбора предложения b (аа)Ь грамматики G5 с использованием матрицы предшествования, изображенной на рис. 5.2. Рисунок 5.3 иллюстрирует разбор. На каждом шаге показаны сентенциальная форма и отношения, определенные между символами в соответствии с рис. 5.2.
УПРАЖНЕНИЯ К РАЗДЕЛУ 5.1
1.	По заданным синтаксическим деревьям определите все отношения предшествования. (После того как определение отношений
<блок>
BEGIN /список V ^описании/
<01шсание>
у список хинстр^кции/ <инструкция>
।-----------1	I--------1
REAL / список	.(переменная):
Хибеишифцкаторов^
по данной основе будет закончено, отсеките ее от дерева; получится другое дерево. Затем определите отношения, исходя из нового дерева.)
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
129
2.	Используя для нахождения основ матрицу предшествования, заданную на рис. 5.2, проведите разбор следующих предложений грамматики G5: b a b, b (aa)b, b (((а а) а)а) Ь.
3.	Попытайтесь найти все отношения предшествования для грамматики (4.1.1).
5.2. ОПРЕДЕЛЕНИЕ И ПОСТРОЕНИЕ ОТНОШЕНИЙ
В разд. 5.1 мы определили три отношения предшествования в терминах синтаксических деревьев сентенциальных форм. Теперь мы хотим определить их заново, основываясь только на правилах грамматики, так, чтобы можно было построить отношения предшествования и доказать утверждение из предыдущего раздела. Эти определения выглядят довольно громоздкими, но для доказательства интересующих нас положений они необходимы.
(5.2.1).	Определение. Если задана грамматика G, то отношения предшествования между символами из словаря V определяются следующим образом:
1.	RjlS тогда и только тогда, когда в G есть правило U: :=. . .RS. . . .
2.	R<«S тогда и только тогда, когда существует правило U: :=. . .RV. . . , такое, что справедливо отношение V FIRST+ S [см. (2.6.4)1.
3.	R »>S тогда и только тогда, когда S — терминал и существует правило U:	. .VW. . . , такое, что справедливы соотноше-
ния V LAST+ R и W FIRST* S [см. (2.6.7) и (2.6.4)].
Далее нам потребуются еще два отношения:
R<«=S тогда и только тогда, когда R^S или R<»S.
R^=S тогда и только тогда, когда R=S или R«>S.
Мы должны доказать, что отношения, определенные таким необычным путем, эквивалентны тем, более понятным, отношениям, которые описаны в разд. 5.1. Покажем это для отношений и •>, а аналогичное доказательство для отношения <• предоставим читателю (упражнение 1).
(5.2.2).	Теорема. R^S тогда и только тогда, когда основа некоторой сентенциальной формы содержит подцепочку RS.
Доказательство. Пусть R=S. Мы должны построить такое синтаксическое дерево, чтобы цепочка RS содержалась в основе. По определению, для некоторых и и v существует правило U: :=uRSv. Так как каждое правило используется в выводе по меньшей мере одного предложения (предполагается, что грамматика
5 д. Грис
130
ГЛАВА S
приведенная), имеем Z=>* хПудля некоторых х и у, где Z — начальный символ. Строим искомое синтаксическое дерево в три этапа:
1.	Строим дерево вывода xUy.
2.	Редуцируем дерево, построенное на 1-м этапе (отсекаем основы), до тех пор, пока U не станет частью основы. Это возможно, потому что каждый символ в некоторый момент является частью основы.
3.	Добавляем к дереву, полученному в результате выполнения 2-го этапа, еще один куст для правила U: :=uRSv. Так как символ U входил в основу, новой основой станет uRSv, т. е. дерево построено.
Обратно, если RS — часть основы, то по определению основы должно существовать правило U: : = . . .RS. . . .
(5.2.3).	Теорема. R«>S тогда и только тогда, когда существует каноническая сентенциальная форма . . .RS. . . , где R — последний символ основы.
Доказательство. Предположим, что R*>S. Покажем, как построить синтаксическое дерево, обладающее требуемым свойством. Во-первых, так как R«>S, то имеется правило U: :~uVWv, где V LAST* R и W FIRST* S. Что это означает? Так как грамматика приведенная, мы знаем, что Z=>«xUy=>xuVWvy (Z — начальный символ). Во-вторых, отношение V LAST+ R означает, что V z>+ . . .R, а из этого следует, что
(5.2.4)	Z =>+ xuVWvy =>+ xu. . .RWvy.
Строим искомое дерево следующим образом:
1.	Рисуем синтаксическое дерево вывода (5.2.4).
2.	Последовательно производим в основе ряд редукций до тех пор, пока R не станет частью основы. Заметим, что, так как V =>+ . . .R, символ R должен быть в основе последним.
3.	Из определения W FIRST* S нам известно, что W =>#S. . . Добавим к синтаксическому дереву, полученному после 2-го этапа, кусты, соответствующие этому выводу. Очевидно, что основа при этом не меняется, так как она содержит R, а кусты добавляются справа от R. Таким образом, символ R — хвост основы и за ним следует S.
Обратно, предположим, что есть такое дерево, в котором R — хвост основы и за R следует S. Во-первых, проведем ряд редукций канонического разбора до тех пор, пока символ S не окажется в основе. При этом будет построено дерево сентенциальной формы . . .VS. . . для некоторого V, такого, что V =>+ . . .R. Возможны два случая:
ГРАММАТИКИ ПРОСТОГО ПР Е Д ШЕ СТ ВОВ АНИЯ
131
1. Если в основу входят и V, и S, заканчиваем работу, так как в этом случае должно существовать правило U:	. .VS. . . ,
и мы получаем, что U: : = . . .VW. .. , где V =>+ . . .R и W=S.
2. Если S — голова основы, проводим ряд редукций до тех пор, пока не получим для некоторого W сентенциальную форму . . . . . .VW. . . , где V входит в основу. Заметьте, что W=>+S. . . . Более того, символ W должен тоже входить в основу, так как если бы последним в основе был символ V, он, начиная с первого этапа, был бы последним символом и в основе цепочки, а это противоречит тому, что в основу входит символ S.
Таким образом, существует правило U:	. .VW. . . , где
V =>+ . . .R и W =>+ S. .. , и теорема доказана.
(5.2.5.). Т е о р е м a. R<£S тогда и только тогда, когда существует сентенциальная форма . . .RS. . . , где S — голова основы.
Доказательство. Доказательство этой теоремы предоставляется читателю (упражнение 1).
Мы видим, что формальные определения (5.2.1) эквивалентны неформальным, описанным в разд. 5.1 в терминах синтаксических деревьев. Важно, чтобы читатель изучил эти неформальные определения. Без их полного понимания не следует двигаться дальше. Теперь можно определить грамматику простого предшествования. (5.2.6.). Определение. Грамматику G называют грамматикой (простого) предшествования или грамматикой (1,1) предшествования, если
1) между любыми двумя символами из словаря определено не более чем одно отношение;
2) ни у каких двух продукций нет одинаковых правых частей.
Запись (1,1) означает, что для принятия решения о том, действительно ли предполагаемая основа является основой, мы используем по одному символу слева и справа от нее. То есть, если в основу входит символ R, мы по одному только следующему за ним символу S определяем, является ли R хвостом основы; аналогично решается вопрос о голове основы. Если G — грамматика предшествования, то отношения позволяют найти основу любой сентенциальной формы; при этом соблюдение второго условия [см. (5.2.6)] гарантирует нам то, что основу можно привести к единственному нетерминалу. Это будет доказано в сформулированной ниже теореме.
Прежде чем перейти к ней, остановимся еще на одной проблеме. Необходимо показать, что основа — это самая левая цепочка Sj . .. •. .Si, удовлетворяющая (5.2.9.). Однако следует не упустить из виду случай, когда в основу входит символ Si — первый символ сентенциальной формы, так как в этом случае символа So, такого, что 5*
132
ГЛАВА 6
S0<Sb нет. Попытаемся решить эту проблему, предположив следующее:
(5.2.7)	Каждая сентенциальная форма заключена между символами и (при этом предполагается, что не есть символ из данной грамматики). Более того, будем считать, что # <• S и S •> # для любого символа S из грамматики.
Теперь перейдем к теореме.
(5.2.8).	Теорема. Грамматика предшествования однозначна. Более того, единственная основа любой сентенциальной формы Sv .. Sn — это самая левая подцепочка Sj. . -Si, такая, что
(5.2.9)	 .iS,<S1+r
Доказательство. Во-первых, покажем, что основа х любой сентенциальной формы единственна. Будем считать очевидным, что если Sk. . .Sp — основа, то
Sk-i <t Sk ... .1.Sp •>Sp+1
Это следует из определения отношений <•, X и •>. Предположим, что найдется синтаксическое дерево, у которого нет самой левой подцепочки Sj. . .Sb удовлетворяющей (5.2.9) и являющейся основой. Тогда, сколько бы мы кустов ни отсекали, подцепочка не станет основой, так как если основа в какой-то момент образует самый левый полный куст, то она образует его всегда.
Во время канонического разбора каждый из символов Sj_1( Sj, . . . , Sj+i должен в какой-то момент стать частью основы. Пусть St — первый из таких символов. Так как эта основа не может совпадать с Sj. . .Si, должно выполняться одно из следующих соотношений:
1.	t=j — 1, в этом случае Sj_1=±=Sj или Sj_j»>Sj.
2.	t=j+l, в этом случае Sj^Sj или St <• Sj+1.
3.	j<t<i
a)	Sj_x входит в основу, в этом случае Sj_1=^Sj;
b)	S1+1 входит в основу, в этом случае S1=^S1+];
с)	для некоторого k, j<k^t, Sk — голова основы, в этом случае Sk-!<»Sk;
d)	для некоторого k, t^k<i, Sk — хвост основы, в этом случае Sk*>Sk+1.
Мы видим, что в каждом из этих случаев между двумя символами из числа символов Sj_....Si+1 существует еще одно отношение,
что противоречит определению грамматики предшествования. Следовательно, Sj. . .Sj — единственная основа.
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
133
Таким образом, на каждом шаге разбора любой сентенциальной формы х основа определяется единственным образом. Так как основа может быть правой частью только одного правила [условие 2 из (5.2.6)], ее можно привести; следовательно, единственна и непосредственная редукция, производимая на каждом шаге. Это означает, что для любого предложения существует только одно синтаксическое дерево, т. е. грамматика является однозначной.
Мы видим, что после того, как определены отношения между символами, разбор предложений, соответствующих грамматикам предшествования, становится простым. По-видимому, это самый простой класс нетривиальных грамматик, который может иметь практическое применение. Сами отношения, если вводить их так, как описано в разд. 5.1, довольно легко воспринимаются, несмотря на то что последние определения выглядят весьма беспорядочными. Далее мы перейдем к другим классам грамматик, которые более сложны для определения, но чаще используются на практике. Тем не менее мы еще поговорим о грамматиках предшествования и составим алгоритмы для построения отношений и для реального распознавателя или анализатора предложений таких грамматик.
Построение отношений предшествования
Построение отношения простого предшествования JL не требует практически никаких объяснений. Нужно только просмотреть правые части правил и установить отношение R—S для всех R и S, таких, что . . .RS. . .— правая часть.
Установление отношений <• и •> — дело более трудоемкое, но и их вычислить несложно; необходимо только воспользоваться теорией булевых матриц, изложенной в разд. 2.7.
(5.2.10).	Теорема. Отношение <• равно произведению отношений А. и FIRST*, т. е.
<• = (=^=) (FIRST+)
Доказательство. Это следует непосредственно из определения произведения отношений и из определения <£ (5.2.1).
(5.2.11).	Теорема. Пусть I — единичное отношение. R«>S тогда и только тогда, когда S — терминал и
R ((LAST+)-1) (=) (I + FIRST + ) S
Доказательство. Доказательство этой теоремы предоставляется читателю (упражнение 4).
Теорема (5.2.10) показывает, что можно вычислить отношение <Ъ если 1) построить булеву матрицу F для отношения FIRST,
134
ГЛАВА S
2)	построить матрицу F+, используя для этого, например, алгоритм Воршалла, 3) построить матрицу EQ для отношения = и 4) умножить EQ на F+. Процесс вычисления отношения •> почти столь же прост.
УПРАЖНЕНИЯ К РАЗДЕЛУ 5.2
1.	Докажите, что R<»S тогда и только тогда, когда существует сентенциальная форма . . .RS. . . , где S — голова основы.
2.	Покажите, что следующая грамматика не является грамматикой предшествования:
Z: :=ЬЕЬ, Е: : = Е+Т |Т
3.	Покажите, что следующая грамматика не является грамматикой предшествования:
Z: :=b Elb, El: :=Е | Е+Т | Т | i, Т: : = i | (Е1)
4.	Докажите теорему (5.2.11).
5.	Постройте отношения для грамматики (5.1.1), используя теоремы (5.2.10) и (5.2.11).
6.	Напишите на каком-нибудь языке высокого уровня и отладьте программу для построения и вывода отношений на печать. Входной информацией для программы должна быть заданная в удобной форме грамматика.
5.3.	АЛГОРИТМ РАЗБОРА
При практическом применении отношений предшествования для распознавания предложений нам потребуется способ их компактного представления. Обычно этой цели служит матрица Р, элементы которой имеют значения:
P1j=0, если Sj и Sj несравнимы
Pi, 1=1, если Si<«Sj
Pi’j=2, если Si^Sj
Pi’j=3, если Sj *>Sj
Для грамматики предшествования такое представление возможно, так как известно, что между любыми двумя символами определено не более одного отношения.
Сами правила должны находиться в таблице, имеющей такую структуру, которая позволяет по заданной правой части найти содержащие ее правила, а затем указать соответствующую левую часть.
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
135
Алгоритм разбора работает так. Символы входной цепочки просматриваются слева направо и заносятся в стек S до тех пор, пока не окажется, что верхний символ стека находится в отношении •> к следующему входному символу. Это означает, что верхний символ стека является хвостом основы и, следовательно, вся основа уже в стеке. Затем ее находят в списке правил, и в стеке она заменяется
< том и только в том случае, когда, I ’2, S(i)=ZuR » #
Рис. 5.4. Распознаватель для грамматики простого предшествования (отношения •> и <• обозначены соответственно и «-).
тем нетерминалом, к которому ее надлежит привести. Процесс повторяется до тех пор, пока в стеке не окажется символ ,,Z“ (Z — начальный символ) и следующим входным символом не станет
На рис. 5.4 изображена блок-схема работы распознавателя; используются следующие обозначения:
1.	S — стек, в котором хранятся символы; его счетчик —i.
2.	j — индекс, используемый для адресации нескольких верхних элементов стека.
3.	Анализируемое предложение — TiT2. . .Тп. Мы начинаем работу, когда в стеке находится разделитель предложения #, и предполагаем, что такой же символ приписан к предложению в качестве Та+1.
136
ГЛАВА 5
4.	Q и R — переменные, принимающие значения символов; они используются для хранения некоторых символов во время работы.
Заметим, что, если цепочка ТР . .Тп не является предложением, алгоритм остановится и сообщит об этом. Далее приводятся пояснения к каждому из элементов блок-схемы.
Блок 1. Прежде всего в стек S занести разделитель предложения. Установить значение индекса, указывающее на первый символ.
Блок 2. «Сканировать» следующий символ — поместить его в R и увеличить значение к.
Блок 3. Если (S(i), R) не находятся в отношении •>, значит еще не вся основа в стеке, поэтому занести R в стек и перейти к сканированию следующего символа.
Блоки 5—7. S(i)»>R. Значит, основа находится в стеке. Теперь ведется поиск головы основы, т. е. такого j, когда S (j — 1) <• S(j).
Блоки 8—9. Проверить цепочку S (j). . .S (i). Если она не является правой частью никакого правила, то работа закончена. Эта цепочка является предложением в том и только в том случае, когда i=2, S (i)=Z и Z — начальный символ. Если цепочка — правая
Шаги	S1	S2	Отношение R	Tk ...
0			< ь	( а а ) b #
1		b	<• (	а а ) b #
2		b	(	<	а	а ) b #
3		b	(а	•>	а	) ь #
4		b	( М	а	) ь #
5		b	( М а	)	ь #
6		ь	( М а )	>	b	
7		ь	( L	>	b	
8		ъ	М	b	
9		ь	Mb	>	#	
10		Z		
Рис. 5.5. Разбор цепочки b (а а) Ь.
часть некоторого правила, то-исключить основу из стека (блок 9), поместить в стек символ, к которому эта основа приводится, и перейти к блоку 3, т. е. к поиску очередной основы.
В этом и в других подобных распознавателях очень привлекательно то свойство, что не нужно одновременно хранить в памяти всю цепочку входных символов (если только грамматика не из ряда вон выходящая). Символы считываются с входного носителя по одно
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
137
му и заносятся в стек, но после редукции основы те символы, которые входили в нее, исчезают. Всю цепочку приходится хранить в памяти только в том случае, если основа находится в правом конце цепочки; но грамматики языков программирования никогда не строятся подобным образом.
Используя описанную блок-схему, проведем еще раз разбор предложения b (аа)Ь грамматики (5.1.1). На рис. 5.5 для каждого шага разбора показано содержимое стека S, сканируемый символ R, отношение между верхним символом стека S (i) и символом R и та часть сентенциальной формы, которая еще не сканировалась; все это — перед выполнением блока 3.
УПРАЖНЕНИЯ К РАЗДЕЛУ 5.3
1.	Используя алгоритм, изображенный на рис. 5.3, и матрицу предшествования грамматики G5 [пример (5.1.1)], попытайтесь разобрать следующие цепочки (не все они являются предложениями): #аа#, #((аа)а)#, #()#, #((((аа)а)а)а)#.
2.	Запрограммируйте и отладьте алгоритм разбора, изображенный на рис. 5.4. Для этого необходимо определить вид матрицы предшествования (это результат работы программы из упражнения 5.2.6). Далее, входной информацией для программы разбора может служить результат работы какого-нибудь сканера, например запрограммированного в упражнении 3.3.2. В любом случае внутреннее представление любого символа должно быть номером строки или номером столбца в матрице предшествования.
5.4.	ФУНКЦИИ ПРЕДШЕСТВОВАНИЯ
Матрица предшествования может занимать слишком большой участок памяти. Если в языке 160 символов, нам понадобится матрица, состоящая из 160x160 элементов, каждый длиной не менее двух битов. Однако во многих случаях информация, задаваемая в матрице, может быть представлена двумя функциями f и g, такими, что
из R=S следует f(R)=g(S)
(5.4.1) из R<»S следует f (R)<g (S) из R >S следует f (R)>g (S)
Для всех символов грамматики. Это называется линеаризацией матрицы. Таким образом, мы можем уменьшить объем требуемой памяти с пхп ячеек до 2хп ячеек. Для матрицы предшествования,
138
ГЛАВА S
изображенной на рис. 5.2, функции f и g таковы:
(5.4.2)
Z ЬМ L а ( ) f 1 4 7 8 9 2 8 g 1 7 4 2 7 5 9
Следует отметить, что эти функции неединственны — если для данной матрицы найдется хотя бы одна пара функций f и g, то для той же матрицы существует бесконечное количество таких функций. Но есть много матриц предшествования, для которых эти функции не существуют. Нельзя линеаризовать, например, даже такую матрицу предшествования размером 2x2 для символов Sx и S2:
Почему же? Если бы функции существовали, то из (5.4.1) следовало бы
f (Sj)>g (S2), g (S2)=f (S2), f (S«)=g (SJ, g (SO=f (Sx),
что ведет к противоречивому утверждению f (Si)>f (Si).
Функции предшествования (если они существуют) строятся следующим образом:
(5.4.3).	Теорема. Построение функций предшествования производится в несколько шагов.
1.	Начертить ориентированный граф с 2хп узлами, помеченными G,. . . , fn и gi, . . . , gn. Если S j«>=Sj, провести дугу от ft к gj. Если Si<^=Sj, провести дугу от gj к ft.
2.	Каждому узлу поставить в соответствие число, равное числу узлов, в которые можно попасть из данного узла (включая и этот узел). Число, поставленное в соответствие fи есть f (Sj); число, поставленное в соответствие g1( и есть g (Sj).
3.	Проверить, не противоречат ли значения построенных функций f и g исходным отношениям (по 5.4.1). Если нет противоречий, то функции f и g построены правильно. Если противоречие есть, то функций предшествования не существует.
Доказательство. Мы должны показать, что Sj ASj влечет за собой равенство f(Sj)=g(Sj), Sj<Sj влечет f(S4)<g (Sj), и Sj«>Sj влечет f (Si)>g (Sj). Первое непосредственно следует из построения, ибо SjstsSj означает, что есть дуга от fj к gj и дуга от gj
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
139
к fP Следовательно, любой узел, который достижим из одного такого узла, достижим и из другого.
Другие два условия симметричны, и мы докажем только, что из Si»>Sj следует f (Si)>g (Sj). Из отношения St»>Sj следует, что есть дуга от f1 к gj. Поэтому любой узел, достижимый из gj, достижим и из fi, следовательно, и f (S,)^g (Sj). Осталось только показать, что в случае равенства (если f (St)=g (Sj)) функций предшествования вообще не существует.
Пусть f (St)=g (Sj). Тогда должен быть путь, ведущий из ft В gj, из gj в fk, из fk в g,, ... , из gm в fj. Следовательно,
Sj»>Sj, Sk<»=Sj, Sk*> = S]..Sl< = SB1.
Из этого ясно, что для любых функций f и g должно выполняться отношение f (Sf)>f (S^, т. е. мы пришли к противоречию.
Ориентированный граф описывает отношение R, заданное в множестве узлов; xRy тогда и только тогда, когда существует дуга из узла х в узел у. Следовательно, такой граф можно представить в виде булевой матрицы В размерностью 2хп таким образом:
(5.4.4)
В :
О GE
LET О
где GE — матрица размерностью n х п для отношения •>= и LET — транспонированная матрица для отношения Таким образом, для i, j = l, ... , п имеем
В [i, j]—В [n+i, n+j]=O;
(5.4.5)	' В [i, n+j]=l тогда и только тогда, когда SiV>==Sj
(в верхней правой четверти);
В [n+j, i]=l тогда и только тогда, когда Si<»=Sj (в нижней левой четверти).
Строки 1, ... , п относятся к fx, ... , fn, тогда как строки п+1, ... , 2хп относятся Kgo... , gB. Предоставим читателю Доказать следующую теорему:
(5.4.6).	Теорема. Если функции предшествования существуют, их можно построить следующим образом:
1.	Построить матрицу В, как в (5.4.4).
2.	Построить рефлексивное транзитивное замыкание В* для матрицы В.
3.	f [5}]:=число к, такое, что В* [j, k]=l.
g [Sj]:=число k, такое, что В* [n+j, k]=l.
140
ГЛАВА 5
Для матрицы предшествования на рис. 5.2, например, получится
Г		0	0 0	0	0	0	0				0	0	0	0	0	0	0	и
		0	0 1	0	0	0	0				0	0	1	0	0	0	0	
		0	1 0	0	1	0	0				0	1	0	0	0	1	0	
GE-		0	1 0	0	1	0	0		LET-		0	0	0	0	0	1	0	
		0	1 0	0	1	0	1				0	1	1	0	0	1	0	
		0	0 0	1	0	0	0				0	1	0	0	0	1	0	
1	__	0	1 0	0	1	0	0	J	1	I		0	0	0	0	1	0	0	J
Таким образом:
~о ооооооооооооо-00000000010000 00000000100100 00000000100100 00000 0-0 0100101 00000000001000 00000000100100 00000000000000 001 00000000000 01000100000000 0 0 0 0 0 1 ооооооо» 01 100100000000 01000100000000
1_0 000100000000 0_
Г 1 000000000000 0"1 0100010001 1000 01100100111100 01110100111100 01101100111101 00000100001000 01100110111100 00000001000000 01100100111100 0100010001 1000 000 0.0 100001000 0110010011 1100 0100010001 1010
1_о 1 1 О 1 1 О О 1 1 1 1 О 1_
Подсчет числа единиц в каждой i-й строке матрицы В* дает нам значение f (Sj) для i=l, ... , п, а подсчет числа единиц в каждой строке n+j, j = l, . . . , п дает нам значение g (S^, как в (5.4.2).
Когда компилятор обнаруживает в программе ошибку, он должен попытаться нейтрализовать ее и продолжать разбор, чтобы найти другие ошибки. Поэтому иногда важно обнаружить ошибку как можно раньше, чтобы можно было ее нейтрализовать. Из-за применения функций процесс обнаружения ошибок может затянуться. Линеаризация ведет к потере информации, потому что неизвестно, существует ли в действительности между двумя символами отношение. Для иллюстрации попробуем в соответствии с грамматикой G5 провести разбор цепочки ba)))))b, используя сначала полную матрицу, а затем функции.
1) Разбор с использованием матрицы
Шаг	S1S2...	Отношение	RTk...
0		<•	b a ) ) ) ) ) b #
1	#ь	<•	a ) ) ) ) ) b#
2	#ba	•	) ) ) ) ) b #
3	#ba)	Нет отношения	) ) ) ) b#
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
141
2) Разбор с использованием функций f и g
Шаг	SjS2... f	Отношение g	RTk...
0		1 < 7	ba ) ) ) ) ) b #
1	#ь	4<7	a) ) ) ) ) b #
2	#ba	9 = 9	)) ) ) ) b #
3	#ba)	8<9	)) ) ) b #
4	#ba))	8<9	)) ) b #
5	#ba)))	8<9	)) b #
6	#ba))))	8<9	) b #
7	#ba)))))	8> 7	b#
При использовании функций до обнаружения ошибки было выполнено на четыре шага больше, чем в первом случае.
УПРАЖНЕНИЯ К РАЗДЕЛУ 5.4.
1.	Постройте функции предшествования f и g для следующей матрицы:
2.	Докажите теорему (5.4.6).
3.	Измените программу из упражнения 5.2.6 так, чтобы среди результатов были значения функций предшествования.
4.	Измените алгоритм разбора из упражнения 5.3.2 так, чтобы вместо отношений предшествования использовались функции.
5.5. ТРУДНОСТИ, ВОЗНИКАЮЩИЕ ПРИ ПОСТРОЕНИИ ГРАММАТИК ПРЕДШЕСТВОВАНИЯ
Мы начали с описания метода простого предшествования, как с самого простого, но иллюстрирующего многие приемы синтаксического анализа, с которыми нам еще предстоит встретиться. С теоретической точки зрения этот метод кажется безупречным и эффективным. Однако на практике он не всегда хорош. И действительно, почти любая другая техника оказывается лучше простого предшествования. Очень часто между двумя символами определено более чем одно отношение. Единственное, что мы можем тогда сделать,— это обработать и изменить грамматику так, чтобы обойти конфликт. Но в результате может измениться вся структура языка, не говоря о том, что грамматика станет неудобочитаемой.
142
ГЛАВА 6
Одна из таких проблем может возникнуть как следствие левосторонней рекурсии. Предположим, что существует некоторое правило U: :=U. . . . Если есть другое правило вида V: :=. . .SU. . получается, что одновременно SaU и S<* U. Иногда можно избавиться от такого конфликта, вводя еще один нетерминал W и промежуточное правило; заменим
V: :=. . .SU. . .
на
V: :=. . .SW. . . , W: :=U,
где W — новый символ; при этом получим, что S^W и S<»U. Такой прием иногда называют стратификацией или разделением. Однако это довольно искусственный прием, и нам придется разработать для распознавателя другие методы, при использовании которых такие конфликты встречаются реже. При правосторонней рекурсии те же проблемы возникают с отношениями •> и =L. Стратификация не всегда спасает, так как она часто приводит к конфликтам иного рода. Если одновременно R<»S и R»>S, то лучший выход— применить другую технику.
Чтобы показать, как делается стратификация, воспользуемся привычной уже грамматикой для арифметических выражений:
Е ::=Е+Т |Т
(5.5.1)	Т ::=T*F | F F ::=(Е) | i
Из первого правила следует, что + = Т, а так как символ Т леворекурсивен, получаем также и 4- <• Т. Та же самая проблема возникает и с символами “)” и “Е”. Без ущерба для структуры предложений и не меняя языка, можно заменить эту грамматику такую:
Е ::=Ех
Ех и^Ех+Тх | Т»
Тх ::=Т
Т ::=T*F | F
F ::=(Е) | i
Тогда	отношения FIRST* и	LAST* задаются следующим
образом:		
и	S—такой символ,	S—такой символ, что
	что (U, S) € FIRST+	(U, S}£LAST+
Е	Е1( Т, Тх, F, (, i	Ex, Tx, T, F, ), i
Ех	Ex, Т, Тх, F, .(, i	T„ T, F, ),. i
Тх	T, F, (, i	T, F, ), i
т	T, F, (, i	F, ), i
F	(. i	). i
ГРАММАТИКИ ПРОСТОГО ПРЕДШЕСТВОВАНИЯ
143
Отсюда получаются матрица и функции предшествования:
Е Ех Tf Т F i (	+	*	) f g
Е
Et Т, T F i
> 4	3
>54 >65 > 7	6
>77 2	7
4	4
6	6
>72
Этот пример может создать впечатление, что изменения не столь значительны, однако если в грамматике 100 правил и около 100 с лишним символов (а так оно и есть в языках типа АЛГОЛ), то даже искушенный человек затратит немало времени на то, чтобы переделать такую грамматику в грамматику предшествования.
Тут читатель может задать вопрос, в чем же заключаются преимущества того или иного метода, с которыми мы собираемся его познакомить. Заметьте, что в методе предшествования решение принимается с учетом весьма ограниченного контекста возможной основы. В сущности, в каждом случае во внимание принимаются только два соседних символа. Если же рассматривать и другие символы или большее количество символов, то можно надеяться, что конфликтных ситуаций станет меньше. Таким образом, не ограничиваясь только Sj и Sj+1, для обнаружения хвоста основы рассмотрим символы
Sj_1( Sj и S1 + 1 или S1( S1 + 1 и Sj+,
Проиллюстрируем это на примере сентенциальной формы E-f-T*F грамматики (5.5.1). Поскольку отношения 4-<»Т и + 2= Т противоречивы, мы не можем всего по двум символам, + и Т, сделать вывод о том, является ли Т головой основы, т. е. нужно ли выполнять сложение. Если же известно два символа + и * или же три символа +Т*, то интуиция нам подскажет, что складывать нельзя, из чего следует, что символ + в основу не входит.
144
ГЛАВА 5
УПРАЖНЕНИЯ К РАЗДЕЛУ 5.5
1.	Не меняя язык, измените заданную грамматику так, чтобы она стала грамматикой предшествования. Найдите все отношения.
<описание>
<тип>
<список ид>
=<тип> <список ид>
=REAL | INTEGER |BOOLEAN =i | <список ид>, i
2.	Не меняя язык, измените заданную грамматику так, чтобы она стала грамматикой предшествования. Найдите все отношения.
Р : :=<лв>
<	лв> : :=л : =<лв> | <ав>=<ав>
<	ав> : :=а : =<ав> | <ат>
<	ат> : :=<ат>—<ап> | <ап>
<	ап> : :=а | «ав» | @(<лв»
<лв> означает логическое выражение, <ав> — арифметическое выражение, <ат> — арифметический терм и <ап> — арифметическое первичное выражение; “л” (“а”) — логический (арифметический) идентификатор. (Чтобы вычислить логическое выражение л:=<лв>, вычисляется значение <лв> и присваивается л. Тогда значение выражения становится значением л. Аналогично делается для арифметического выражения а:=<ав>. Значение «лв» равно 1, когда <лв> истина, и равно 0 в противном случае.)
5.6.	ИСТОРИЧЕСКИЕ ЗАМЕЧАНИЯ
Первая формальная трактовка отношений предшествования и функции предшествования принадлежит Флойду [63]. Формализация, предложенная Флойдом, будет обсуждаться в следующей главе. Описанная здесь форма отношений была введена Виртом и Вебером [66J. Специфический метод вычисления отношений появился в статье Мартина [68]. Метод, использованный для вычисления функций предшествования, обязан своим появлением Беллу [691. В компиляторе с ALGOL W (см. Бауэр, Беккер и Грехем [68]) используется автоматически построенная матрица предшествования. Кроме того, есть несколько статей, расширяющих понятие простого предшествования. Ссылки на них см. в конце гл. 6.
Глава 6
Другие восходящие распознаватели
В гл. 5 изложены методы простого предшествования для восходящего разбора. В этой главе мы обсудим несколько других алгоритмов разбора того же типа. Сходство всех этих алгоритмов состоит в том, что до тех пор, пока предложение не будет приведено к начальному символу, выполняется следующая последовательность шагов:
1.	Найти основу х (или некоторую ее разновидность).
2.	Найти правило U : : = х с х в правой части.
3.	Привести х к U и таким образом построить один куст синтаксического дерева.
Различия в таких распознавателях сводятся к двум моментам: к числу и расположению символов, по которым определяется основа, и к структуре задания таблиц и правил в самом распознавателе. Реализация любого распознавателя распадается на такие этапы:
1.	Программируется общая часть распознавателя, которая пользуется таблицами. В таблицах описывается грамматика.
2.	Программируется конструктор, который проверяет, приемлема ли заданная грамматика, и строит необходимые для распознавателя таблицы.
3.	Наконец, программа-конструктор выполняется (исходными данными для нее будет грамматика), а результат ее работы объединяется с общей частью распознавателя. Таким образом получается распознаватель для заданной грамматики.
Теперь, когда есть распознаватель предложений данной грамматики, его можно использовать по мере необходимости, не выполняя всякий раз программу-конструктор. В случае простого предшествования получается такой распознаватель, как на блок-схеме, изображенной на рис. 5.4, а таблица — это матрица предшествования и правила.
Некоторые методы восходящего разбора являются формализацией методов, использовавшихся в ранних компиляторах; в ту пору требования к грамматикам определялись по большей части тем,
146
ГЛАВА б
как работали распознаватели, построенные по интуитивным соображениям. Это касается предшествования операторов и матриц переходов. Другие грамматики с (ш, п) ограниченным контекстом и типа LR (к) первоначально были получены теоретически и на практике пока не применялись
Каждый из следующих разделов посвящен определенному типу распознавателей. Изложение будет полным, но кратким. Доказательства теорем предоставляются читателю в качестве упражнений. Напомним, что все излагаемые методы схожи между собой в том, что при решении проблемы поиска основы и символа, к которому надлежит ее привести, рассматривается контекст, в котором эта основа встречается.
6.1.	ПРЕДШЕСТВОВАНИЕ ОПЕРАТОРОВ
Операторные грамматики
Кто бы ни вычислял выражение 2 + 3*5, результатом всегда будет 17. На вопрос, почему сначала выполняется умножение, вам ответят, что умножение всегда выполняется раньше сложения, что оператор * предшествует оператору +. Важно отметить, что операнды 2, 3 и 5 никоим образом не влияют на последовательность выполнения операций; принимаются во внимание только сами операторы. Напомним, что при использовании техники простого предшествования определялись отношения предшествования между всеми символами — как операторами, так и операндами, что вызывало некоторые трудности. Техника предшествования операторов формализует понятие предшествования только между операторами, причем операторами могут быть лишь терминальные символы грамматики.
Начнем изложение с краткой иллюстрации того, как на практике применяется эта техника в некоторых компиляторах, обычно для анализа арифметических выражений. Программа разбора использует два стека вместо одного: стек операторов OPTOR и стек операндов OPAND. В первом хранятся бинарные и унарные операторы, круглые скобки и ограничитель #, которым начинается и заканчивается каждое выражение. В стек OPAND заносятся идентификаторы и другие операнды по мере их получения. Кроме этих двух стеков, есть еще два целочисленных вектора f и g, аналогичных функциям простого предшествования из разд. 5.4.
Шаг работы общего алгоритма разбора выполняется следующим образом (Si — верхний символ в стеке операторов, S2 — входной символ):
1.	Если Si — идентификатор, то поместить его в стек операндов и пропустить шаги 2 и 3.
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
147
2.	Если f (S^ g (S2), поместить Sa в стек операторов и взять следующий входной символ.
3.	Если f (Si) > g (S2), вызвать (семантическую) подпрограмму, определяемую символом Si. Эта подпрограмма выполнит семантическую обработку, исключит из стека операторов St и, возможно, другие символы, исключит из OPAND связанные с этими символами операнды и занесет в OPAND нечто, являющееся результатом работы оператора Si. Это соответствует редукции основы сентенциальной формы.
Чтобы проиллюстрировать это на примере разбора арифметических выражений, проведем разбор выражения # А + В + С #; одновременно будем присваивать компонентам векторов f и g надлежащие значения. Когда начинается разбор, в стеке OPTOR находится #, а стек OPAND пуст. На первом шаге идентификатор А помещается в OPAND. На втором + следует поместить в OPTOR (мы не можем ни редуцировать, ни обрабатывать +, пока не получим сведений от обоих его операндах). А это значит, что f (#) < g(+), поэтому положим, например, f (#) = 1 и g (+) = 4.
На третьем шаге в OPAND помещается В, и теперь
оставшаяся часть f (*) = 1
цепочки,: +C#	9 С+)
Заметьте, что + есть верхний элемент стека OPTOR, а его операнды А и В — верхние в стеке OPAND. Теперь необходимо обработать + семантически, возможно, порождая при этом внутреннюю форму операции наподобие (+ А, В, Т1), а затем сделать следующее: исключить + из OPTOR, исключить А и В из OPAND и поместить в OPAND имя временной переменной Т1, представляющей результат выполнения операции А + В. Тогда получим
OPTOR
OPAND:

оставшаяся часть f (#)я1 цепочки,: +C#	Т(+)в5	д(+)“4
Следовательно, должно выполняться неравенство f (+) > g (+), поэтому положим значение f (+) равным 5. Заметьте, что все это соответствует редукции с использованием правила Е : : = Е + Т.
На следующем шаге в OPAND помещается символ С
OPTOR
OPAND:
оставшаяся часть цепочки : #
f(#)= 1
f(+)=5 д(+)=4
Заметьте опять, что + находится в OPTOR, а его операнды — верхние в OPAND, так что вновь необходимо обработать либо ре
148
ГЛАВА 6
дуцировать операцию +. Следовательно, нужно, чтобы f (+) > g(#). Выполнив этот шаг, заканчиваем работу, имея
optor:
OPAND:|T2 I	оставшаяся чает f (#) = 1 д (#) = 1
I	I цеаотхв: *	f(+)=4 9(+)“5
Таким образом, изучая, как поступить с каждой парой символов (верхний элемент в OPTOR, входной символ), и устанавливая соответствующие значения функций, мы строим функции f и g. Обратите внимание, что. из выражения # А + В * С # непосредственно следует соотношение f (+) < g (*), поскольку входной символ * заносится в стек, если символ + уже находится в OPTOR. Это можно сделать, если положить g (*) = 6. Для более систематического построения функций сначала определяются все нужные отношения, а затем используется либо метод графа, либо метод булевой матрицы (см. разд. 5.4).
Чтобы выявить ошибки и установить, какой операцией является минус — бинарной или унарной, необходимо тщательно проверить, действительно ли за каждым оператором или круглой скобкой следует операнд. Для этого в каждом элементе стека OPTOR отводится один бит, первоначально равный нулю. Всякий раз, когда в OPAND заносится операнд, в верхнем элементе стека OPTOR значение этого бита заменяется на 1, чтобы показать, что за данным символом следует операнд.
Этот метод известен так же как метод двойного приоритета, поскольку для определения того, какой из операторов предшествует, или имеет приоритет, используются две функции. Может показаться, что удалось бы обойтись и одной функцией h; если h (SJ > h (S2), в стеке выполняется редукция, а в противном случае в стек заносится символ S2. Однако обычное употребление круглых скобок в выражении делает это невозможным. Судя по выражению #(А) * В #, нам нужно, чтобы h ())>h(*); в то же время из # (А * В) # следует h (*) > h ()), а это невозможно.
Заметьте, что, когда в OPTOR производится «редукция», верхний символ некоторым образом определяет подпрограмму, которая будет выполняться. Символ S в OPTOR можно представить так:
f (S) '	адрес подпрограммы^поторую нужно выполнить 1
Компиляторы до сих пор пишут на языке ассемблера еще и потому, что обычные языки высокого уровня не позволяют использовать многие эффективные представления такого рода.
Приводя здесь краткое описание процесса, мы стремились показать, с чем обычно сталкиваются при написании компилятора. Остановимся теперь более детально на формализации этой техники.
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
149
Здесь вместо двух стеков будет использоваться один; правда, читатель дальше убедится в том, что один или два стека — это исключительно дело вкуса.
Мы накладываем на грамматики такое ограничение, что в сентенциальной форме между операторами не может быть неограниченного числа операндов (нетерминалов). Другими словами, операции должны быть записаны в инфиксной форме.
(6.1.1).	Определение. Грамматика G называется операторной грамматикой (ОГ), если ни одно из ее правил не имеет вида U :: = ... VW . . . , где V и W — нетерминалы.
Таким образом, в ОГ никакая правая часть не содержит двух соседних нетерминальных символов. Это сказывается на сентенциальных формах; можно доказать (упражнение 1), что никакая
операнды (нетерминалы)
(вызов процедуры) :: = (идентификатор) «список факт, параметров))
7
операторы (терминалы)
(список факт.параметров)'.*. = (список факт.параметров), (факт.параметры)\(фаха.паражтры)
(список идентификитород)*.*^(список буке)	|
 .......Л,,	., операнды
(нетерминалы)
Рис. 6.1. Операторы и операнды.
сентенциальная форма в ОГ не содержит двух соседних нетерминальных символов. Большинство тех грамматик, с которыми мы работаем, можно без особого труда превратить в операторные грамматики.
Слова оператор и терминал мы будем употреблять как взаимозаменяемые. То же относится и к словам операнд и нетерминал. На рис. 6.1 показаны операнды и операторы в нескольких правилах.
Грамматики предшествования операторов
Описанные далее отношения полностью аналогичны отношениям простого предшествования. Единственное различие опять-таки состоит в том, что мы определяем эти отношения только для операторов.
(6.1.2).	Определение. Пусть G — операторная грамматика, a R и S — любые два оператора (терминалы). Тогда для некоторых нетерминалов V и W
150
ГЛАВА в
1.	R _£= S тогда и только тогда, когда существует правило вида U : : = . . . RS . . . или вида U : : = ... RVS ....
2.	R <5 S	тогда и только тогда,	когда существует правило вида
U : :	=	. . .	RW . . ., где W =>+	S . . . или W =>+ VS . . . .
3.	R	S	тогда и только тогда,	когда существует правило вида
U ::	=	...	WS .... где W=>+	... R или W=>+ . . .RV.
(6.1.3).	Определение. ОГ называется грамматикой предшествования операторов (ОПГ), если между любыми двумя терминалами определено не более, чем одно из отношений <5, 2> или °*.
(6.1.4).	Пример. Рассмотрим опять грамматику G [Е]:
Е : : = Е 4- Т | Т Т : : = Т * F |F F :: = ( Е ) |i -
Из последнего правила мы получаем отношение (.°.). Это отно-
шение встречается в грамматике только однажды, так как лишь в
4-* ( )
Рис. 6.2. Матрица предшествования для грамматики (6.1.4).
этой правой части имеются два терминальных символа. Так как E=>E + T=>T + T=>T*F4~T=>F*F + T=^i*F + T, то из правила F : : — (Е) следует, что (<+,(<* и (<5 i. Из того же правила и из последовательностей непосредственных выводов получаем 4- §>).
Полная матрица предшествования операторов показана на рис. 6.2. Так как любые два терминала находятся не более чем в одном отношении, это ОПГ.
Построение отношений
Построение отношений SL не требует детальных объяснений: нужно просмотреть правые части правил в поисках конфигурации RVS или RS. Чтобы построить отношение <5, необходим способ, который позволяет для каждого W находить множество терминальных символов S, таких, что W=>4- S . . . или же W =>4- VS . . ., где V —нетерминал. (Аналогичное утверждение справедливо также
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
151
при построении отношения с>.) С этой целью определим два новых отношения:
(6.1.5).	О п р е д е л е н и е. Пусть G — ОГ, a S — терминал.
U FIRSTTERM S тогда и только тогда, когда существует правило U : : = S . . . или U : : = VS . . . .
U LASTTERM S тогда и только тогда, когда существует правило U ::=... S или U : : — . . . SV, где V — любой нетерминальный символ.
Сокращения «FIRSTTERM» («LASTTERM») означают, что речь идет о первом (последнем) терминале в правой части правила. Очевидно, что эти отношения легко получить непосредственно из правил. Теперь можно построить отношения <5 и с>, используя отношения (отношение простого предшествования), FIRST, FIRSTTERM, LAST и LASTTERM, которые мы уже умеем строить в виде булевых матриц.
(6.1.6).	Теорема. Отношение <5 есть произведение трех отношений =Г, FIRST* и FIRSTTERM: < = ( ^ ) (FIRST*) (FIRSTTERM). Аналогично > = ((LAST* ) (LASTTERM))-1 (i). (Несложное доказательство этой теоремы предоставляется читателю в качестве упражнения 3.) Следовательно,( теперь мы располагаем простыми средствами для вычисления отношений <5 (а также $>). Достаточно, исходя из правил, построить булевы матрицы для трех отношений, вычислить рефлексивное, транзитивное замыкание одного из них и затем перемножить все три матрицы.
Первичные фразы — их обнаружение и редукция
Для нахождения основ, состоящих из одного нетерминала, отношения, о которых сейчас идет речь, применять нельзя, так как для них нетерминальные символы просто не существуют. Рассмотрим, например, сентенциальную форму F + Т из арифметической грамматики примера (6.1.4). Основой является F, а отношения есть только такие: # <j * и + §> # (как и прежде, предполагаем, что сентенциальная форма заключена между ограничителями # и что # <5 S и S с> # для всех терминальных символов S). Тогда техника предшествования операторов не позволит провести канонический разбор так, как мы его определяли, а тот разбор, который можно провести с помощью этих методов, будет только отчасти напоминать канонический. Разбор здесь по-прежнему восходящий, но не строго слева направо. На каждом шаге разбора мы будем распознавать и редуцировать некую цепочку, которая будет называться самой левой первичной фразой.
(6.1.7).	Определение. Пусть G — грамматика с начальным символом Z. Первичной фразой (сентенциальной формы) считается
152
ГЛАВА 6
такая, в которую входит по крайней мере один терминал, а сама она не содержит других первичных фраз.
Рассмотрим сентенциальную форму (с ограничителями #) #T + T*F + i# грамматики (6.1.4). Ее фразами являются Т + Т * F 4- i, Т + Т * F, i, самый левый символ Т и Т » F (их легко найти, просматривая единственное синтаксическое дерево сентенциальной формы (рис. 6.3)). Какие из этих фраз первичные?
Е
♦ F
Рис. 6.3. Синтаксическое дерево для T+T*F+i.
Первичная фраза не должна содержать другую фразу с терминальным символом, поэтому исключается фраза Т + T»F + i, содержащая i. Аналогично, в Т + T*F содержится фраза Т * F. Остаются Т, Т * F и i. Т не может быть первичной фразой, так как не содержит ни одного терминала. (Обратите внимание на то, что Т — основа сентенциальной формы, но не является первичной фразой.) Следовательно, первичные фразы — это Т * F и i.
Запишем теперь общий вид сентенциальной формы, заключенной между ограничителями # и #, так:
#N1T1N2T2...NnTnNn+1#
где N i — нетерминальные символы, которые могут и отсутствовать, a Ti — терминальные символы. Иначе говоря, сентенциальная форма состоит из п терминалов, причем между каждыми двумя соседними терминалами находится не более чем один нетерминал. Напомним, что в ОГ любая сентенциальная форма подчиняется этому требованию. В упражнениях 6—8 требуется доказать такую теорему:
(6.1.8).	Теорема. Самой левой первичной фразой сентенциальной формы в ОПГ является такая самая левая подцепочка N <Т;. . . . . . NJt Ni+1, что
(6.1.9)	ТЬ1 <5 Ть Tj Д Tj+1, . . ., Tb Ti > Т1+1
Эта теорема кажется необычайно похожей на соответствующую теорему для простого предшествования. Основное различие состоит в том, что здесь из отношений исключены нетерминальные символы. Заметим, что нетерминал, находящийся слева от Tj или справа от Tj, всегда принадлежит первичной фразе,
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
153
Чтобы проиллюстрировать теорему, проведем разбор сентенциальной формы Т + Т *F+ i, используя матрицу предшествования, изображенную на рис. 6.2. Читатель должен тщательно изучить и этот разбор, и синтаксическое дерево на рис. 6.3. В таблице даны символы, к которым приводятся первичные фразы; правда, мы еще не объяснили, как определить этот символ. Речь об этом пойдет дальше.
Шаг	Сентенциальная форма	Отношения	Первичная фраза	Привести к
1	#T + T*F + i#	4-	4-	T*F	Т
2	#T+T+i#	# 4-	+	Т4-Т	Е
, 3	#E + i# .		i	F
4	#E + F#		E + F	Е
При проведении этого разбора указываются нетерминалы, к которым приводится каждая из первичных фраз. Отметим, что при поиске самой левой первичной фразы, которую нужно редуцировать, нетерминальные символы вообще не принимаются во внимание. Поэтому совершенно безразлично, какой нетерминальный символ будет занесен в стек — правильный или нет. Их вообще можно было опустить!
В компиляторах с каждым нетерминалом или операндом связано много семантической информации — тип операнда, его адрес, является ли операнд формальным параметром. В гл. 1 уже кратко описывалось, что для обработки первичных фраз будут использованы семантические программы. Эти программы нуждаются только в семантической информации, связанной с символами, а не в самих символах. Например, для семантической программы, которая редуцирует Т * F, безразлично, какая это фраза: Т * F, F * Т или F * F. Она использует только семантику каждого из рассматриваемых нетерминалов — его тип, адрес и т. д. То есть для семантических программ несущественны фактические имена нетерминалов. Следует помнить (см. разд. 2.4), что появление в грамматике нетерминалов Т и F не связано с какими-либо семантическими целями; но они позволили сделать грамматику однозначной и определить предшествование операторов.
Обратите, пожалуйства, внимание на то, что если бы нам был нужен формальный алгоритм разбора, нам пришлось бы указать нетерминал, к которому следует привести первичную фразу. Мы, однако, не ставим себе формальных целей — только практические. Заметьте, что ничего не было сказано о том, что синтаксическое дерево для каждой сентенциальной формы единственно. Это и не обязательно. Но можно сказать, что структура дерева, если не принимать во внимание имен в узлах, соответствующих нетерминалам,
154
ГЛАВА 6
единственна. Это очевидно, так как на каждом этапе разбора самая левая фраза единственна.
Конечно, здесь на долю семантических программ выпадает больше хлопот. Они должны производить более подробную проверку
Рис. 6.4. Распознаватель, использующий предшествование операторов.
операндов, чем в грамматиках простого предшествования. Но что-то мы на этом и выигрываем, так как распознаватель становится более эффективным. Отметим, что уменьшается матрица предшествования и нам потребуется меньше памяти — матрица теперь содер
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
155
жит только те столбцы и строки, которые относятся к терминалам. Более того, распознаватель не выполняет в явном виде таких редукций, как Е => Т или Т => F, поскольку Т и F не первичные фразы. И это хорошо, так как такого рода редукции не несут никакой семантики.
На рис. 6.4 показана блок-схема распознавателя ОПГ, в котором, как обычно, используется стек S со счетчиком i, входная цепочка 7\ . . . Тп со счетчиком к, целочисленная переменная j и две переменные R и Q, принимающие значения символов.
По отношениям предшествования операторов можно построить функции предшествования и использовать их так же, как мы это делали в разд. 5.4. Тогда мы формально проделаем то, что объяснялось в начале раздела применительно к конкретному случаю.
Проведем в качестве примера разбор предложения i * (i + i) грамматики из примера (6.1.4):
Шаг	S(l)...	Отношение	R	Tk...
0		<5	i	«0 + 0#
1				(i + i)#
2				(i + 0#
3	# N »		(	i + i)#
4	# N* (	<5	i	+ D#
5	# N * (i		+	i)#
6	#N»(N	<9	+	0#
7	#N»(N +	<5	i	)#
8	#N*(N + i . '		)	#
9	#N»(N + N		)	4t
10	#N*(N	о	)	
11	# N * (N )		#	
12	#N*N			
13	#N	СТОП		
УПРАЖНЕНИЯ К РАЗДЕЛУ 6.1
1.	Докажите, что в ОГ ни в какой сентенциальной форме не могут встретиться рядом два нетерминала. Указание: при доказательстве воспользуйтесь индукцией по числу непосредственных выводов в выводе сентенциальных форм.
2.	Пусть G — грамматика. Докажите, что если в какой-то правой части содержатся рядом два нетерминала, то по крайней мере в одной сентенциальной форме содержатся рядом два нетерминала. Указание: помните, что все грамматики приведенные [см. определение (2.8.4)].
156
ГЛАВА 6
3.	Докажите теорему (6.1.6).
4.	Постройте булевы матрицы для отношений =°_, =, FIRST, FIRSTTERM, LAST и LASTTERM для грамматики из примера (6.1.4). Затем, исходя из FIRST и LAST, вычислите FIRST* и LAST*. Наконец, используя теорему (6.1.6), вычислите отношения <§ и £>.
5.	Составьте список фраз и первичных фраз следующих сентенциальных форм грамматики из примера (6.1.4) : Е, Т, i, Т * F, F * F, i * F, F * i, F 4- F + F.
6.	Докажите, что если Т входит во фразу некоторой сентенциальной формы, то это относится и к любому нетерминалу U, непосредственно предшествующему (или следующему за) Т. Указание: предположив, что это не так, постройте сентенциальную форму с двумя соседними нетерминалами.
7.	Докажите, что любая первичная фраза удовлетворяет (6.1.9) (учитывая, что сентенциальная форма всегда ограничена символом # и что # <§ S, S <> # для любого терминала S).
8.	Докажите, что любая подцепочка 14/14 . . . NjTjNJ+i из сентенциальной формы, удовлетворяющая (6.1.9), является первичной фразой. Указание: в качестве примера возьмите доказательство теоремы (5.2.8).
9.	Проведите разбор предложений i, i + i, i * i 4- i, i * (i * i) и i * (i 4~ i * i) + ((i + i)*i) грамматики (6.1.4), пользуясь матрицей на рис. 6.2 и распознавателем, изображенным на рис. 6.4.
6.2. ПРЕДШЕСТВОВАНИЕ БОЛЕЕ ВЫСОКОГО ПОРЯДКА
(1,2)^.,\)предшествование
Этот метод позволяет продвинуться на два шага от простого предшествования в сторону более практичного распознавателя, т. е. такого распознавателя, который годился бы для большего числа грамматик. Во-первых, разделяются задачи поиска хвоста и головы основы. Когда ведется поиск хвоста, нас интересует выполнение отношения •> . И совсем не важно, выполняются ли отношения R <• S или R = S, поскольку нас интересует один-единственный вопрос, является ли R хвостом основы или нет. Следовательно, здесь мы пользуемся только отношениями •> и <•= . Аналогично при поиске головы основы используются только отношения <• и •> =. Во-вторых, для обнаружения головы и хвоста основы используются три символа вместо двух. Определим следующие отношения:
(6.2.1).	Определение. Пусть G — грамматика. R, S и Т — три символа. Тогда
RS о> Т, если есть каноническая сентенциальная форма ... RST ... , такая, что S — хвост основы;
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
157
RS о<= Т,
R O<ST,
R 0>=ST,
если есть каноническая сентенциальная форма .. . RST ... , такая, что Т входит в основу; если есть каноническая сентенциальная форма . . . RST . . . , такая, что S — голова основы; если есть каноническая сентенциальная форма . . . RST . . . , такая, что S входит в основу.
Термин (1,2) (2,^предшествование указывает на число символов, используемых для обнаружения головы и хвоста основы. Для обнаружения хвоста проверяем, находятся ли три символа R, S и Т в отношении RS о> Т или RS о<= Т, отсюда в термине — (2,1). Как только найден последний символ, мы, чтобы найти голову, начинаем двигаться от него влево, проверяя, не выполняется ли одно из отношений R о< ST или R о>= ST. Отсюда в термине—(1,2).
. Теперь возникает меньше конфликтных ситуаций, чем в случае простого предшествования. Для выражений в грамматике (6.2.2) получается, что + X Т и + <• Т. Теперь это противоречие устранено, поскольку' используется символ, следующий за Т. В самом деле, + о> Т+ и + о < Т*,
(6.2.2)	Е : : = Т | Е Ч- Т Т : : ~ F | Т * F F : : = i | (Е)
Отношения, заданные в (6.2.1), можно переопределить в терминах выводов следующим образом (здесь Z — начальный символ, а подцепочка, заключенная в квадратные скобки, может и отсутствовать):
RS о> Т,	если существует вывод Z =>* . . . [R] UTx
=> . . . RSTx
RS о<= Т,	если существует вывод	Z =>* . . . [R] [S]Ux
=> . . . RST ... х
R о< ST, если существует вывод Z =>* xRU [Т] . . . => xRST . . .
R о>= ST, если существует вывод Z =>* xU [S] [Т]. . . =► xRST ....
Определения, позволяющие построить алгоритмы для вычисления отношений по грамматике, см. в упражнениях 1—5. Приведем некоторые отношения предшествования для грамматики (6.2.2):
+т О-	>)	(( °<	:= т
(F о>	>)	(( о<	:= f
+F	>)	(( °<	:= (
*F	>)	(( о<	; = 1
(i	>)	(Е о<	:= )
+ 1	>)	(Е о<	:=+
Е) о-	>)	(Е о<	:= )
Т о	> +	+Т о<	с =*
( (	О< о<	: е + : f +	( Т	О>	>= Е) >= +т
(	о <	: i +	F	О/	>= +F
(	о <	; т*	i	О /	>= +F
(	о <	: F*	Т	О/	>= *F
(	о<	; i *	F	О/	>= *F
+	о <	: (Е	i		> = » F
+	о<	; Т*			>= т +
158
ГЛАВА в
Теперь мы можем определить грамматику (1,2)(2,1)предшество-вания.
(6.2.3).	Определение. Грамматикой (1,2)(2,^предшествования называется грамматика, удовлетворяющая следующим условиям.-
1.	Все правые части правил единственны.
2.	Ни для каких символов R, S и Т не допускается, чтобы одновременно выполнялись отношения RSo>T и RSo<=T.
3.	Ни для каких символов R, S и Т не допускается, чтобы одновременно выполнялись отношения R о< ST и R о>= ST.
Можно доказать (см. упр. 6), что грамматика (1,2)(2,^предшествования однозначна и что основа любой канонической сентенциальной формы SiS2 . . . Sn является самой левой последовательностью Sj ... St, удовлетворяющей таким соотношениям:
Sj-i о< SjSj+1,
Sj о>= Sj+1Sj+2, . . . , Sj_j о>= SjSj+1,
Sj-jSj o> Sj+1.
Как и в грамматиках простого предшествования, разбор начала и конца цепочки требует осторожности. Так как отношения определены для трех символов, необходимо поместить в начало и в конец каждой разбираемой цепочки по два символа # и условиться, что для любых символов грамматики Sx и S2
(6.2.4)	SxS2 о> #, #SX о> #, # о< SjSt, # о< Sx#.
Распознаватель для грамматики (1,2)(2,1)предшествования совершенно аналогичен распознавателю для грамматики простого предшествования, и его блок-схема совпадает с блок-схемой на рис. 5.4. Придется несколько изменить блок 1, так как только что описанные ограничители предложений отличаются от применявшихся ранее. В блоке 3 с помощью, скажем, трехмерной матрицы РТ, задающей отношения о> и о<=, будет проверяться выполнение отношения Si-iSj о >R, а в блоке 6 с помощью трехмерной матрицы PH, задающей отношения о< и о>=, будет проверяться, выполняется ли отношение Sj_j о< SjSJ+1. (Заметьте, что в блоке 3 мы предполагаем, что Si+1 является значением переменной R.)
Грамматики большинства используемых формальных языков весьма близки к грамматикам (1,2)(2,1)предшествования, и чтобы свести их к тому же классу, почти не требуется изменений. Однако с практической точки зрения эта техника бесполезна, так как требуются две трехмерные матрицы. Это означает, что для грамматики, в которой используется 100 символов, необходимо иметь две матрицы размерностью 100 х 100 х 100! Так как это обычно разреженные
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
159
матрицы, то можно для экономии памяти хранить список только тех троек элементов R,S и Т,для которых действительно определено отношение; но даже и этот список может иметь недопустимо большие размеры. Оставшаяся часть данного раздела посвящена тому, каким образом можно переработать грамматику и алгоритм, чтобы сделать его приемлемым для практического использования. Мы рассмотрим не все приемы, разработанные Маккиманом и др. [70]. Однако и этого будет достаточно, чтобы составить представление об имеющихся возможностях.
Как сделать (1,2)(2^предшествование пригодным для практического использования
Шаг 1. Отказаться от матрицы PH, задающей отношения о< и о>=. Матрица PH используется при поиске головы основы, когда хвост уже найден. Для большинства основ эта матрица не нужна, так как мы можем просто сопоставить верхние символы стека с правыми частями правил. Проблема возникает только в том случае, когда одна из правых частей — правильный хвост другой правой части; если есть два правила U:: = uxhV:: = xhb стеке находится их, то какую редукцию следует применить? Для правых частей, состоящих из 2 и более элементов, на этот вопрос отвечает следующая теорема (доказательство предоставляется читателю):
(6.2.5).	Теорема. Пусть G — грамматика (1,2)(2,1)предшест-вования. И пусть символ Si — хвост основы, содержащей не менее двух символов. Тогда окончательной основой будет самая длинная подцепочка Sj . . . Sb которая является правой частью некоторого правила.
Следовательно,,нужно рассмотреть только случай, когда имеются два правила: U : : = иХ и U : : = X. Для каждого такого случая можно составить список тех троек (R, X, Т), для которых R о< XT. Когда во время разбора RX о> Т, мы проверяем, есть ли в списке тройка (R, X, Т). Если она есть, то применяется правило U : : =Х; в противном случае применяем то единственное правило с самой длинной правой частью, которое соответствует вэршине стека.
Таким образом удалось избавиться от матрицы PH; взамен мы просто ищем самую длинную из подходящих правых частей, не упуская, конечно, из виду проблему, о которой только что шла речь.
Шаг. 2. Исключить ненужную часть матрицы РТ. Матрица РТ используется для обнаружения хвоста основы. Поскольку мы проводим канонический разбор предложения, справа от любой основы всегда находится терминальный символ. Поэтому из матрицы РТ
160
ГЛАВА 6
можно исключить все столбцы, относящиеся к нетерминальным символам. Матрица уменьшается приблизительно вдвое.
Шаг. 3. Использовать там, где можно, отношения простого предшествования. Для реализации таких отношений (2, ^предшествования, как о> и о<=, требуется слишком много памяти, в то время как бинарные (1,1)отношения •> и <•= либо просто неадекватны, либо недостаточно гибки для обычных языков программирования. Мы идем на компромисс, т. е. там, где можно, применяем отношения простого предшествования, но если возникают конфликтные ситуации, то переключаемся к отношениям (2, ^предшествования. Для реализации используем двумерную матрицу Р, значениями элементов которой могут быть: 0 (отношение не определено), 1 (<•=), 2 (•>) и 3 (одновременно <£= и £>). При Р [i, j) = 3 мы обращаемся к списку TQ, содержащему четверки (Sk, Sb Sj, q), где q = 1, если SkSt o> Sj, и q = 2, если SrSj o<= Sj.
Пересмотренный алгоритм (1,2)(2,1) разбора
На рис. 6.5 показан пересмотренный распознаватель, в котором используются следующие переменные и таблицы. Таблицы Р, TQ, TH и PROD строит конструктор.
1.	S — стек, его счетчик—i.
2.	Ti . . . Tn — входная цепочка; к используется для индексирования символов в цепочке.
3.	Р — матрица размером n х т, где п — число символов, ат — число терминальных символов.
Р li, j] = 0, если между символами St и Sj отношение не определено;
Р В, jl = 1, если Si <•_= Sj-,
Р В, j) = 2, если Sj •> Sj;
P [i, jl = 3, если Si <•= Sj и St •> Sj.
4.	TQ — список четверок (R, S, T, q), где R и S — символы, T — терминальный символ, a q — это 1 или 2. Четверка (R, S, T, 1) находится в списке, если S <£= Т, S £> Т и RS о<= Т. Четверка (R, S,T, 2) находится в списке, если S <•= Т, S •> Т и RS о> Т.
5.	PROD — список правил. Для быстрЬты доступа список следует упорядочить по последнему символу правой части.
6.	TH — список троек, по которым принимается. решение об использовании правила, правая часть которого состоит из одного символа. Тройка (R, X, Т) входит в список, если Т — терминал, если существует правило U : : = X и есть хотя бы еще одно правило V	X и если R о< XT.
Блок, 1
СТОП 5 Предложение 8 w и только о том случае, когда R = #, 1= 2 ic S(t) » Z
Рис. 6.5. Распознаватель для грамматики (1,2)(2,1)предшествованиЯг
6 д. грис
162
ГЛАВА 6
Верхний символ стека:	Т
Список TQ пуст
тов •>, <•=)
тн (#,	т, #)
(#,	т, +)
(( .	т, +)
((,	т. ))
(#>	F, #)
(-н	F, #)
(нет конфлик-
(#, F, +) (( , F, +) (+, F, +) (( . F. )) (+, F, )) (#, F, *) (( , F, *) (+, F, *)
F ) i
Правила, которые мож- Е ::= Т	Т ::= F	F ::= (Е) F :: = i
но было бы применить: Е :: = Е + Т Т :: = Т * F
Рис. 6.6. Таблицы (1,2) (2,1) для грамматики (6.2.2).
На рис. 6.6 в качестве примера изображены таблицы, необходимые для грамматики (6.2.2).
(ш, ^предшествование
Можно предполагать, что если для какой-то грамматики не подходит ни (1, 1)предшествование, ни (1,2)(2,1)предшествование, то для получения однозначных отношений можно использовать больший контекст. Для многих грамматик это действительно так, и в настоящем разделе мы хотим в общих чертах описать такой метод. Однако необходимо отметить, что эта схема непригодна для практического использования из-за размера матриц предшествования. Можно было бы пуститься на всякого рода хитрости — как в случае (1,2)(2,1)предшествования, но не стоит этого делать.
Определим (т,п)предшествование для грамматик в терминах синтаксических деревьев так: пусть ш > 0, п > 0 — целые числа. Пусть хиу — произвольные цепочки, причем |х| = m и |у| = п. Тогда
х <• у, если существует каноническая сентенциальная форма . . . ху . . . , такая, что первый символ цепочки у является головой основы;
х JL у, если существует каноническая сентенциальная форма . . . ху . . . , такая, что первый символ цепочки у и последний символ цепочки х входят в основу;
х •> у, если существует каноническая сентенциальная "форма . . . ху . . . , такая, что последний символ цепочки х является хвостом основы.
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
163
Тогда грамматика (ш, п)предшествования — это такая грамматика, которая удовлетворяет следующим условиям:
1. Все правые части правил единственны.
2. Для любой пары цепочек х, у, таких, что |х| = m и |у| = п, выполняется не более одного из трех отношений (т, ^предшествования.
Простое предшествование — это не что иное, как (1,^предшествование. Можно было бы также показать, что грамматика (т.п) предшествования однозначна, что основа канонической сентенциальной формы определяется отношениями единственным образом и т. д. Мы предоставляем это читателю. Конечно, труднее всего определить отношения так, чтобы можно было их построить. Один способ построения отношений — это просмотр достаточно большого количества синтаксических деревьев и построение отношений по каждому из них до тех пор, пока не будет уверенности, что все отношения найдены. Именно так поступил Маккиман и др. 170] в случае (1, 2)(2,1 предшествования.
УПРАЖНЕНИЯ К РАЗДЕЛУ 6.2
В некоторых упражнениях используются следующие определения:
S — допустимый предшественник U, что обозначается как U АР S, если есть правило Ui :: = ... SV .. . , такое, что V FIRST* U.
S — допустимый преемник U, что обозначается как U AS S, если есть правило Ui: : = . . . VW . . . , такое, что V LAST* U и W FIRST* S.
1. Докажите, что отношение АР определяется как АР = (л=) (FIRST*).
2. Покажите, что RS о> Т тогда и только тогда, когда имеет место один из следующих случаев:
а)	существует правило U : : = VW ... , где U АР R, V SS+ S и W FIRST* Т;
Ь)	существует правило U : : = VW .... где V =>+ . . . RS и W FIRST* Т;
с)	существует правило U : : = . . . RVW . . . , где V SS+ S и W FIRST* Т.
.3. Покажите, что RS о<= Т тогда и только тогда, когда имеет место один из следующих случаев:
а)	есть правило U : : = SW ... , где U АР R и W FIRST* Т;
6*
164
ГЛАВА 6
Ь)	есть правило U : : = . . . RSW .... где W FIRST* Т.
4.	Покажите, что R о< ST тогда и только тогда, когда имеет место один из следующих случаев:
а)	есть правило U : : — . . . RW, где W SS+ S и U AS Т;
Ь)	есть правило U : : = . . . RW . . . , где W =►+ ST . . . ;
с)	есть правило U : : = . . . RWV .... где W SS+ S и V FIRST* Т.
5.	Покажите, что R о>= ST тогда и только тогда, когда имеет место один из следующих случаев:
а)	есть правило U : : = . . . RS, где U AS Т;
Ь)	есть правило U : : = . . . RSV. . . , где V FIRST* Т.
6.	Докажите, что если выполняются отношения (6.2.4), то в грамматике (1,2)(2,1)предшествования основа любой канонической сентенциальной формы Sx . . . Sn является самой левой последовательностью узлов Sj . . . St, удовлетворяющих отношениям Sj_i О< SjSj+1, SkSk+1 о<= Sk+2 для k = j, • . ., i — 2, Si—i Si O> si+1.
7.	Докажите теорему (6.2.5).
8.	Используя программу (рис. 6.5) и таблицы (рис. 6.6), разберите следующие предложения грамматики (6.2.2): i + i + i, i * i + i, i + i * i, (i + i) * i. Попытайтесь разобрать цепочки ii, i + * i, не являющиеся предложениями.
9.	Постройте все необходимые таблицы для описываемой ниже грамматики. Эта грамматика аналогична (6.2.2); отличие состоит в том, что операторы выполняются справа налево, а не слева направо: Е : : = Т | Т + Е, Т : : = F | F + Т, F : : = (Е) | i.
10.	Используя грамматику и таблицы из упражнения 9, разберите следующие предложения: i + i + i, i * i + i.
11.	Объясните, почему грамматика A : : — (В ) | dBe, В : : = с | с В не является грамматикой простого предшествования, хотя грамматика А : : = ( В ) ] d В е, В : : = с | В с, порождающая тот же язык, является грамматикой простого предшествования.
6.3. ОГРАНИЧЕННЫЙ КОНТЕКСТ
Одна из проблем, связанных с техникой простого предшествования, состоит в том, что не допускается существования двух или более правил с одинаковыми правыми частями. В качестве примера рассмотрим грамматику
(6.3.1)	Z:: = aUa|bVb U::=c V : : = с.
Она не является грамматикой простого предшествования, но по контексту с можно легко установить, к U или к V следует приво
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ ..	165
дить с. Так, в предложении аса с следует привести к U, а в bcb — к V. Схемы разбора, в которых используется ограниченный контекст, позволяют нам для решения вопроса о том, к чему приводится основа, употребить фактические символы с любой стороны от обнаруженной основы (а не только отношения предшествования между символами). Заметим, что переход к технике (1,2)(2, 1)предшествования также до некоторой степени позволял нам это.
Определение (1 ^ограниченного контекста
Предположим, что Z =>+ х TURy, где U : : = и — одно из правил грамматики. Тогда если во время разбора канонической сентенциальной формы в стеке окажется хТи, а входным символом будет R, то и следует привести к U. Хотелось бы уметь по TuR составлять суждение о том, является ли и простой фразой для U в любой канонической сентенциальной форме . . . TuR . . . .То есть мы хотим, используя только контекст Т . . . R возможной основы, определять, во-первых, является ли она действительно основой, и, во-вторых, к чему следует ее приводить. Какие новые возможности возникают для основы, если Z =>+ . . . TuR . . . ?
Так как выполняется каноническая редукция, то если в стеке находится . . . Ти, мы знаем, что основа не может оказаться целиком слева от хвостового символа цепочки и. Это вытекает из сущности нашего разбора; как только в стек помещается хвостовой символ основы, мы ее редуцируем.
Теперь можно перечислить следующие возможные основы:
1.	. . . Ти
2.	и; но эта основа будет приведена к V, где V ф U
3.	и2, где и = ихи2
(6.3.2)	4. . . . TuR . . .
5.	uR . . .
6.	u2R . . .
7.	Полностью справа от и
В каждом из этих случаев мы можем показать, какие каноничес-
кие выводы порождают перечисленные основы:
1.	Z =>+ . . .	VR . .	.	,	где	V ::	= ... Ти
2.	Z =>+ . . .	TVR .	.	. , где	V : :	= и и V ф	U
3.	Z =>+ . . . TuxVR .... где V : : = и2 и и = ихи2 (6.3.3)	4. Z ...	V ...	,	где	V ::	= ... TuR	.. .
5.	Z =>+ ...	TV ...	,	где	V : :	= uR . . .
6.	Z =>+ . . . TuiV . . . , где V : : = u2R и и = ихи2
7.	Z =>+ . . . TuV, где V . . . =»+ R
166
ГЛАВА б
Если ни один из этих семи случаев не имеет места, мы знаем, что единственное, что можно сделать в случае . . . TuR .... — это привести и к U.
(6.3.4).	Определение. Правило вида U : : — и является (слева направо) (I,^ограниченно контекстным правилом, если для каждой пары символов Т и R, таких, что Z =>+ ... TuR ... , не имеет места ни один из семи случаев (6.3.3).
(6.3.5).	Определение. Грамматика G является (слева направо) (1 ,\)ограниченно контекстной грамматикой, если все ее правила (слева направо) (1,1)ограниченно контекстны.
Дополнительный термин «слева направо» используется потому, что мы могли бы также определить (1,1 Ограниченный контекст для разбора, проводимого справа налево. Можно было бы даже ввести такое определение, чтобы проводить разбор в любом направлении или же с середины, т. е. можно было бы обнаружить не только основу, но и любую простую фразу.
Заданная ниже грамматика не является (1,1 Ограниченно контекстной, так как правила U : : = с и V : : = с не являются (1,1)ограниченно контекстными; в цепочке abc # по контексту Ьс # невозможно определить, какое из правил, U : : = с или V : : = с, следует применить для редукции самого правого с:
Z:: = abU|cbV U : : = с V : : = с.
,\)ограниченно контекстный распознаватель
Этот распознаватель пользуется единственной таблицей; в каждой строке таблицы три столбца. В первом столбце находится содержимое стека в форме Ти, где Т — любой символ или ограничитель # (который помещается на обоих концах разбираемого предложения), а и — правая часть правила. Во втором столбце находятся соответствующие правые контексты, каждый из которых либо одиночный терминал, либо ограничитель #. В третьем столбце записывается номер правила. Этой таблицей распознаватель пользуется следующим образом. На каждом шаге работы распознаватель среди строк таблицы ищет ту, в которой содержимое первого столбца совпадает с верхней частью стека, а содержимое второго столбца совпадает с последним сканированным символом предложения. Если такая строка найдена, выполняется редукция: основа в стеке заменяется в соответствии с правилом, указанным в третьем столбце. Если такой строки нет, то редукция невозможна; тогда последний сканированный символ помещается в стек и сканируется следующий символ предложения.
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
167
Таблица содержит все возможные конфигурации вида (Tu, R, i), такие, где и в контексте TuR можно редуцировать, используя правило с номером i. Если в любой момент можно найти совпадение не более чем с одной строкой, то всякую редукцию можно выполнить, зная только контекст Т . . . R, т. е. грамматика (^^ограниченно контекстная. На рис. 6.7 показана таблица для такой грамматики:
(6.3.6)	(1) Z : : = V (4) V : : = b U (7) W : : = О W 1
(2)	Z : : - a W	(5) U : : = U 0 (8) W : : = 0 1
(3)	V : : - V 1	(6) U : : = О
Язык, порождаемый этой грамматикой, является объединением двух множеств {aOnln | п > 0} и {bOnlm| п > 0, m 0}. Просматривая таблицу, обратите внимание, что там перечислены все
Рис. 6.7. Таблица ограниченного контекста для грамматики (6.3.6).
возможные комбинации Tu и входных символов R, вызывающие редукцию.
Сам распознаватель изображен на рис. 6.8. В нем, как обычно, используется стек со счетчиком i. Нам предстоит разобрать предложение Ti . . . Тп. Добавим символ Тп+1 = #, ограничитель предложения, и начнем работать, имея в стеке символ #. Для сканирования символов предложения применяется счетчик к.
168
ГЛАВА 6
1.	Установить i : = 1, к : = 1 и S (1): = #.
2.	Если верхние символы стека соответствуют содержимому стека в таблице и если Тk совпадает с правым контекстом, то номер этой строки занести в j и перейти к шагу 3; в противном случае перейти к шагу 4.
3.	В стеке содержится Ти, где Ти задается в первом столбце (содержимое стека) строки j. Привести и к U, используя правило, определяемое третьим столбцом строки j. Перейти к шагу 2.
4.	Если Тк равно #, перейти к шагу 6.
5.	i: = i + 1; S (i) — Tk; k : = k 4- 1; перейти к шагу 2.
6.	Стоп. Предложение правильное тогда и только тогда, когда i: = 2 и S (i) = Z.
Рис. 6.8. (1,1)ограниченно контекстный распознаватель.
Определение (ш, ^ограниченного контекста
Также как мы расширяли (1,1 (предшествование до (ш,^предшествования, можно расширить (1,1 (ограниченный контекст до (т, ^ограниченного контекста. В действительности ни один компилятор не пользуется (ш, п)ограниченным контекстом, хотя в некоторых случаях это было бы возможно. Поэтому при изложении мы останавливаемся только на основных моментах. Описание этого метода включено потому, что грамматики, приемлемые для всех уже описанных распознавателей, равно как и для тех, о которых будет говориться позже, являются (т, п)ограниченно контекстными при некоторых тип.
(6.3.7).	Определение. Рассмотрим правило U : : = и. Для каждой пары цепочек v, w, где |v| = m, |w| = п и Z =>* xvUwy при некоторых хиу, предположим, что и является основой всякой канонической сентенциальной формы . . . vuw ... и что и нужно привести к одному символу U. Тогда такое правило (т ^ограниченно контекстное.
Это означает следующее. Предположим, что U : : = и — ограниченно контекстное правило, и предположим далее, что существует вывод Z =>* xvUwy => xvuwy. Тогда всякий раз, когда при разборе слева направо и снизу вверх в стеке оказывается . . . vu, а в оставшейся части входной цепочки — w . . ., и можно привести к U. Заметьте, что мы не перечисляли случаи, которые не должны иметь места, как это делалось в (6.3.3) для (1, ^ограниченного контекста. Предоставляем это4 читателю.
(6.3.8).	Определение. Грамматика (т,п)ограниченно кон~ текстная, если каждое правило ограниченно контекстное,
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
169
(гл, ^ограниченно контекстный распознаватель и конструктор
Как и в случае (1,1 Ограниченного контекста, в таблице будет три столбца. В первом столбце находится содержимое стека tu, во втором — соответствующий правый контекст v, а в третьем — номер правила, при помощи которого . . . tuv . . . приводится к . . . tUv .... Сам распознаватель, в сущности, остается прежним, но теперь на каждом шаге приходится сравнивать большее число символов.
Как и следовало ожидать, конструктор таблиц довольно сложен. На первом шаге строятся таблицы для (1,^ограниченного контекста. То есть для каждого канонического вывода Z =>* . . . VUW . . . => VuW ... к таблице добавляется строка (если ее раньше не было), в первом столбце которой Vu, во втором столбце — W, а в третьем столбце — номер правила U : : = и. (Напоминаем читателю, что нельзя исследовать все такие выводы, так как их бесконечное количество. Но можно исследовать грамматику для того, чтобы установить, существует ли такой вывод для каждого правила U : : = и и символов V и W.)
Когда таблица уже построена, исследуются все строки для того, чтобы найти пару строк
V, И,	W	п
V2U2	W	тп
где V2u2 является хвостом ViUi (не обязательно правильным).Если таких пар строк нет/ грамматика (1,1)ограниченно контекстная и таблица уже построена. Она (1, 1)ограниченно контекстная потому, что на каждом шаге с верхней частью стека сопоставляется не более чем одна строка.
Однако предположим, что существуют две (или более) такие строки. Тогда, если в стеке находится ViUi, a W — входной символ, для редукции можно использовать любую из этих строк. Нужно изменить таблицу так, чтобы она включала больший контекст справа от W или слева от V2u2. Предположим, например, что мы хотим добавить контекст слева и что все канонические выводы, включающие . . . V2u2W ... и использующие правило ш, имеют вид Z =>* . . . XV2UW . . . => XV2u2W или Z =>* . . . YV2UW . . . => YV2u2W .... Тогда мы можем заменить упомянутые выше строки на следующие:
Vf u(	w	n
XV2u2	w	ТП
VV2u2	w	ТП
170
ГЛАВА 6
Возможно, что такая замена позволит избавиться от неоднозначности. Если нет, придется опять добавить контекст слева или справа. Заметьте, что на каждом шаге следует проверять и менять только те строки, которые вызывают неоднозначность. С остальными все в порядке. Одна из проблем заключается в том, как решить, с какой стороны расширять контекст. На этот счет нет твердых и быстро приводящих к результату рекомендаций. Другая проблема состоит в том, что до тех пор, пока не будет получена непротиворечивая таблица, неизвестно, является грамматика ограниченно контекстной или нет.
6.4.	МАТРИЦЫ ПЕРЕХОДОВ
Одна из проблем, связанных с большинством изложенных методов, состоит в том, что при выборе нужной редукции всегда необходим поиск правила или просмотр таблицы. Конечно, таблицу можно организовать таким образом, чтобы минимизировать время поиска.
* IF
IF <выр>
<перем> ; =
<выр> + ъ
THEN
а
Рис. 6.9. Частично заполненные матрицы переходов.
В описываемом здесь способе работы с этой целью используется большая матрица, иногда называемая таблицей решений или матрицей переходов. Предполагается, что мы имеем дело с операторной грамматикой (ОГ), поскольку именно эта формализация соответствует тому, как, исходя из интуитивных предположений, применялись матрицы переходов. Проиллюстрируем этот способ на примере такой грамматики.
<прог> : : = <инстр>
<инстр> : : = IF <выр> THEN <инстр>
(6.4.1)	<инстр> : : = <перем> : = <выр>
<выр> : : — <выр> + <перем> | <перем>
<перем> : : = i
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
171
Составляем матрицу переходов М; ее строки соотносятся с теми головами (оканчивающимися терминальными символами) правых частей правил, которые могут появиться в стеке, а столбцы соотносятся с терминальными символами, включая и ограничитель предложений # (рис. 6.9, а). Элементами матрицы будут номера или адреса подпрограмм.
Распознаватель пользуется обычным стеком S и переменной R, принимающей значение входных символов. Однако структура стека несколько отличается от привычной. Элементами стека являются не символы, а цепочки символов. Появляющиеся здесь цепочки — это головы (оканчивающиеся терминальным символом) правых частей правил. Например, если в обычном стеке в какой-то момент содержится
# IF <выр> THEN IF <выр> THEN <перем> :== то этот стек будет выглядеть так:
<перем> : = 1F <выр> THEN IF <выр> THEN
Заметьте, что все сводится к совместному хранению тех символов, о которых известно, что они должны редуцироваться одновременно. И, наконец, нам понадобится третья переменная U. Она либо будет пуста, либо будет содержать тот символ, к которому была приведена последняя первичная фраза. Следовательно, если частичная цепочка, еще не разобранная до конца, выглядит так:
# IF <выр> THEN IF <выр> THEN <перем> : = <выр>, то стек будет таким, каким он изображен выше, а в U будет <выр>.
Распознаватель использует матрицу следующим образом. На каждом шаге работы верхний элемент стека соответствует некоторой строке матрицы, так как в обоих случаях речь идет о голове (заканчивающейся терминальным символом) некоторой правой части. По входному терминальному символу R определяется элемент матрицы, являющийся номером подпрограммы, которую нужно выполнить. Эта подпрограмма произведет необходимую редукцию или занесет R в стек и будет сканировать очередной исходный символ.
Например, в самом начале в стеке находится # и U пусто. В соответствии с грамматикой (6.4.1) входным символом должен быть либо IF, либо i. Поскольку с них начинаются правые части, необходимо занести их в стек и продолжить работу. Тогда первой подпрограммой (рис. 6.9, а) будет такая:
1: IF U =/= ' ' THEN ERROR; i : = i + 1; S (i) = R ; SCAN, где SCAN означает «поместить очередной символ из входной цепочки в R». Смысл сравнения U с пробелом сейчас будет пояснен.
172
ГЛАВА 6
Предположим, что i — верхний элемент стека. Какие символы из тех, которые могут оказаться' в R, считаются допустимыми? За i может следовать один из символов: #, THEN, : = или +. В любом из этих случаев необходимо заменить i на <перем>, т. е. выполнить редукцию . . . <перем>	i .. . . В подпро-
грамме 2 (рис. 6.9,Ь) именно это и делается: .
2: IF U ' ' THEN ERROR; i : = i — 1; U : = '<перем>' .
Тогда каждый элемент матрицы является номером подпрограммы, которая либо заносит входной символ в стек, либо выполняет редукцию. В связи с тем что изменился вид элемента стека, операция занесения в стек стала несколько более сложной. Полная матрица и подпрограммы для грамматики 6.4.1 приведены на рис. 6.10. Читателю предлагается, используя матрицу и подпрограммы, провести разбор предложения: # IF i + i THEN i : = i #.
Преимущества и неудобства матриц переходов
Этот метод использовался в нескольких ранних компиляторах с АЛГОЛа, причем все матрицы были построены вручную в основном тем же способом, которым мы только что пользовались. Элементы матрицы заполнялись так: по структуре языка делался вывод о том, каким может быть следующий входной символ и какое действие следует в соответствии с этим предпринять. Используя матрицу, автор транслятора мог концентрировать свое внимание только на одной конструкции одновременно. Это позволяло разделить одну большую задачу на ряд мелких подзадач.
Способ этот гарантирует быструю работу, так как полностью исключается поиск; каждой подпрограмме точно известно, что она должна делать. Другим удобством является легкость в нейтрализации ошибок. Каждый нуль в матрице соответствует синтаксической ошибке, и так как обычно больше половины элементов — нулевые, можно написать много подпрограмм для обработки этих ошибок и допустить несколько способов нейтрализации. Ни для каких других методов хороших способов нейтрализации ошибок не найдено, потому что при нейтрализации совсем не просто выделить частные случаи. Одно из неудобств, связанных с использованием матриц переходов, — большой объем требуемой памяти. Например, в типичном компиляторе с АЛГОЛа для IBM 7090 используется матрица 100 x 45 и приблизительно 250 подпрограмм.
Построение аугментной операторной грамматики
До сих пор обсуждалось, как работает построенная по интуитивным соображениям матрица переходов. Теперь остановимся на основных особенностях построения матрицы. Все доказательства
	Т Н IE : # FN=+i
	5 1 0 6 0 1
IF	0 0 9 0 3 1
IF <выр> THEN	7 1 0 6 0 1
<перем>: =	8 0 0 0 3 1
<выр>+	4 0 4 0 4 1
i	2 0 2 2 2 0
1:	IF U=#" THEN ERROR;
i := i + 1; S(i):=R; SCAN.
2:	IF U#=''THEN ERROR;
i:=i — 1; U :='<перем>
3:	IF U =/= '<выр>' OR
U#= '<перем>' THEN ERROR;
i := i + 1; S(i) :='<выр> + ';
U:=''; SCAN.
4:	IF U^='<перем>'THEN ERROR; i:=i — 1; U:='<Bbip>'.
5:	IF U =#='<npor>'OR
U #='<инстр>'THEN ERROR; STOP.
6:	IF U#= '<перем>' THEN ERROR; U:=' '; i: = i + l;
S (i) :='<перем> :='; SCAN.
7:	IF и#='<инстр>'
THEN ERROR;
i:=i — 1; U: = '<инстр>'.
8:	IF '<перем>'OR
и#='<выр>'
THEN ERROR;
i:=i—1; U:= '<инстр>'.
9:	IF U#='<перем>'OR и=#'<выр>'
THEN ERROR;
S (i) :='IF <выр> THEN';
U:="; SCAN.
0: ERROR; STOP.
Рис. 6.10. Матрица и подпрограмма для грамматики (6.4.1).
174
ГЛАВА 6
предоставляются читателю в качестве упражнений. Помните, что формальный подход очень близок к интуитивному.
Как и в случае предшествования операторов, возьмем вначале ОГ. Предположим далее, что любой вывод одного нетерминального символа из другого является единственным. Таким образом, если Ui =>+ Uj, то такой вывод — единственный.
Первый шаг — замена грамматики другой, эквивалентной ей грамматикой, в которой в правой части каждого правила содержится не более трех символов. Для этого введем новые нетерминальные символы, которые будем называть выделенными нетерминальными символами (ВНТС); предполагается, что их можно отличить от первоначальных невыделенных нетерминальных символов (ННТС). Для этого ВНТС будем подчеркивать. Далее, каждый ВНТС построен так, что он является головой (оканчивающейся терминальным символом) правой части некоторого правила исходной ОГ и таким образом соотносится с одной из строк описанной ранее матрицы переходов. Эту новую грамматику назовем аугментной операторной грамматикой (АОГ). Она необязательно должна быть ОГ.
Построение происходит следующим образом: шаг 1 выполняется столько раз, сколько возможно; то же самое относится и к шагу 2. Наконец, до тех пор, пока это возможно, попеременно повторяются шаг 3 (а) и шаг 3 (Ь). Здесь Т — терминальный символ, U — нетерминальный и U — ВНТС.
1.	Если есть правило Ui : : = Tyi (цепочка yi может быть пустой) и если уже существует к —- 1 ВНТС U1} . . . , Uk~i, образовать новый BHTCUk, заменить все правила Uf. : = Tyj (т. е. все правила, начинающиеся с одного и того же Т) правилами Up : = Ukyi и добавить правило Uk :: = Т.
2.	Если есть правило Ui :: = UTyi и уже существует к — 1 ВНТС, то образовать новый ВНТС Uk, заменить каждое правило Ui :: = UTyi (т. е. все правила, начинающиеся одной и той же последовательностью символов UT) правилами Ui :: = Ukyi и добавить правило Uk :: = UT.
3.	(а). Если есть правило Ui : : = UTyi и уже существует к —- 1 ВНТС, то образовать новый ВНТС Uk, заменить каждое правило U4 :: — UTyi правилом Ui :: = Ukyi и добавить новое правило Uk :: — UT.
(Ь) Если есть правило Ui ::=UUTyi и уже существует к — 1 ВНТС, то образовать еще один ВНТС Uk, заменить каждое правило Ui= UUTyi правилом Ui:: — Ukyi и включить новое правило Uk = UUT.
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
175
Заметив, какую форму принимают правила после выполнения каждого шага, читатель должен доказать такую лемму (упражнение 1).
(6.4.2).	Лемма. Любое правило в АОГ представлено в одной из следующих семи форм:
U1::=U2, U/.^U, U(::=UU2,
U::=T,	U::=UT, U^^U/T,	U^^UjUT.
Кроме того, читателю предлагается выполнить описанное построение для грамматики (6.4.1) (упражнение 2). И, наконец, читатель должен доказать две следующие леммы и теорему.
(6.4.3).	Лемма. Для каждого правила грамматики ОГ U : : = у, где у не является нетерминалом, существует такое единственное множество правил Ui: : = у0, U2: : = Uiyi. . . , Un: : = Un-iYn-i, U : : = Unyn “грамматики’ АОГ, “что у = уоух. . .уп.
(6.4.4).	Лемма. Для каждого из отличающихся друг от друга синтаксических деревьев сентенциальной формы s ОГ можно построить другое синтаксическое дерево для s согласно АОГ следующим образом: заменить каждый куст, соотносящийся с применением правила Uj:: = у, множеством кустов, соотносящихся с правилами, описанными в лемме (6.4.3).
(6.4.5).	Теорема. Если АОГ однозначна, то таким же свойством обладает и соответствующая ей ОГ.
<инсшр>
<if -lhen>
<инсшр>	f..............
<и>	:
Г-----j----1---1-------|	j	I
IF <ewp> THEN <UHCinp> IF <выр> THEN <инстр> a	b
Рис. 6.11. Кусты для ОГ и соответствующей ей АОГ.
В качестве иллюстрации к построению, о котором говорилось в лемме (6.4.4), заменим куст дерева (рис. 6.11, а) на поддерево (рис. 6.11 ,Ь). Речь идет о грамматике (6.4.1). На рисунке <if> и <if-then> — ВНТС. Читатель должен сам убедиться в том, что восходящий разбор, проведенный в соответствии с АОГ, по своей сути не отличается/от разбора, проводимого в соответствии с ОГ; появляются, правда, некоторые дополнительные промежуточные редукции.
176
ГЛАВА 6
Разбор в АО Г
При восходящем разборе на каждом шаге распознается самая левая первичная фраза АОГ, где первичная фраза АОГ определяется как фраза, которая не содержит никакой другой первичной фразы АОГ, но содержит по крайней мере один терминал или ВНТС. Читатель должен сравнить это определение с определением (6.1.7). Можно сказать и так, что у является первичной фразой некоторой сентенциальной формы xyz в АОГ, если существует такая сентенциальная форма xVz (где V — ВНТС или ННТС), что V : : = у или V : : = У1, yi =>* у и все непосредственные выводы в последовательности yi =>* у следуют из правил Up : = U< для ННТС Ut и Uj.
Из леммы (6.4.2) следует, что первичная фраза АОГ должна иметь одну из следующих шести форм:
(6.4.6)	U UT Т UU UT UUT
Теперь читателю предстоит доказать следующее (упражнение 6):
(6.4.7).	Теорема. При разборе предложения из АОГ каждая редукция, которая редуцирует самую левую первичную фразу АОГ, имеет одну из следующих шести форм:
uUt => uUt	иСЦ => uU2Tt	uUtt => uTt
uUjt=>uUU2t	uUjt=>uU2UTt	uUt=>uUTt
где u — (возможно, пустая) цепочка ВНТС, t — цепочка терминальных символов, U, Ui, U2 — ННТС, a U, Ui и U2 — ВНТС. Следовательно, при разборе любая сентенциальная форма имеет вид ut или uUt.
В алгоритме разбора используется стек S, в котором находится подцепочка и сентенциальной формы u[U]t. Если есть символ U, он будет значением переменной U; в противном случае значением переменной будет пустой символ. Матрица содержит по одной строке для каждого ВНТС и по одному столбцу для каждого терминала. Заметим, что так как каждый ВНТС соответствует некоторой строке, в стеке он может быть представлен номером этой строки.
На каждом шаге разбора верхний символ стека (ВНТС) и очередной входной символ определяют элемент матрицы. Там записан номер подпрограммы, которую нужно выполнить. Подпрограмма проверяет значение переменной U и предпринимает соответствующее действие, выполняя одну из шести возможных редукций. Сейчас мы перечислим достаточные условия, выполнение которых позволяет только по верхнему элементу стека, входному терминалу и переменной U определить, какую именно редукцию производить на каждом шаге.
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
177
Достаточные условия для однозначной АО Г
Определим некоторые условия, из которых следует однозначность АОГ. Более того, эти условия позволяют нам на каждом шаге разбора находить самую левую первичную фразу АОГ и тот символ, к которому она должна быть приведена. Известно, что все сентенциальные формы имеют вид u_ [U] t (квадратные скобки означают, что символ U может и отсутствовать). Условия позволят нам, судя только по хвостовому символу цепочки и, головному символу цепочки t и переменной U, определять, какую сделать редукцию, если она возможна.
(6.4.8).	Определение. АР (S) — это множество допустимых предшественников символа S: АР (S) : = {Si | . . . SiS ... — сентенциальная форма}.
Условие 1. Для каждой пары U, Т, где U — ВНТС, а Т — терминал, справедливо не более чем одно из следующих утверждений: (6.4.9)	Есть	правило U : : = U и U	£	АР (Т).
(6.4.10)	Есть	правило Ui: : = UT.
(6.4.11)	Есть	правило Ui: : = Т, и U	€	АР (Ui).
Более	того,	если имеет место (6.4.9),	существует только один
такой ННТС U.
Условие 2. Для каждой тройки U, U и Т, где U — ВНТС, U — ННТС, а Т — терминал, справедливо не более чем одно из следующих утверждений:
(6.4.12)	Есть	правило U,: : = UU2, где Ui € АР (Т) и	U2=>*U.
(6.4.13)	Есть	правило Ui: : = UU2T, где U2=>*U.
(6.4.14)	Есть	правило Uj: : = U2T, где U ё АР (Ux) и	U2=>*U.
Более того, если имеет место (6.4.12), Ui и U2 единственны, а в остальных двух случаях единствен символ U2.
Предоставляем читателю доказательство следующей теоремы (упражнение 7):
(6.4.15).	Теорема. Грамматика АОГ, удовлетворяющая условиям 1 и 2, однозначна.
УПРАЖНЕНИЯ К РАЗДЕЛУ 6.4
1.	Докажите лемму (6.4.2).
2.	Постройте АОГ для грамматики (6.4.1). Вначале, на первом шаге, второе правило заменяется двумя правилами: Ux: : = IF и
178
ГЛАВА 6
<инстр> : : == Ui <выр> THEN <инстр>, а последнее правило заменяется на Ъ2: : = i, <перем> : : — U2.
3.	Докажите лемму (6.4.3). (Доказательство леммы легко следует из построения.)
4.	Докажите лемму (6.4.4).
5.	Докажите теорему (6.4.5). Указание-, покажите, что если синтаксическое дерево некоторой сентенциальной формы единственно в АОГ, это же свойство сохраняется у соответствующего дерева в ОГ.
6.	Докажите теорему (6.4.7). Указание: обратите внимание на шесть возможных форм первичной фразы АОГ и соответствующие им редукции.
7.	Докажите теорему (6.4.15).
6.5. ИСТОРИЧЕСКИЕ ЗАМЕЧАНИЯ
Идея предшествования операторов была формализована Флойдом [63], но методы, основанные на этой идее, интуитивно использовались уже в ранних компиляторах. Соответствующие ссылки можно найти в работах Алларда и др. [64], Галлера и Перлиса [67] и Гриса [65]. Простое предшествование, предложенное Виртом и Вебером, использовалось в компиляторе с языка АЛГОЛ W (см. Бауэр, Беккер и Грехем [68]). Вирт и Вебер также предложили (т, п)предшествование, но их определения неверны. Маккиману [66] принадлежит идея расширения обычного предшествования до (1,2)(2,1)предшествования. Позднее эта идея была уточнена и использовалась в разработанной Маккиманом и др. [70] системе написания компиляторов. Колмерор [70] определил технику предшествования, включающую в себя как частные случаи простое и операторное предшествования. Здесь, говоря кратко, разработчик компилятора сам выбирает операторы. Имеются некоторые ограничения, так, например, все терминалы должны быть операторами. Если остановить выбор на терминальных символах, случай сводится к предшествованию операторов, если же отнести к операторам все символы, получается простое предшествование.
Некоторые исследователи пытались расширить простое предшествование так, чтобы повысить его практическую ценность. Например, Ихбия и Морз [70] определили отношения «слабого предшествования», чтобы для поиска головы основы в стеке вместо отношений использовать сопоставление с образцом. Это очень походит на то, что мы делали в случае (1, 2)(2, 1)предшествования. Циммер [70] в своей работе смягчает требование единственности правых частей, а для того, чтобы установить, какое из правил употребить при редукции, разрешает использовать контекст. Это уже походит на (1,1)ограниченный контекст.
ДРУГИЕ ВОСХОДЯЩИЕ РАСПОЗНАВАТЕЛИ
179
Идея об ограниченном контексте уровня (1,1) имеет корни в работе Пола [62] и впервые упоминалась Эйкелом и др. [63]. Расширение до (т, ^ограниченного контекста встречается у Флойда [64] и Айронса [64]; однако они ввели разные определения. Мы следовали формализации Флойда. Две последние работы были представлены на Конференции по структурам формализованных языков, состоявшейся в августе 1963 года. Материалы этой конференции помещены в февральском номере САСМ за 1964 г. и стоят того, чтобы с ними познакомиться. Реальный способ построения таблиц для (т, п) контекстно ограниченного распознавателя, работающего слева направо, был впервые предложен Эйкелом [64]. Эту статью не только трудно читать, но ее нелегко и найти. Лёкс [69] предложил для построения таблиц новый, гораздо более понятный алгоритм.
Окончательным результатом, к которому привели все исследования по распознаванию слева направо и снизу вверх, явились LR (к)грамматики и языки, введенные Кнутом [65]. Кнут показывает, как построить для LR (к)грамматики распознаватель, которому при выборе редукции доступны все символы в стеке и к символов справа. Конечно, в действительности не нужно просматривать весь стек на каждом шаге работы. Вместе с каждым символом стека хранится «состояние», содержащее информацию об остальных элементах стека, достаточную для того, чтобы принять решение.
Следует упомянуть о том, что LR (к)грамматика или по крайней мере ее слегка ограниченный вариант, предложенный Деремером [70], может быть достаточно эффективным для практического использования в компиляторах. Хорнинг [701 отмечает, что при хорошей реализации распознаватель для LR (к)грамматики может работать быстрее и занимать меньше места в памяти, чем эквивалентный (1, 2)(2, ^распознаватель.
Матрицы переходов, впервые описанные Замельзоном и Бауэром [60], использовались в нескольких компиляторах с языка АЛГОЛ, написанных группой АЛГОЛ (см. Грис [65]). Сотрудники NELIAC (см. Халстед) также применяли матрицы переходов под названием СО-NO таблиц (текущий оператор и следующий оператор), но без стеков. Эта техника была формализована Грисом [68].
Здесь необходимо упомянуть о других формализациях процесса синтаксического распознавания, хотя и не все перечисляемые работы используют восхождение. Линч [68] определил грамматики типа ICOR и дал для них алгоритм распознавания. Алгоритм До-мелки описывается в статье Хекста и Робертса [70]. Джилберт добавляет к контекстно зависимым грамматикам функцию выбора, указывающую (для текущей сентенциальной формы), какое правило употребить при редукции. Таким образом, эта грамматика скорее является синтетической, чем аналитической, ее можно использовать как алгоритм разбора без предварительной обработки конструктором.
180
ГЛАВА 6
Тиксир [67] рассматривает множество правил для любого нетерминала как регулярное выражение, определяющее для этого нетерминала множество цепочек. Можно построить конечный автомат для того, чтобы распознавать цепочки, относящиеся к каждому нетерминальному символу. Задача состоит в том, чтобы установить, во-первых, в какой момент можно переключаться с одного конечного автомата на другой, и, во-вторых, на какой из них переключиться. Например, если есть правило V : : = Ах | By, на что следует переключаться — на автомат, распознающий цепочку из множества А, или на автомат для множества В? Тиксир некоторым образом ограничивает допустимые в его конструкторе грамматики для того, чтобы решать такие проблемы, используя ограниченный контекст.
У всех описанных до сих пор распознавателей не было выходной информации, т. е. они распознавали предложения некоторой грамматики, но не переводили их ни в какую другую форму. Эта работа выполняется так называемыми семантическими программами, о которых речь пойдет в гл. 12. Льюис и Стирнз [68] определяют синтаксически ориентированные трансляторы, которые не только распознают предложения, но и переводят их в некоторую другую форму.'Эта формальная модель не является настолько общей, чтобы ее можно было действительно использовать при конструировании компиляторов.
Стирнз и Льюис [68] и независимо от них Витни расширяют контекстно свободные грамматики таким образом, чтобы на каждом шаге разбора был возможен доступ к таблице. Это делается для того, чтобы формализовать просмотр идентификаторов в таблице, а также их проверку и изменение их атрибутов. Таким образом, возможна дальнейшая формализация определения языка и как следствие облегчение процесса разработки компиляторов.
Глава 7
Продукционный язык
Продукционный язык (ПЯ) — это язык программирования, предназначенный для написания синтаксических распознавателей. Программа в основном состоит из последовательности продукций. Употребление здесь этого слова надо считать довольно неудачным, так как обычно с этим словом связывают другой смысл, обозначая им правила подстановки в грамматике. Эти новые продукции лучше было бы называть редукциями, так как они (обычно, но не всегда) используются для приведения предложений к начальному символу грамматики. Не станем, однако, изобретать новый термин и будем придерживаться слова «продукция», которое постоянно употребляется в литературе.
ПЯ обычно входит в состав системы построения трансляторов (т. е. системы, предназначенной для реализации таких программ, как ассемблеры или компиляторы), и любой программе на ПЯ предшествует определение сканера, во многом похожего на сканер, описанный в разд. 3.4. Таким образом, программа на ПЯ будет иметь дело с внутренним представлением символов исходного языка — служебными словами, идентификаторами и т. п., а не с настоящими литерами.
Обозначения, принятые в ПЯ, неодинаковы в разных реализациях. Поэтому не следует думать, что те обозначения, которые встретятся здесь, единственны или наиболее удачны. Нас главным образом интересует, что может делать написанная на ПЯ программа, а не то, как пробить ее на перфокартах.
7.1. ЯЗЫК
Продукции
Программа, написанная на ПЯ, автоматически использует стек, называемый SYN, в точности такой же, как во всех других распознавателях, рассмотренных ранее. В стеке могут содержаться символы исходного языка или объекты, подобные нетерминалам. Сама
182
ГЛАВА 7
программа в основном состоит из каждая из которых имеет вид
[(метка> :] (символ) {(символ)} i-левая часть________________I
последовательности продукций, [-► {(символ)}] {(действие)} L-правая часть—j
В данном определении (символ) — это символ исходного языка или идентификатор (возможно, заключенный в угловые скобки (и», называемые нетерминалами, в то время как (метка) — это любой обычный идентификатор. Как мы уже видели, все символы, непосредственно следующие за (метка), если она есть, образуют левую часть продукции, в то время как правая часть состоит из стрелки и любых следующих за ней символов.
Продукция задает сопоставление с образцом и преобразование стека. При выполнении программы, написанной на ПЯ, всегда есть текущая продукция. Когда продукция становится текущей, (символ) ее левой части сравниваются с верхними символами, находящимися в стеке SYN. Если полного соответствия нет, текущей становится следующая продукция из последовательности, и вновь производится сравнение. Это продолжается до тех пор, пока не произойдет совпадения. Когда же это случится, совпавшие символы стека будут заменены символами из правой части текущей продукции. (Если в этой продукции отсутствует правая часть, в стеке не произойдет никаких изменений.) Затем, если есть (действие), они выполняются в заданном порядке. В частности, такие действия могут вызвать сканирование и занесение в стек символов исходной программы, а также могут указать, какая из продукций будет следующей текущей продукцией.
(7.1.1).	Пример. Предположим, что продукция
УСЛ: IF (Е) THEN-> (ICL) SCAN GO THENPART текущая. Если верхним, вторым и третьим элементами стека являются соответственно символы THEN, (Е) и IF, произойдет следующее:
1.	Из SYN исключаются три верхних элемента.
2.	В стек помещается символ (ICL).
3.	Выполняются два действия:
a)	SCAN — сканируется и помещается в стек очередной символ исходной программы;
b)	GO THENPART — текущей становится продукция с меткой THENPART.
Переходя к терминам правил подстановки, можно сказать, что мы привели IF (Е) THEN к (ICL), используя правило (ICL) : : = IF (Е) THEN.
ПРОДУКЦИОННЫЙ ЯЗЫК
183
На рис. 7.1 изображены другие примеры продукций, которые мы рассмотрим позже. Это распознаватель арифметических выражений, построенных в соответствии с синтаксисом (7.1.2)
ST: #	SCAN	Поместить первый сим-
LF: I	F	SCAN	GO LT вол в стек. Проверить
' N	F	SCAN	GOLT 1-й символ F (I,N или ()
(	SCAN	GO LF и выбрать соответству-
		ющее продолжение.
ANY	HALT 2	Ошибка — стоп.
LT: Т * F	ANY->T ANY GO LT1	Редукция Т: :=T*F.
F ANY	T ANY	Редукция T::=F.
LT1: T *	SCAN	GO LF Проверка, * ли. Если
		да, набор последую-
		щего F.
LE: E 4- T	ANY E ANY GO LEI	Эти три продукции соот-
E — T	ANY E ANY GO LEI	ветствуют редукциям:
T ANY	-> E ANY	Е:Е+Т, Е:Е—Т
		и Е::=Т.
LEI: E +	SCAN	GO LF Решить, что же делать
E —	SCAN	GO LF с Е.
( E )	F	SCAN	GO LT Редукция F::=(E).
# E #	-> # Z # HALT 0	Редукция Z:: = Е, стоп.
ANY	HALT 3	Ошибка — стоп.
Рис. 7.1. Программа на ПЯ для грамматики 7.1.2.
Метасимволы
Метасимвол ANY часто встречается в продукциях в качестве (символ >. По смыслу, однако, он отличается от обычного нетерминала; когда символ ANY встречается в левой части, он всегда совпадает с соответствующим символом стека.
В левой части символов ANY должно быть не меньше, чем в правой. Не упуская из виду то обстоятельство, что любой (символ) из правой части помещается в стек, мы приписываем символу ANY из правой части следующий смысл: i-й (считая справа налево) символ ANY в правой части и есть тот находящийся в данный момент в стеке символ, который сопоставляется с i-м символом ANY из левой части. Поясним это двумя примерами.
Пусть текущая продукция
Т * F ANY Т ANY GO LE
и в стеке содержится Т « F + . Тогда содержимое стека сопоставляется с символами в левой части продукции. Символом ANY в правой части стал символ + , совпавший с символом ANY из ле
184
ГЛАВА 7
вой части; содержимое стека заменится на Т+. Текущей станет продукция с меткой LE.
Если в стеке содержится W X Y Z, а текущей является продукция ANY Y ANY -> ANY U ANY V, то содержимое стека будет заменено на W X U Z V.
Есть еще два метасимвола — I, который совпадает с любым полученным от сканера идентификатором, и N, который совпадает с любым целым числом.
Действия
После того как левая часть совпала с символами стека и содержимое стека было преобразовано, выполняются действия. Два наиболее важных — SCAN и GO — нам уже встречались. Выполнение действия SCAN требует вызова сканера. Он сканирует следующий исходный символ, помещает его в качестве верхнего символа в синтаксический стек и возвращается. Выполнение действия GO (метка) приводит к тому, что помеченная меткой продукция становится текущей и начинается сравнение с содержимым стека.
Позднее, в подходящий момент, мы объясним действия CALL, RETURN и EXEC. К другим общепринятым действиям относятся: ERROR (целое) и HALT (целое). Первое из них вызывает печать сообщения «ERROR (целое)», где (целое) — это целое число на стандартном устройстве вывода. Второе вызывает печать сообщения «HALT (целое)», после чего прекращается синтаксический разбор.
Пример
На рис. 7.1 представлены продукции, которые позволяют распознать предложения грамматики (7.1.2). Мы здесь опять предполагаем, что сканер распознает идентификаторы I и целые числа N. Мы предполагаем также, что к началу работы программы система поместила в стек ограничитель # и что ограничитель # встретится также и в конце предложения.
Z :: = Е
Е::=Т|Е4-Т|Е—Т
T::=F|T*F
(7.1.2)	F::=I|N|(E)
1::=буква {буква | цифра)
N цифра {цифра}
Прежде чем мы воспользуемся рис. 7.1 для разбора предложения, рассмотрим его несколько подробнее. Заметим, что продукция LF + 2 не имеет правой части. Это означает, что в случае совпаде
ПРОДУКЦИОННЫЙ язык
185
ния содержимое стека не изменяется. Заметим также, что в продукции LT + 1 вообще не производится никаких действий. Если в продукции нет действия GO, то после выполнения всех действий текущей становится следующая продукция (в данном случае LT1).
Смысл программы можно описать следующим образом. В продукции ST всегда происходит совпадение, после чего сканируется
Текущая	Содержимое	Совпадение	Очередная
продукция	стека	в продукции	продукция
ST		ST	LF
LF	# (	LF + 2	LF
LF	# (I	LF	LT
LT	# (F +	LT+1	LT1
LT1	# (T +	LE+2	LEI
LEI	# (E +	LEI	LF
LF	# (E + I	LF	LT
LT	# (E + F)	LT 4-1	LT1
LT1	# (E + T)	LE	LEI
LEI	# (E )	LE14-2	LT
LT	# F*	LT + 1	LT1
LT1		LT1	LF
LF	#T*I	LF	LT
LT	#T*F#	LT	LT1
LT1		LE + 2	LEI
LEI	#E#	LE1+3	HALT 0
Рис. 7.2.	Разбор предложения (A-f-B)*C, согласно		рис. 7.1.
первый символ предложения. Продукции от LF до LF + 2 проверяют, может ли этим символом начинаться множитель F; если нет, то продукция LF + 3 выдает сообщение об ошибке. Если множителем оказывается I или N, то они приводятся к F, затем сканируется следующий символ и текущей становится продукция LT. Если множитель начинается с левой круглой скобки, продукция LF + 2 сканирует первый символ выражения внутри скобок и вновь переходит к LF.
Важен порядок продукций LT и LT + 1.В этот момент в стеке обязательно содержится F ANY, так что продукция LT + 1 обязательно заменит F на Т (правило подстановки Т : : = F). Однако, прежде чем это произойдет, надо проверить, не применимо ли правило Т ::= Т * F. После того как выполнена одна из этих редукций, мы смотрим, не находится ли в стеке *. Если да, то мы готовимся в дальнейшем выполнить редукцию Т : : = Т * F и с этой
186
ГЛАВА 7
целью сканируем следующий символ, а затем возвращаемся к LF, чтобы найти фразу для F. Если * в стеке нет, мы находим подходящую редукцию для Е и выполняем ее. Здесь также важен порядок расположения продукций от LE до LE + 2.
Последней выполняется продукция LE1 +3, в которой проверяется наличие завершающего ограничителя # и Е приводится к Z.
На рис. 7.2 показан разбор предложения (А + В) * С. В каждой строке указаны первая продукция, ставшая текущей после последнего совпадения, содержимое стека в этот момент, продукция, в которой происходит следующее совпадение, и, наконец, очередная текущая продукция.
Названия классов .
Продукции LE и LE + 1 на рис. 7.1 отличаются только тем, что в одной из них написан +, а в другой —- . Чтобы сократить такую запись, мы.предваряем все продукции описанием
CLASS ПМ + -
и заменяем две продукции одной
LE: Е ПМ Т ANY -> Е ANY GO LEI
Вообще говоря, описание вида
CLASS (название класса) (символ) {(символ)} ставит в соответствие символам (название класса), которое может быть любым удобным идентификатором. Если название класса встречается в левой части, оно совпадает с любым из символов, соответствующих ему по описанию.
Если название класса встречается в правой части продукции, то оно должно встречаться в левой части по меньшей мере столько же раз, сколько в правой. Смысл (название класса) в правой части аналогичен смыслу метасимвола ANY в правой части. Название класса, встречающееся в правой части i-й раз (считая справа налево), — это тот, находящийся в данный1! момент в стеке символ, который совпал с i-м вхождением данного названия класса в левой части. Например, для уже упоминавшегося описания: если в стеке содержится Н---А + , а текущая продукция
ПМ А ПМ -> В С ПМ ПМ
содержимое стека изменится на + В С — +. Заметьте, что эта продукция эквивалентна четырем продукциям:
+	А	+	->	В	С	+	+
+	А	—	—>	В	С	+	—
_ А + В С — +
 —	А	—	—+	В	С	—	—
ПРОДУКЦИОННЫЙ язык
187
В разд. 7.3 мы обсудим, какие преимущества дает установление соответствия между так называемыми номерами семантических подпрограмм и символами в классе. Чтобы связать подпрограмму номер 20 с символом + , а подпрограмму номер 2 с символом —, перепишем описание ПМ так:
CLASSNO ПМ +20	-2
Такое название класса можно употреблять так же, как и прежнее; об использовании дополнительных возможностей речь пойдет в разд. 7.3. Вот формат этого нового описания:
CLASSNO <название класса) <символ> (целое> {(символ) (целое)}
Использование названий классов не делает ПЯ более мощным языком, т. е. не появляется никаких принципиально новых возможностей. Однако применение названий классов упрощает программирование на ПЯ и позволяет сократить размеры программ.
Противоречия между исходным языком и символами ПЯ
Если -> является символом исходного языка, то как тогда понимать продукцию
(7.1.3)	ANY ANY -> В
Ясно, что она неоднозначна. Для решения проблемы неоднозначности (и других проблем) мы условимся, что любому символу исходного языка, содержащему одну из литер -> $ ; или #, или являющемуся одним из таких символов ПЯ, как CLASS, CLASSNO, I, N, ANY, CALL, ERROR, EXEC и GO, должен предшествовать знак доллара $. Таким образом, знак доллара является специальным символом (авторегистром), обозначающим, что следующие за ним литеры (вплоть до ближайшего пробела) составляют один символ исходного яыка.
Используя это обозначение, выпишем три разные возможные интерпретации продукции (7.1.3):
ANY	-+	ANY	>	В
ANY	ANY	S—	В
ANY	ANY	—>	В
Еще одйн пример: в продукции
LABE: I $: $$ -> (метка)
произойдет совпадение, если первым (верхним) элементом стека является символ «$», вторым — символ « : », а третьим — некий идентификатор; эти элементы будут заменены одним элементом (метка).
188
ГЛАВА 7
Краткое изложение синтаксиса языка
Программа на ПЯ <программа> <описание>	имеет следующий синтаксис: {(описание)} {(продукция)} :: = CLASS (название класса> (символ) {(символ)} | CLASSNO (название класса) (символ) (целое) {(символ) (целое)}
<продукция>	{(метка):] (левая часть) [(правая часть)] {(действие)}
<левая часть> <правая часть>	:: = (символ) {(символ)} :: = —>(символ)
где
1.	Каждая <метка> и каждое <название класса > —это несовпадающий с другими обычный идентификатор, который не является ни символом исходного языка, ни символом ПЯ.
2.	Каждый <символ> — это либо
а)	один из метасимволов I, N или ANY, либо
Ь)	несовпадающий с другими обычный идентификатор, возможно, заключенный в угловые скобки < и > и не являющийся ни символом ПЯ, ни символом исходного языка, либо
с)	символ исходного языка, не содержащий литер : -> ; $ или # и не являющийся символом ПЯ, либо
d)	любая последовательность отличных от пробела литер, которой предшествует $.
3.	Любое <целое> — это непустая последовательность из цифр от 0 до 9.
4.	Символами ПЯ являются ANY, CALL, CLASS, CLASSNO, EXEC, GO, I и N.
5.	(Действия) и их смысл таковы:
a)	CALL (метка>. Выполнять продукции, начиная с помеченной (метка) и продолжая до тех пор, пока не будет выполнено действие RETURN. Затем продолжить выполнение со следующего действия или со следующей продукции. Таким образом, это просто вызов подпрограммы; пример приводится в разд. 7.2.
b)	ERROR (целое). Напечатать “ERROR (целое)”.
с)	ЕХЕС (целое). Выполнить семантическую подпрограмму с меткой (целое). Закончив, перейти к следующему действию. (Более подробное объяснение мы дадим в разд. 7.3.)
d)	EXEC (название класса). Это (название класса) должно присутствовать в левой части данной продукции. Его описание должно связывать семантические подпрограммы с сим
ПРОДУКЦИОННЫЙ язык
189
волами. Выполняется семантическая подпрограмма, связанная с символом, находящимся в стеке и совпавшим с Название класса) в левой части. (Подробное объяснение см. в разд. 7.3.)
е)	GO <метка>. Сделать текущей продукцию, помеченную меткой, и вновь приступить к проверке на совпадение. Любые другие действия, следующие за данным, никогда выполнены не будут.
f)	HALT <целое>. Напечатать сообщение “HALT <целое>” и прекратить выполнение программы.
g)	RETURN. Вернуться в точку программы, расположенную непосредственно вслед за последним выполненным действием CALL [см. п. а)].
h)	SCAN. Вызвать сканер, который построит очередной символ исходного языка, и поместить этот символ в стек. Если больше символов нет, в стек помещается ограничитель #.
УПРАЖНЕНИЯ К РАЗДЕЛУ 7.1
I.	Проведите разбор следующих предложений грамматики (7.1.2), выполняя (вручную) программу на ПЯ, приведенную на рис. 7.1. Считайте, что сканер строит идентификаторы и целые числа:
(а) А*В, (b) (А), (с)А+В+1, (d)A*B*l, (е) А+(В+1), (f)A*(B+l).
2.	Используя (название класса), перепишите следующие шесть продукций так, чтобы число их сократилось до двух:
X	Y	—>	Y	X	Z	—>Z	XANY Y
W	Y	—*	Y	W	Z	—>'L	WANY —•> W
3.	Что неверйо в каждой из следующих продукций?
a)	E+T-*ANY
b)	L: LI: Е+Т-*Е SCAN GO Y
с)	L:	(метка) SCAN GO STATE
4.	Напишите программу на ПЯ для распознавания предложений грамматики G (Z): Z : : = + I Z+ I Z —.
7.2.	ИСПОЛЬЗОВАНИЕ ПЯ
Поскольку ПЯ — язык программирования, а не механический конструктор распознавателей, как большинство других, изученных нами программ разбора, он более гибок. При разборе необяза
190
ГЛАВА 7
тельно слепо следовать формальной грамматике там, где можно постараться программировать эффективно. Большая свобода предоставляется и при согласовании синтаксиса с семантикой. Этот вопрос обсуждается в гл. 12 и 13. В данном разделе мы хотим дать некоторые советы по программированию на ПЯ. Мы покажем вначале, как можно запрограммировать на ПЯ (ш,п)ограниченно контекстный распознаватель; затем рассмотрим, как программируется метод рекурсивного спуска, изложенный в разд. 4.3. Наконец, перейдем к простому примеру, чтобы проиллюстрировать несколько приемов программирования.
Восходящий разбор на ПЯ
При изучении этого параграфа потребуется знание материала из разд. 6.3. В гл. 6 на рис. 6.7 представлена таблица для (1,1 Ограниченно контекстного распознавателя грамматики (6.3.6). В каждой строке таблицы указан образец для сопоставления и задано преобразование стека. Например, первая строка указывает, что если в стеке находится “а 0 Г, а входной символ то содержимое стека нужно заменить на “a W”.
Главное различие между программой на ПЯ и этой таблицей состоит не в форме записи, а в том, что таблица ни программой, ни алгоритмом не является; таблица — это лишь данные для алгоритма, описанного на рис. 6.8. Продукция ПЯ указывает, что нужно делать после того, как выполнено заданное в ней преобразование; строка таблицы на рис. 6. 7 таких указаний не содержит.
Можно построить на ПЯ программу, являющуюся по сути дела тем же распознавателем, который получается при объединении таблицы (рис. 6.7) и алгоритма (рис. 6.8). Именно это и сделано на рис. 7.3. В программе правый контекст R всегда оказывается верхним в стеке, поскольку лишь при таком расположении символов исходного языка ПЯ допускает обращение к ним.
Программа на рис. 7.3, как обычно, начинается с продукции, которая сканирует первый символ предложения. Затем продукции от L до М выполняют шаг 2 алгоритма, представленного на рис. 6.8. Каждая продукция соответствует одной строке таблицы на рис. 6.7. Если в некоторой продукции имеет место совпадение, то содержимое стека преобразуется так, как это задано в шаге 3 на рис. 6.8, а затем программа возвращается к продукции L, чтобы продолжить сравнение. Если совпадений не было, то продукции от N до N+2 выполняют шаги с 4-го по 6-й: работа прекращается и печатается сообщение “HALT 0”, если входная цепочка оказалась предложением, либо печатается сообщение “HALT 2”, если она предложением не является, либо сканируется следующий символ и выполняется переход к метке L, чтобы продолжать проверки на совпадение.
продукционный язык
191
Конечно, программа, приведенная на рис. 7. 3, не особенно эффективна. Для сокращения числа сравнений на каждом шаге можно изменить порядок записи продукций, чаще используя метки. Например, после преобразования в продукции L можно применить только L + 4.
Мы показали, как перевести на ПЯ—даже чисто механически — любую таблицу для (1,1)ограниченно контекстной грамматики. На самом деле этот процесс нетрудно модифицировать так, чтобы с его помощью переводить на ПЯ таблицы для (ш,п)ограниченно
S:	#	SCAN
L:	а 0	1 #	—* a W # GO L
0	0	1 1	—>	0 W 1 GO L
0 0 W	1 1	—*	0 W 1 GO L
а 0 W	1 #	a W # GO L
# а b b	W # 0	0 0	1	—>	#	Z	#	GO	L —>	b	(J	0	GO	L b	U	1	GO	L
ь и	0	0	—> b U 0 GO	L
ь и	0	1	—* b U 1 GO L
# ь	и 1	—-> # V 1 GO L
# V	1 1	— # V 1 GO L
# V	1 #	— # V # GO L
М:	# N:	#	V # Z # ANY	—> # Z # GO L HALT 0 HALT 2 SCAN	GO L
Рис. 7.3. Программа восходящего разбора на ПЯ.
контекстной грамматики. Нужно будет только внимательно следить за увеличившимся контекстом с обеих сторон от возможной основы. Поскольку все грамматики, допускающие применение описанных способов восходящего разбора, относятся к классу (гл, п)ограниченно контекстных при некоторых ш и п, мы можем написать на ПЯ программы, выполняющие для таких грамматик восходящее распознавание.
Нисходящий разбор без возвратов на ПН
При изучении этого параграфа потребуется знание материала из разд. 4.3. Два действия — CALL и RETURN — позволяют использовать группу продукций подобно тому, как используются процедуры в языках АЛГОЛ или ФОРТРАН. А это значит, что
192
ГЛАВА 7
можно запрограммировать на ПЯ нисходящий разбор, применяя описанный в разд. 4.3 метод рекурсивного спуска. И в самом деле, поскольку ПЯ разработан для преобразования символьной информации, запрограммировать рекурсивный спуск здесь намного легче.
S: SO:	# <инстр> # ANY			SCAN CALL ИНСТР HALT 0 HALT 2
ИНСТР: IF ANY <перем> <выр> ANY ИН1:	IF <выр> THEN ANY ИН2:	<инстр> ELSE <инстр> ANY ANY		—> —►	<инстр> ANY	SCAN CALL BMP GO ИН1 CALL ПЕРЕМ SCAN CALL ВЫР RETURN SCAN CALL ИНСТР GO ИН2 HALT 2 SCAN CALL ИНСТР RETURN HALT 2
ПЕРЕМ: ПЕРЕМ 1:	I I ( I ANY I (<выр>) ANY	—->	<перем> <перем>	SCAN SCAN CALL ВЫРООПЕРЕМ1 RETURN SCAN RETURN HALT 2
ВНР:	ANY <терм> + <терм> ANY ANY	—> —>	<выр> ANY	CALL ТЕРМ SCAN GO ВЫР RETURN HALT 2
ТЕРМ:	ANY <множ> * <множ> ANY ANY	—>	<терм> ANY	CALL МНОЖ SCAN GO ТЕРМ RETURN HALT 2
МНОЖ: МН1:	( ANY <перем> ANY (<выр>) ANY	—> —>	<множ> ANY <множ>	SCAN CALL ВЫP GO MH 1 CALL ПЕРЕМ RETURN SCAN RETURN HALT 2
Рис. 7.4. Программа нисходящего разбора на ПЯ.
На рис. 7.4 показан пример программы на ПЯ, которая распознает предложения грамматики (4.3.2). На рисунке каждому правилу этой грамматики соответствует набор продукций. Первая продукция выполняет начальную установку, заносит в стек первый символ предложения и вызывает продукции, соответствующие <инстр>. Если программа останавливается и печатает сообщение
ПРОДУКЦИОННЫЙ ЯЗЫК
193
“HALT 2”, то это означает, что входная цепочка не является предложением.
При работе с продукциями следуют тем же соглашениям, что и при работе с рекурсивными процедурами. При вызове набора продукций в стеке находится первый символ искомой фразы, а по возвращении в стеке уже будет находиться символ, следующий за этой фразой. Если читатель сравнит каждый из наборов продукций с соответствующей процедурой из разд. 4.3, он обнаружит их поразительное сходство; изменилась только форма записи.
Программирование на ПЯ
Если, программируя распознаватель, строго придерживаться только восходящего или только нисходящего (без возвратов) метода, то ни одна из программ не достигнет предела эффективности. Как мы уже видели, восходящий распознаватель неэффективен в силу того, что на каждом шаге приходится проводить сравнение с-большим числом продукций. (Это утверждение справедливо даже в том случае, когда продукции располагают в другом порядке и изменяют действия GO, стремясь уменьшить число сравнений.) С другой стороны, строго нисходящий распознаватель слишком часто пользуется действиями CALL и RETURN, а они гораздо медленнее простых переходов.
Обычно наилучшие результаты дает объединение обоих методов. Напишите подпрограмму (т. е. набор продукций, используемый как подпрограмма) для основных единиц исходного языка. Например, для языка АЛГОЛ напишите по одной подпрограмме для инструкций, для описаний и для выражений. (Заметьте, что такое разделение позволяет разным людям писать части программы почти совершенно независимо.) Каждая из подпрограмм может из разных мест обращаться к другим подпрограммам. Подпрограмма для инструкции вызывает подпрограмму для выражения и даже обращается сама к себе всякий раз, когда нужно разобрать подинструкцию.
Как уже говорилось раньше и как было показано на примере (рис. 7.1), не представляет особого труда написать отдельную подпрограмму, применяя восходящий метод. Суть состоит в использовании контекста и переходов для сокращения числа продукций, которые приходится сравнивать на каждом шаге.
Подпрограммы, соответствующие набору правил подстановки с одинаковыми левыми частями, обычно начинаются с нескольких продукций, в которых по первому символу выбирается альтернатива и происходит переход к поднабору продукций, выполняющих разбор этой альтернативы. Хорошим примером здесь может служить подпрограмма для инструкции. Рассмотрим усеченную грамматику:
7 д. грио
194
ГЛАВА 7
<инструкция>
= IF <выр> THEN <инструкция>
|<перем>: =<выр>
|FOR <перем>: = <список элементов цикла> DO <инструкция> |READ (<перем>)
Программа на ПЯ для такой грамматики приводится ниже. Заметим, что <перем> рассматривается как одна из основных синтаксических единиц и распознается при помощи подпрограммы. Сделано так потому, что <перем> многократно встречается в разных местах. Если бы мы использовали обычное действие GO ПЕРЕМ, нам после распознавания <перем> пришлось бы пройти длинную последовательность продукций, чтобы по контексту установить, куда именно нам нужно вернуться.
ИНСТРУКЦИЯ: IF	SCAN	CALL ВЫР GO CONDL
FOR	SCAN	CALL ПЕРЕМ GO	FORL
READ	SCAN	GO READL
ANY	CALL	ПЕРЕМ GO ASSL
CONDL:	(продукции, распознающие усл. инструкцию)
FORL:	(продукции, распознающие цикл типа FOR)
READL:	(продукции, распознающие READ)
ASSL:	(продукции, распознающие присваивание)
Опять-таки, используя названия классов, мы можем сэкономить усилия, затрачиваемые на программирование, и время, необходимое для выполнения программы. Сравните
READ	EXEC	1	SCAN	GO	IDLIST
INTEGER	EXEC	2	SCAN	GO	IDLIST
BOOLEAN	EXEC	3	SCAN	GO	IDLIST
STRING	— EXEC	4	SCAN	GO	IDLIST
COMPLEX	EXEC	5	SCAN	GO	IDLIST
и
CLASSNO TYPE REAL 1 INTEGER 2
BOOLEAN 3 STRING 4 COMPLEX 5;
TYPE	—> EXEC TYPE SCAN GO IDLIST
Последнее гораздо проще написать и проще прочесть.
ПРОДУКЦИОННЫЙ язык
195
УПРАЖНЕНИЯ К РАЗДЕЛУ 7.2
1.	Сделайте более эффективной программу, представленную на рис. 7.3, переупорядочив продукции и изменив действия GO.
2.	Напишите на ПЯ программу, распознающую предложения следующей грамматики, используя (1) нисходящий и (2) восходящий методы:
Z : : = ЬМЬ | Me, М : : = (La, L : : = Ма)
7.3.	ВЫЗОВ СЕМАНТИЧЕСКИХ ПОДПРОГРАММ
Теперь мы объясним, как используются действия ЕХЕС. Может быть, на этом этапе изложенное не будет совершенно понятным, поскольку речи о семантических подпрограммах еще не было. Поэтому, изучив гл. 12 и 13, стоит вернуться к этому разделу.
В любой реализации ПЯ используются по меньшей мере два параллельно работающих стека. Об одном из них, синтаксическом стеке SYN, мы уже говорили. Во втором, который называется семантическим стеком SEM, содержится «смысл» символов, находящихся в первом стеке. То есть элемент SEM (i) содержит «смысл» или «семантику» символа, находящегося в SYN (i). В гл. 12 и 13 будет объяснено, что имеется в виду, когда речь идет о смысле.
Семантика большинства терминальных символов первоначально не определена. Это означает, что сканер помещает в стек SYN некоторый символ, а в соответствующую ячейку стека SEM никакое новое значение не заносится. Однако для терминальных символов I и N дело обстоит иначе. Когда сканер находит идентификатор (целое число) и помещает I (N) в синтаксический стек SYN, он помещает тот же идентификатор (целое число) в SEM. (Возможно, туда будет помещен указатель на данный идентификатор или целое число, но в рамках нашего изложения это безразлично.) Таким образом, семантикой I (N) является идентификатор (целое число), к которому I (N) относится.
В любой реализации ПЯ предполагается некоторый связанный с ПЯ семантический язык, на котором пишутся семантические подпрограммы. Им может быть язык ассемблера, ФОРТРАН, ПЛ/1, АЛГОЛ — все равно. Под семантической подпрограммой мы подразумеваем не что иное, как такой набор инструкций семантического языка, который можно как-то идентифицировать. Каждый такой набор может быть либо отдельной процедурой, либо одной подинструкцией в инструкции выбора (case statement), либо в семантическом языке могут быть предусмотрены специальные «семантические метки», по которым определяется начало семантической подпрограммы.
?*
196
ГЛАВА 7
В семантическом языке имеется также доступ к семантическому стеку SEM. Когда в продукции встречается, например, действие ЕХЕС5, то в семантической программе выполняется семантическая подпрограмма с номером 5. Она производит необходимые вычисления, используя верхние элементы стека SEM, переменные, описанные на семантическом языке, а затем происходит возврат.
Предположим, например, что текущей является продукция
I -> F ЕХЕС 5 SCAN GO LT
а содержимое стеков выглядит так:
SYN	I		АВ	SEM
	♦		смысл ♦	
	т *		смысл Т	
Следовательно, левая часть совпадает с содержимым стека и верхний элемент SYN заменяется на F. Затем вызывается подпрограмма 5. Она может взять из верхней позиции стека SEM идентификатор АВ, найти его в таблице символов и поместить в качестве верхнего элемента стека SEM номер позиции идентификатора АВ в таблице, равный, например, 3. В результате получим
SYN
SEM
Затем происходит возврат из семантической подпрограммы 5, сканируется следующий символ и текущей становится продукция LT.
Нам осталось только сказать о применении действия ЕХЕС «(название класса). Напомним, что это название класса должно обязательно встретиться в левой части данной продукции. Выполнение такого действия приводит к выполнению семантической подпрограммы, которая в описании данного названия класса связана с тем находящимся сейчас в стеке символом, который совпал с названием класса в левой части текущей продукции. Это проще пояснить на примере. Рассмотрим описание
CLASSNO OPER * 1 +2 — 3 /4
ПРОДУКЦИОННЫЙ язык
197
и текущую продукцию Е OPER Е -► Е EXEC OTER. Если в стеке содержится Е — Е, будет выполнена подпрограмма 3.
7.4.	ИСТОРИЧЕСКИЕ ЗАМЕЧАНИЯ
Впервые продукционный язык был описан Флойдом [61Ь]. Предложенный им язык предназначался для обработки символьной информации и, помимо формы записи, во многом напоминает ПЯ. Идея такого языка получила дальнейшее развитие в работе Эванса [64]. ПЯ в качестве синтаксического метода применяется в системе построения трансляторов FSL, разработанной и реализованной Фелдманом [66]. Он также используется в некоторых других системах построения трансляторов. Было разработано несколько конструкторов (автор одного из них — Эрли), которые для заданной грамматики с подходящими свойствами пишут программу на ПЯ, являющуюся синтаксическим распознавателем для этой грамматики. Ихбия и Морз [70] описывают алгоритм, с помощью которого по грамматикам предшествования генерируются «оптимальные» продукции. В их методе семантические действия CALL и RETURN не используются.
Глава 8
Организация памяти во время выполнения программы
В этой главе описывается организация памяти во время выполнения программы, написанной на одном из трех хорошо известных языков высокого уровня. Будут объяснены многие понятия, относящиеся к языкам программирования. Можно отметить простую организацию в языках, подобных ФОРТРАНу, более сложную для языков, подобных АЛГОЛу, и самую сложную организацию для языков, подобных PL/1 со структурами, указателями и инструкциями ALLOCATE и FREE.
Конечно, у нас нет возможности описать подробно все эти языки. Нашей целью является описание основных деталей управления памятью и представление некоторых общих концепций. После усвоения материала, изложенного в этой главе, читатель должен быть в состоянии создать свою собственную программу управления памятью для любого языка. Чтение этой главы поможет уяснить, что существует много тонкостей, которые можно понять лишь при детальном знании исходного языка.
Некоторые языки требуют схемы динамического распределения памяти, когда блоки оперативной памяти произвольным образом выделяются, затем освобождаются и снова занимаются для других целей. Исчерпывающее рассмотрение таких схем выходит за рамки данной книги. Для полноты мы представим один такой метод распределения памяти в разд. 8.10, но для изучения этих методов читателю рекомендуется книга Кнута [68]. Теперь предположим, что имеются две программы
GETAREA (ADDRESS, SIZE) и FREEAREA (ADDRESS, SIZE) Первая из них выделяет для вызывающей программы последовательность, содержащую SIZE ячеек, передавая начальный адрес последовательности через выходной параметр ADDRESS. Программа FREEAREA освобождает SIZE ячеек, начиная с адреса ADDRESS.
В некоторых системах память выделяется, но никогда явно не освобождается. Если память исчерпана, то система запускает про
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 199
грамму «мусорщик», которая находит и освобождает фактически не используемые ячейки. Может оказаться также необходимым объединить всю используемую память — поместить все данные в последовательные ячейки — так, чтобы освободился большой блок последовательных ячеек вместо нескольких небольших блоков. Эта проблема кратко рассматривается в разд. 8.10; мы предполагаем также, что существует программа FREEGARBAGE, выполняющая эту работу.
Глава строится следующим образом. С целью унификации всего описания в разд. 8.1 вводится понятие области данных. В разд. 8.2 рассматриваются описатели, тогда как разд, с 8.3 по 8.6 посвящены задачам, возникающим в связи с реализацией структур данных в порядке возрастания их сложности. В разд. 8.7 рассматриваются вызовы процедур и соответствие формальных и фактических параметров. Затем мы возвращаемся к проблемам, связанным с ФОРТРАНОМ и АЛГОЛом. Глава оканчивается разд. 8.10, в котором описывается динамическое распределение памяти и сбор мусора.
8.1.	ОБЛАСТИ ДАННЫХ И ДИСПЛЕИ
Областью данных является ряд последовательных ячеек — блок оперативной памяти,— выделенный для данных, каким-то образом объединенных логически. Часто (но не всегда) все ячейки области данных принадлежат одной и той же области действия в программе на исходном языке; к ним может обращаться один и тот же набор инструкций (т. е. этой областью действия может быть блок или тело процедуры). Вегнер [68] использует для аналогичного понятия термин запись активации; мы предпочитаем другой термин — область данных.
Для переменных, описываемых инструкцией COMMON на ФОР-ТРАНе, должна быть выделена память в одной области данных, в области данных непомеченного блока COMMON. В АЛГОЛе область данных времени счета может формироваться из ячеек для простых переменных, описанных в главной программе, и из необходимых ячеек для временных переменных.
Во время компиляции ячейка для любой переменной времени счета может быть представлена упорядоченной парой чисел (номер области данных, смещение), где номер области данных — это некоторый единственный номер, присвоенный области данных, а смещение — это адрес леременной относительно начала области данных. (Так, первая ячейка области данных 3 представляется парой (3,0), вторая — (3,1) и т. д.) Когда мы генерируем команды обращения к переменной, эта пара переводится в действительный адрес переменной. Это обычно выполняется установкой адреса базы (машинного адреса' первой ячейки) области данных на регистр и
200
ГЛАВА 8
обращению к переменной по адресу, равному смещению плюс содержимое регистра. Пара (номер области данных, смещение) таким образом переводится в пару (адрес базы, смещение).
Области данных делятся на два класса — статический и динамический. Статическая область данных имеет постоянное число ячеек, выделенных ей перед началом счета. Эти ячейки выделяются на все время счета. Следовательно, на переменную в статистической области данных возможна ссылка по ее абсолютному адресу вместо пары (адрес базы, смещение).
BEGIN PROCEDURE А;
BEGIN PROCEDURE В; BEGIN...END;
PROCEDURE C; BEGIN...END;
END;
PROCEDURE D; BEGIN...END;
END;
Рис. 8.1. Структура программы с процедурами на АЛГОЛе.
Динамическая область данных не всегда присутствует во время счета. Она появляется и исчезает, и всякий раз, когда она исчезает, теряются все величины, хранящиеся в ней, Примером может служить область данных, содержащая переменные процедуры в АЛГОЛе. Процедура, к которой обратились, вызывает программу QETA. REA, чтобы выделить память для своей области данных постоянной длины. Непосредственно перед возвратом в вызывающую программу процедура вызывает программу FREEAREA для освобождения памяти. Заметим, что, возможно, процедуре выделяются не одни и те же ячейки памяти для ее области данных. Однако GETAREA всегда выдает адрес базы выделенной области данных и процедура всегда хранит его в одной и той же ячейке, например, ВА. Таким образом, адресом любой переменной в области данных всегда является CONTENTS(BA)+OFFSET.
Если вызов процедуры происходит рекурсивно, то имеется несколько областей данных в оперативной памяти, каждая для отдельного вызова процедуры. Это именно то, что мы хотим; каждый раз при сбращении к процедуре она должна иметь новую область действия. Такая область данных принадлежит не собственно процедуре, а выполнению процедуры. В любой момент выполнения процедуры возможны обращения к переменным из нескольких областей данных. Рассмотрим, например, программу на АЛГОЛе, содержащую процедуры (рис. 8.1). Если для главной программы и для каждой из процедур существуют различные области данных, то при выполнении процедуры В возможно обращение к областям данных для процедур А, В и главной программы. Чтобы иметь
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 201
возможность ссылаться на них, мы собираем адреса этих областей данных в таблицу, которую назовем дисплеем. Для этого частного примера при выполнении процедуры В дисплей будет иметь вид
Адрес области. данных главней, программы
Дисплей
Адрес области данных процедуры А
Адрес области данных процедуры В
Таким образом, когда мы создаем области данных (получаем для них память), мы имеем постоянное место для запоминания их адресов. Нам нужно ответить на несколько вопросов: где должен помещаться сам дисплей? В каком порядке мы заносим в дисплей адреса? На эти вопросы мы ответим, когда будем рассматривать реализацию отдельных языков.
Вообще при счете должно быть несколько дисплеев, используемых при выполнении различных частей программы. Предположим, что в вышеприведенном примере в процедуре В есть вызов процедуры D. Здесь мы должны создать дисплей, содержащий только адреса областей данных для D и главной программы, используемых в D. Мы должны сохранить нетронутым предыдущий дисплей; он будет нужен снова, когда выполнение D завершится и выполнится возврат в В. В любой момент выполнения программы активным дисплеем является тот, который в настоящее время используется для обращений к областям данных.
УПРАЖНЕНИЯ К РАЗДЕЛУ 8.1
1.	Как должен выглядеть текущий дисплей при выполнении процедуры С на рис. 8.1? При выполнении процедуры А? Какой вид будет иметь дисплей при выполнении процедуры D на рис. 8.5? При выполнении процедуры В?
8.2.	ОПИСАТЕЛИ1)
Если компилятор знает все характеристики переменных во время компиляции, то он может сгенерировать полностью команды обращения к переменным, основываясь на этих характеристиках. Но во многих случаях информация может задаваться динамически
х) В оригинале употребляется термин TEMPLATE. Однако автор отмечает, что, например, Стендиш вместо TEMPLATE использует термин DISCRIPTOR.— Прим, перев,
202
глава 8
во время счета. Например, в АЛГОЛе не известны нижняя и верхняя границы размерностей массивов, а в некоторых языках тип фактических параметров не соответствует точно типу формальных параметров. В таких случаях компилятор не может сгенерировать простые и эффективные команды, так как он должен учитывать все возможные варианты характеристик.
Чтобы решить эту задачу, компилятор выделяет память не только для переменных, но и для их описателей, которые содержат атрибуты (характеристики), определяемые во время счета. Этот описатель заполняется и изменяется по мере того, как становятся известными и меняются характеристики при счете.
Возьмем простой пример: если формальный параметр является простой переменной и тип соответствующего фактического параметра может меняться, фактический параметр, передаваемый процедуре, может выглядеть, например, так:
Описатель 0 = действительный, 1»целый, 2=6улевый и т.д. Адрес значения (или само значение)
Если в процедуре есть обращение к формальному параметру, процедура должна запрашивать или интерпретировать этот описатель и затем выполнить любое необходимое преобразование типа. Эти действия можно, конечно, выполнить, обращаясь к другой программе.
Во многих случаях компилятор не может выделить память для значений переменных, так как неизвестны атрибуты размерности. Так происходит с массивами в АЛГОЛе. Все, что компилятор может сделать,— это выделить память в области данных для описателя фиксированной длины, описывающего переменную. Во время выполнения программы, когда станут известны размерности, будет вызвана программа GETAREA, которая выделит память и занесет в описатель адрес этой памяти. При этом ссылка на значение всегда выполняется с помощью такого описателя.
Для структур или записей требуются более сложные описатели, в которых указывается, как связаны между собой компоненты и подкомпоненты. Мы рассмотрим это в разд. 8.6.
Чем больше атрибутов могут меняться при счете, тем больше приходится выполнять работы во время счета. Причина, по которой для ФОРТРАНа можно скомпилировать более эффективную программу, чем для АЛГОЛа или PL/1, заключается именно в том, что для ФОРТРАНа практически все характеристики известны во время компиляции и не нужны ни описатели, ни их интерпретация.
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 203
8.3.	ПАМЯТЬ ДЛЯ ДАННЫХ ЭЛЕМЕНТАРНЫХ ТИПОВ
Типы данных исходной программы должны быть отображены на типы данных машины. Для некоторых типов это будет соответствием один к одному (целые, вещественные и т. д.), для других могут понадобиться несколько машинных слов. Коротко мы отметим следующее:
1.	Целые переменные обычно содержатся в одном слове или ячейке области данных; значение хранится в виде стандартного внутреннего изображения.целого числа в машине.
2.	Вещественные переменные обычно содержатся в одном слове. Если желательна большая точность, чем возможно представить в одном слове, то может быть употреблен машинный формат двойного слова с плавающей запятой. В машинах, где отсутствует формат с плавающей запятой, могут быть употреблены два слова — одно для показателя степени, второе для (нормализованной) мантиссы. Операции с плавающей запятой в этом случае должны выполняться с помощью подпрограмм.
3.	Булевы или логические переменные могут содержаться в одном слове, причем нуль изображает значение FALSE, а не нуль или 1 — значение TRUE. Конкретное представление для TRUE и FALSE определяется командами машины, осуществляющими логические операции. Для каждой булевой переменной можно также использовать один бит и разместить в одном слове несколько булевых переменных или констант.
4.	Указатель — это адрес другого значения (или ссылка на него). В некоторых случаях бывает необходимо представлять указатель в двух последовательных ячейках; одна ячейка содержит ссылку на описатель или является описателем текущей величины, на которую ссылается указатель, в то время как другая указывает собственно на значение величины. Это бывает необходимо в случае, когда во время компиляции невозможно определить для каждого использования указателя тип величины, на которую он ссылается
8.4.	ПАМЯТЬ ДЛЯ МАССИВОВ
Мы предполагаем, что каждый элемент массива или вектора занимает в памяти одну'ячейку. Обобщение на случай большого числа ячеек предоставляется читателю.
Векторы
Элементы векторов (одномерных массивов) обычно помещаются в последовательных ячейках области данных в порядке возрастания или убывания адресов. Порядок зависит от машины и ее системы команд. Например, на машине IBM 7090 для получения исполни
204
ГЛАВА 8
тельного адреса содержимое индекс-регистра вычитается из базового адреса; поэтому элементы массива хранятся обычно в порядке убывания индексов.
Мы предполагаем, что используется более стандартный возрастающий порядок, т. е. элементы массива, определенного описанием ARRAY А [1 : 101, размещаются в порядке А [11, А [21, ..., А [101.
Матрицы
Существует несколько способов размещения двумерных массивов. Обычный способ состоит в хранении их в области данных по строкам в порядке возрастания, т. е. (для массива, описанного как ARRAY All : М, 1 : N1) в порядке
All, 1], АН, 21, •••, АН, N1, А[2, 1], •••, А[2, N1, •••, А[М, 11, • ••, А[М, NI.
Вид последовательности показывает, что элемент АН, jl находится в ячейке с адресом ADDRESS (АН, ll) + (i—l)*N+(j—1), который мы запишем в виде
(8.4.1)	(ADDRESS (АН, 1 ])—N—1 )+(i*N+j).
Первое слагаемое является константой, и его требуется вычислить только один раз. Таким образом, для определения адреса A[i, jl необходимо выполнить одно умножение и два сложения. Язык ФОРТРАН IV фирмы IBM требует размещения массивов по столбцам.
Второй метод состоит в том, что выделяется отдельная область данных для каждой строки и имеется вектор указателей для этих областей данных. Элементы каждой строки размещаются в соседних ячейках в порядке возрастания. Так, описание ARRAY All : М, 1 : N1 порождает
Вектор указателей строк хранится в той области данных, с которой ассоциируется массив, в то время как собственно массив хранится в отдельной области данных. Адрес элемента массива Ali, j] есть CONTENTS (адрес вектора+i—l)+(j—1).
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 205
Первое преимущество этого метода состоит в том, что при вычислении адреса не нужно выполнять операцию умножения. Другим является то, что не все строки могут находиться в оперативной памяти одновременно. Указатель строки может содержать некоторое значение, которое вызовет аппаратное или программное прерывание в случае, если строка в памяти отсутствует. Когда возникает прерывание, строка вводится в оперативную память на место другой строки. Этот метод применен в компиляторе с расширенного АЛГОЛа на машине В 55ЭЭ фирмы Burroughs. Если все строки находятся в оперативной памяти, то массив требует больше места, поскольку место нужно и для вектора указателей.
Когда известно, что матрицы разреженные (большинство элементов нули), используются другие методы. Может быть применена схема хеш-адресации, которая базируется на значениях i и j элемента массива АН, j] и ссылается по хеш-адресу на относительно короткую таблицу элементов массива (гл. 9). В таблице хранятся только ненулевые элементы матрицы.
Многомерные массивы
Мы рассмотрим размещение в памяти и обращение к многомерным массивам, описанным, например, на АЛГОЛе следующим образом:
ARRAY A[Lx : Ux, L2 : U2, •••, Ln : Unl
Метод, который мы рассмотрим, заключается в обобщении первого метода, предложенного для двумерного случая; он также применим и в случае одномерного массива.
Подмассив А[i, #, •••,#] содержит последовательность АЩ, *, • • •, *], A[Li+1, *, •••,*] и т. д. до A[Ui, •», • • •, *]. Внутри подмассива АП, *, • • •, %] находятся подподмассивы Ali, L2,	• • •, d,
АН, L2+l, *, •••,*!, • • • и A[i, U2, *, • • •, d. Это повторяется для каждого измерения. Так, если мы продвигаемся в порядке возрастания по элементам массива, быстрее изменяются последние индексы:
под массив А[ L1 ]
A[L1,L2]	A[L1,L2+1]	A[L1,U2]

A[U1]
Вопрос заключается в том, как обратиться к элементу АП, j, к, •••, 1, ml. Обозначим
•••,da-Ua-Ln+l.
206
ГЛАВА 8
То есть di есть число различных значений индексов в i-м измерении. Следуя логике двумерного случая, мы находим начало подмассива A[i, *, •••,*]
BASELOC+(i—L1)*d2*d3*- • -*dn
где BASELOC — адрес первого элемента A[Li, L2, •••, LJ, a d2*d3*- • -«dn — размер каждого подмассива A[i, *, •••,*]. Начало подподмассива A[i, j, *, • ••, *1 находится затем добавлением
(j—L2)*d3*- • -*dn
к полученному значению.
Действуя и дальше таким же образом, мы окончательно получим
(8.4.2)	BASELOC+(i—L4)*d2*d3*- • • *dn+(j—L2)*d3*- • • *dn
+(k—L3)*d4*- • -*dn4-+(1—Ln-i)*dn+m—Ln.
Выполняя умножения, получаем
(8.4.3)	CONSPART+VARPART
где
(8.4.4)	CONSPART=BASELOC—((• • • ((L1*d2+L2)*d3+L3)*d4+
+ • • • +Ln-i)*dn+Ln)
и
(8.4.5)	VARPART=(- • • ((i*d2)+j)*d3+ • • • +l)*dn+m.
Значение CONSPART необходимо' вычислить только один раз и запомнить, так как оно зависит лишь от нижних и верхних границ индексов и месторасположения массива в памяти. VARPART зависит от значений индексов i, j, • • •, m и от размеров измерений d2, d3, • • •, dn- Вычисление выглядит довольно громоздким только потому, что оно представлено в таком виде. В действительности вычисление можно упростить, выполнив последовательность таких действий:
VARPART: = первый индекс (i)
VARPART: = VARPART*d2 + второй индекс (j) VARPART: = VARPART»dgH-третий индекс (k)
VARPART: = VARPART*dn4-n-fl индекс (m)
Таким образом, метод борьбы со сложностями существует; он позволяет очень просто запрограммировать адреса величин с индексами или сгенерировать команды вычисления этих адресов по опи
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 207
санной схеме. В действительности вследствие итеративного характера вычислений сформировать любые ссылки на многомерный массив так же легко, как и ссылки на дву- или трехмерный массив.
В некоторых случаях, когда нужно осуществить оптимизацию программы, лучше использовать (8.4.2) (отделив, конечно, постоянную и переменную часть), где dj не будут перемножаться, так как при этом вычисление распадается на несколько независимых частей. Мы увидим это в гл. 18.
Информационные векторы
В ФОРТРАНе верхняя и нижняя границы массивов известны во время трансляции. Поэтому компилятор может выделить память для массивов и сформировать команды, ссылающиеся на элементы
L1	U1	di
L2. d	U2	If di
п	C0NSPART	
BASE ЮС		
Описате гмссива
Рис. 8.2. Информационный вектор для массива.
массива, используя верхние и нижние границы и постоянные значения dlt d2, • • •, dn. В АЛГОЛе и PL/1 это невозможно, так как границы могут вычисляться во время счета. Таким образом, нам нужен описатель для массива, содержащий необходимую информацию. Этот описатель для массива называется допвектор (dope vector), или информационный вектор. Информационный вектор имеет фиксированный размер, который известен при компиляции, следовательно, память для него может быть отведена во время компиляции в области данных, с которой ассоциируется массив. Память для самого массива не может быть отведена до тех пор, пока во время счета не выполнится вход в блок, в котором описан массив. При входе в блок вычисляются границы массива и производится обращение к программе распределения памяти для массивов. Эта программа вычисляет число необходимых ячеек, вызывает GETAREA, чтобы выделить область данных нужного объема, и вносит в информационный вектор необходимую информацию. На рис. 8.3 представлена схема работы программы распределения памяти для массива.
Какая информация заносится в информационный вектор? Для предложенной выше n-мерной схемы нам как минимум нужны зна
208
ГЛАВА 8
чения d2, • ••, dn и CONSPART. Если перед обращением к массиву нужно проверять правильность задания индексов, то следует также занести в информационный вектор значения верхних и нижних границ. Необходимая информация показана на рис. 8.2.
УПРАЖНЕНИЯ К РАЗДЕЛУ 8.4
1. Выделите в (8.4.2) постоянную часть и переменную часть, которая зависит от индексов, но без перемножения dj. Предположим, что мы пользуемся новой формулой для ссылки на элементы
Рис. 8.3. Программа распределения памяти для массивов.
Параметры программы: п (число индексов), нижние и верхние границы Llt Ut, . . . . . . , Ln Un и адрес информационного вектора.
массива. Какие значения должны быть в информационном векторе, чтобы вычисления были эффективными? Переделайте блок-схему программы распределения памяти для массива так, чтоэы она заносила эти значения в информационный вектор.
2. Напишите и отладьте программу распределения памяти для массивов на каком-либо языке. Для ее отладки напишите фиктивную процедуру GETAREA.
3. Предположим, что массив хранится не по строкам, а по столбцам. Преобразуйте формулы вычисления элемента массива A[i, j, •••, ml. Составьте блок-схему программы распределения памяти для массива и формат информационного вектора.
8.5. ПАМЯТЬ ДЛЯ СТРОК
Строка есть последовательность литер. Если длина строки (число литер) не изменяется во время выполнения программы и известна при компиляции, то строку можно разместите в некотором коли
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 209
честве подряд расположенных слов, зависящем от числа символов в слове [например, на IBM 7090 в одном слове помещается6 литер, на IBM 360—4 литеры (одна литера в байте)]. Однако длина строки не всегда известна. Если известна максимальная’ длина, то можно выделить столько слов, сколько нужно для строки максимальной длины, и перед этими словами поместить слово, содержащее текущее число литер:
Текущая длина
Текущая строка
Неиспользованные слова
Место для максимального числа литер
Слово, содержащее текущее значение длины,— это своего рода описатель. Иногда бывает необходимо добавить слово, содержащее максимальную длину.
Наиболее сложный случай встречается тогда, когда значение максимальной длины не известно при компиляции. Его реализацию наиболее трудно осуществить. Выполнение присваивания значений для строк А :=В сталкивается с трудностями, если значение В, стоящее справа, содержит больше литер, чем область, занимаемая А. Одно решение, используемое в XPL и SPL (см. Маккиман и др. [70]) на IBM 360, состоит в том, что в статической области данных отводится большая область строк, способная вместить все строки. Каждая строка-переменная имеет описатель, состоящий из двух последовательных слов, содержащих текущую длину строки, и указатель-ссылку на саму строку в области строк. Этот тип описателя мы назовем указателем строки. Если мы выполним присваивания SI := 'АВ', S2 : = 'QED' и S3:= 'F', мы будем иметь
Указатель на строку
Например, выполнение присваивания SI := S2 не изменит строку, а изменит только указатель строки. Присваивание S3 :== SUBSTR (S2, 1,1) можно выполнить аналогичным образом. Например, вы.
210
ГЛАВА 8
полнение этих двух присваиваний преобразует приведенную выше схему следующим образом:
Заметьте, что все три указателя строк указывают на части той же самой строки. Простые присваивания выполняются эффективно, так как изменяются только указатели строк, но не сами строки. Сцепление строк (скажем, SI := S2 CAT S3) выполняется медленнее, так как целая строка должна быть составлена на свободном месте. Выполнение этого присваивания привело бы к схеме
В некоторый момент область строк может переполниться. Но большая ее часть при этом может оказаться «мусором», а именно символами, на которые не указывает ни один указатель строки. Символы АВ в изображенной выше области являются мусором. Когда подобное происходит, вызывается программа FREEGARBAGE, чтобы обнаружить мусор, выбросить его и перенести полезную информацию в начало области строк.
8.6. ПАМЯТЬ ДЛЯ СТРУКТУР
Существует несколько альтернатив для определения новых типов данных как структур, составленных из типов данных, определенных ранее. Величины такого типа мы называем структурными величинами. В языках КОБОЛ, PL/1, АЛГОЛ W, АЛГОЛ 68 и СНОБОЛ имеются средства определения структур в том или ином виде. Они отличаются по предоставляемым возможностям и по эффективности, с которой они могут быть реализованы. Мы опишем
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 211
три из них и коротко рассмотрим их реализацию. Здесь необходимо ответить на четыре вопроса:
Как выделять память для структурных величин?
Как строить структурные величины?
Как ссылаться на компоненту структурной величины?
Как освобождать память?
Записи по Хоору (Вирт и Хоор [66])
Определение нового типа данных имеет вид
RECORD «(идентификатору («(компонента)», <компонента>,..., «(компонента»
где каждая компонента имеет вид
Спростой тип> <идентификатор>
причем Спростой тип> является одним из основных типов языка —REAL, INTEGER, POINTER и т. д. (АЛГОЛ W использует вместо POINTER термин REFERENCE). Например,
RECORD COMPLEX (INTEGER REALPART, INTEGER IMAGPART)
определяет новый тип COMPLEX, состоящий из двух компонент типа INTEGER, названных REALPART и IMAGPART. Во время компиляции известны все характеристики всех компонент, включая тип данных, на которые могут ссылаться указатели. Во время счета не нужны описатели ни для самой структуры, ни для ее компонент, причем может быть сгенерирована эффективная программа.
Любая^структурная величина с п компонентами может храниться в памяти в виде
. компонента, 1	компонента, 2	• • •	компонента, л
Поскольку при компиляции известны все характеристики, то известен также объем памяти, необходимый для каждой компоненты, и, следовательно, компилятор знает смещение каждой компоненты относительно начала структурной величины. Для упрощения сбора мусора лучше всего присвоить номер каждому типу данных (включая типы, определенные программистом) и иметь описатель для каждого указателя. Описатель будет содержать номер, описывающий тип величины, на которую в данный момент ссылается указатель.
Память для указателей и их описателей может быть выделена компилятором в области данных, с которой они связаны; это не
ГЛАВА 8
212
трудно, так как они имеют фиксированную длину. Для хранения текущих значений структурных величин может быть использована отдельная статическая область данных и специальная программа GETAREA для выделения памяти в этой области. В языке АЛГОЛ W нет явного оператора для освобождения памяти, так что, когда память исчерпана, система обращается к программе FREEGARBAGE. Заметим, что для того, чтобы иметь возможность обнаружить мусор, нужно знать, где расположены все указатели, включая те, которые являются компонентами структурных величин.
Новое значение типа COMPLEX с компонентами S1 и S2 может быть получено при выполнении инструкции
Р := COMPLEX(S1, S2)
где Р описан как указатель величин типа COMPLEX. Выполнение этой инструкции сводится к обращению к GETAREA за областью нужного размера, к запоминанию в этой области вычисленных значений S1 и S2 и к занесению в Р их адреса.
На структурные величины ссылаются только описанные указатели. Если выполняются инструкции присваивания
Р1 :== Р; Р COMPLEX (S3, S4)
первоначальная величина, связанная с Р, не теряется, так как Р1 указывает на нее. Заметим, что сразу после выполнения Р1 : = Р величины Р1 и Р указывают на одну и ту же величину.
Ссылка на компоненту имеет вид
<имя компонентьО («указатель на структурную величину») Таким образом, при выполнении
IMAGPART(P) := IMAGPART(P)+1
ко второй компоненте структурной величины с указателем Р прибавляется 1. Для этого случая можно сгенерировать эффективную программу. Например, команды для вышеприведенной инструкции на IBM 360 могли бы выглядеть так:
L 1,Р
L 2,4(1)
А	2, —F'lz
ST	2,4(1)
Загрузить значение Р в регистр 1. Загрузить IMAGPART в регистр 2. Заметим, что смещение 4 известно во время компиляции.
Прибавите 1 к содержимому регистра 2. Запомнить результат в IMAGPART.
Структуры PL/1
Структуры PL/1 являются более сложными, чем записи по Хоору. Их компоненты сами могут иметь подкомпоненты. Например:
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 213
DEGLARE 1 PERSON,
2 NAME CHARACTER (20),
2 FATHER POINTER,
2 MOTHER POINTER,
2 CHILDREN,
3 OLDEST 1 POINTER,
3 OLDEST2 POINTER,
3 OLDEST3 POINTER,
2 AGE FIXED;
Эта структура есть дерево, узлы которого связаны с именами и концевые узлы которого имеют значения данных. Корень PERSON имеет уровень I, а узлы, до которых можно дойти от корня по пути длиною i—1, находятся на уровне i.
Если бы возможность иметь подкомпоненты была бы единственным различием между записями по Хоору и структурами PL/1, не было бы существенной разницы во время выполнения программы; можно было бы спокойно разместить все компоненты и подкомпоненты так, чтобы каждая имела фиксированное смещение относительно начала структуры и это смещение было бы известно во время компиляции. Однако в языке PL/I существует еще два расширения.
С целью экономии памяти атрибут CELL для компоненты требует, чтобы все ее подкомпоненты непременно занимали одно и то же место в памяти. В любое заданное время только одна из нескольких переменных может иметь значение. Присваивание значения подкомпоненте приводит к тому, что подкомпонента, к которой обращались ранее, утрачивает свое значение. Например, в случае
DECLARE 1 DATE CELL, 2 A FLOAT, 2 В FIXED;
обе компоненты А и В используют одну и ту же память. Это вызывает осложнения во время компиляции, но в действительности не очень изменяет код готовой программы, если только объектная программа не должна проверять при каждом обращении к подкомпоненте, что значение подкомпоненты действительно существует.
Для другого расширения требуются более сложные административные функции во время выполнения программы. В PL/1 корневой узел структурного дерева или любая из подкомпонент могут быть снабжены размерностями. Например, мы могли бы переписать первую инструкцию DECLARE так:
DECLARE 1 PERSON,
2 NAME CHARACTERS (20),
2 FATHER POINTER,
2 MOTHER POINTER,
2 CHILDREN (3) POINTER,
2 AGE FIXED;
214
ГЛАВА 8
чтобы получить в основном тот же результат; одна компонента, CHILDREN, состоит из трех указателей (обращаются к ним иначе). В качестве другого примера рассмотрим определение структуры
DECLARE 1 А, 2 В (2), 3 X (М, N), 3 Y (М, N), 3 Z, 2 С LIKE В,
2 N (2);
Это приводит к структуре
X (1,N),
X(M,N) Y(M,N)
С(2)
N( t)
N (2)
Так как выражения, которые определяют границы изменения индексов, должны быть вычислены при выполнении программы, для них, как и в случае массивов, следует употреблять описатели, или информационные векторы. В предыдущей инструкции DECLARE нам необходимы информационные векторы для всех компонент, исключая само А и Z.
В PL/1 используется три различных метода распределения памяти — статическое (STATIC), автоматическое (AUTOMATIC) и управляемое (CONTROLLED) распределение.
В отличие от АЛГОЛа W нет различия в методах распределения памяти для структурных и других величин. Следует отметить, что PL/1 требует от программиста, чтобы он освобождал память, отведенную для величин CONTROLLED. Сбор мусора для них не производится.
Ссылка на компоненту состоит из последовательности имен узлов, которые ведут к ней, разделенных точками. Так, в первом примере этого раздела можно ссылаться на компоненту OLDEST1 посредством PERSON.CHILDREN.OLDEST1.
Когда компоненты являются массивами, нужно задавать требуемые индексы. В последнем примере, для того чтобы сослаться на компоненту Х(1, 2) структуры С(1), пишем А.С(1).Х(1, 2). Возможны и некоторые другие варианты. Чем больше массивов
ОРГАНИЗАЦИЯ ПАМЯТИ ВО =ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ' 215
появляется в определении структуры, тем больше работы приходится выполнять во время компиляции, чтобы создать, обратиться и уничтожить величины этого структурного типа.
Структуры данных по Стендишу (Стендиш [68])
Мы продвигаемся от простого к сложному — от структур данных, которые могут быть реализованы эффективно, к таким, для которых это невозможно, но которые богаче и мощнее. В своей диссертации Стендиш предлагает систему записи для структур, встроенную в АЛГОЛ 60, которая предусматривает, что структуры изменяются во время работы программы. Динамически могут изменяться не только размерности компонент, но и число компонент и их типы. Обычно во время компиляции ничего не известно, и все делается во время выполнения программы на основе описателей, которые сами строятся в это же время. Например, при выполнении процедуры TEMPLATE PROCEDURE
TEMPLATE PROCEDURE SEQ (n, t); INTEGER n;
TEMPLATE t; SEQ := n*[t];
создается описатель, описывающий структуру, состоящую из п компонент типа t. Выполняя
DI : = SEQ(2, REAL); D2 := SEQ(k, DI),
заносим в описательную переменную D1 описатель структурной величины, имеющий две компоненты REAL, и в D2 — описатель структурной величины, состоящий из к компонент, каждая типа D1. Имеется множество других способов построения описателей во время счета, кроме методов распределения памяти для структурных величин и создания величин. Мы не будем их здесь рассматривать; единственное, что нам хотелось отметить,— это то, что во время выполнения программы мы должны хранить описатель для каждой структурной величины. Действительно, этот описатель аналогичен набору элементов таблицы символов, используемому компилятором при компиляции, скажем, структур PL/1! Как мы увидим в гл. 10, такие описания структур лучше всего реализуются в виде дерева, где для каждого узла должно быть по крайней мере известно:
1)	концевой ли это узел или нет;
2)	если узел концевой, то каков его тип;
3)	если узел концевой, то указатель на значение, если таковое существует;
4)	если узел не концевой, то указатели на узлы для подкомпонент;
5)	размерности подкомпонент.
216
ГЛАВА S
Всякий раз при обращении к значению компоненты должен быть проинтерпретирован описатель. Начиная с корневого узла, находится путь к узлу, к которому обращаются, проверяется тип этого узла и, наконец, используется или изменяется его значение.
8.7. СООТВЕТСТВИЕ ФАКТИЧЕСКИХ И ФОРМАЛЬНЫХ ПАРАМЕТРОВ
Рассмотрим различные типы формальных параметров и их соответствие фактическим параметрам и покажем, как каждый из них может быть реализован. Под формальным параметром мы понимаем идентификатор в процедуре, который заменяется другим идентификатором или выражением при вызове процедуры. В подпрограмме на ФОРТРАНе, начинающейся с
SUBROUTINE А(Х, Y)
X и Y являются формальными параметрами. Эквивалентная процедура на АЛГОЛе имеет заголовок PROCEDURE А(Х, Y). При обращении к процедуре, скажем, А(В, C*D) устанавливается некоторым образом связь между формальными параметрами и фактическими параметрами В и C*D.
Когда в каком-нибудь языке происходит обращение к процедуре, ей передается список адресов аргументов. Процедура переписывает эти адреса в свою собственную область данных и использует их для установления соответствия фактических и формальных параметров. Кроме фактических параметров, часто имеется несколько неявных параметров, о которых программист не знает. Один из них это, конечно, адрес возврата. (Мы могли бы также включить в этот список все индекс-регистры, которые нужно сохранить; однако такие вопросы зависят от специфики машины, и мы не будем их касаться.) Мы увидим, какие необходимы другие, зависящие от языка параметры, когда будем исследовать различные типы параметров в различных языках.
Следовательно, вызываемой процедуре передается список такого вида:
неявный параметр 1
неявный параметр m
адрес фактического параметра 1
адрес фактического параметра п
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 217
Обратите, пожалуйста, внимание на то, что на самом деле не все параметры могут передаваться в таком списке; мы примем такую форму для удобства изложения. Например, адрес возврата обычно передается в каком-либо регистре.
Что представляют собой адреса в списке? Это зависит от языка и от типа параметра, и мы обсудим это позднее. Ниже перечислены типы параметров, которые мы будем рассматривать:
1)	вызов по ссылке;
2)	вызов по значению;
3)	вызов по результату;
4)	фиктивные аргументы;
5)	вызов по имени;
6)	имена массивов в качестве фактических параметров;
7)	имена процедур в качестве фактических параметров.
(Упражнения 2 и 3 предназначены для того, чтобы убедить читателя в том, что первые пять типов параметров различны в определенных контекстах.)
Вызов по ссылке (by reference)
Этот тип параметра самый простой для реализации. Фактический параметр обрабатывается во время выполнения программы перед вызовом; если он не является переменной или константой, оц вычисляется и запоминается во временной ячейке. Затем вычисляется адрес (переменной, константы или временной ячейки), И этот адрес передается вызываемой процедуре. Вызываемая процедура использует его для ссылки на ячейку (ячейки), содержащую значение. Например, предположим, что мы вызываем процедуру Р, определенную так:
(8.7.1)	PROCEDURE Р(Х); BEGIN...X := Х+5; ...END
с помощью вызова Р(А[11), где А — массив. Перед вызовом процедуры вычисляется адрес А{1] и заносится в список, который должен быть передан процедуре. Выполнение присваивания X := Х+5 внутри процедуры приводит к добавлению 5 к элементу АП1. Команды для этого присваивания могли бы выглядеть так:
1.	Загрузить в регистр 1 (например) адрес фактического параметра.
2.	Загрузить в регистр 2 (например) значение из 0(1);
3.	Прибавить 5 к регистру 2;
4.	Запомнить содержимое регистра 2 в ячейке 0(1), где 0(1) означает адрес ячейки 0+CONTENTS (регистр 1).
218
ГЛАВА 8
Вызов по значению (by value)
При этом типе соответствия формального и фактического параметров вызываемая процедура имеет ячейку, выделенную в ее области данных для значения формального параметра этого типа. Как и при вызове по ссылке, адрес фактического параметра вычисляется перед вызовом и передается вызываемой процедуре в списке параметров. Однако перед фактическим началом выполнения процедура выбирает значение по адресу и заносит его в свою собственную ячейку. Эта ячейка затем используется как ячейка для величины точно так же, как любая переменная, локализованная в процедуре. Таким образом, нет никакого способа изменить в процедуре значение фактического параметра.
Другими словами, параметр, вызываемый по значению, является локализованной в процедуре переменной, которая при входе в процедуру принимает значение соответствующего фактического параметра.
Предположим, что параметр X процедуры (8.7.1) вызывается по значению. Тогда соответствующая объектная программа могла бы выглядеть так:
загрузить в регистр 1 адрес фактического параметра
загрузить в регистр 2 значение из 0(1)
запомнить содержимое регистра 2 в X (в области данных этой процедуры)
г...
загрузить в регистр содержимое X; прибавить 5; запомнить в X
Начало процедуры:
Вызов по результату (by result)
В языке АЛГОЛ W (см. Бауэр и др. [68]) для любого формального параметра X, объявленного параметром RESULT, справедливо следующее:
1.	Для параметра X отводится ячейка в области данных процедуры. Эта ячейка используется в процедуре как локализованная ячейка для переменной X.
2.	Как и в случае параметра VALUE, при вызове процедуры вычисляется и передается адрес фактического параметра.
3.	Когда выполнение процедуры заканчивается, полученное значение X запоминается по адресу, описанному в п. 2.
ОРГАНИЗАЦИЯ ПАМЯТИ ВО бР'ЁМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 219
Другими словами, параметр RESULT есть переменная, локализованная в процедуре, значение которой при выходе запоминается в соответствующем фактическом параметре (который должен быть, конечно, переменной). Понятие RESULT было предназначено для того, чтобы дополнить в АЛГОЛе вызов по имени (который будет описан позднее), так как последний весьма неэффективен и обладает большими возможностями, чем это необходимо в большинстве случаев.
Параметр X в АЛГОЛ W может быть описан и как VALUE, и как RESULT; в этом случае локализованная переменная X возникает при входе в процедуру, а конечное значение X запоминается снова в фактическом параметре. В ФОРТРАН II все аргументы вызываются, по ссылкам; в ФОРТРАН IV формальный параметр вызывается как VALUE RESULT, но программист может указать, чтобы вызов осуществлялся по ссылке.
Фиктивные аргументы
В языке ФОРТРАН II обращение Р(3) к процедуре Р (8.7.1) приводит к необнаруживаемой ошибке. Так как ФОРТРАН передает адрес ячейки, которая содержит константу 3, выполнение присваивания Х = Х+5 изменяет саму константу на 8! С этого момента значение 8 используется вместо константы, значение которой должнр быть равно 3. PL/1 решает эту проблему, по-разному обрабатывая следующие фактические параметры:
1)	константы;
2)	выражения, которые не являются переменными;
3)	переменные, чьи характеристики отличаются от характеристик, указанных для соответствующих формальных параметров.
Для такого фактического параметра в вызывающей процедуре заводится временная переменная. Фактический параметр вычисляется и запоминается во временной переменной, и адрес этой ячейки передается в списке параметров. Таким образом, в приведенном выше примере константа 3 не изменится.
Такая временная переменная называется фиктивным аргументом. За исключением того, что временная переменная заводится процедурой вызова, фиктивный аргумент в PL/1 аналогичен параметру, вызываемому по значению в АЛГОЛе.
И есть еще одно различие в вызовах. В АЛГОЛе формальный параметр объявляется VALUE. В PL/1 вызывающая процедура выбирает, использовать ли фактический параметр как VALUE или как в ФОРТРАНе применить вызов по ссылке. Например, если написать P(J), фактический параметр J вызывается по ссылке; если написать P((J)), он вызывается по значению, или как фиктивный аргумент.
220
ГЛАВА 8
Вызов по имени
Согласно сообщению о языке АЛГОЛ, использование вызова параметра по имени означает буквальную замену имени формального параметра фактическим параметром. Так, если для процедуры
PROCEDURE R(X, I);
BEGIN I := 2; X := 5; I := 3; X := 1 END
задано обращение
(8.7.2)	R(B[J*2], J),
это должно было бы привести к изменению ее тела на BEGIN J := 2; B[J»2] := 5; J := 3; B[J*2] := 1 END
как раз перед ее выполнением.
Получить эффективную реализацию с помощью такой текстовой замены, конечно, нельзя, и мы должны разработать соответствующий эквивалентный способ. Заметим, что фактический параметр, соответствующий X, изменяется всякий раз, когда изменяется значение J, так что мы не можем просто вычислить адрес фактического параметра и использовать его; мы должны пересчитывать его каждый раз, когда мы обращаемся к формальному параметру внутри процедуры.
Обычный способ реализации вызова параметров по имени состоит в том, чтобы иметь отдельную программу или процедуру в объектном коде для каждого такого параметра. Для такой программы был введен Ингерманом [61 а] термин «санк» (thunk), и этот термин прижился. Когда происходит вызов санка, он вычисляет значение фактического параметра (если этот параметр не переменная) и передает адрес этого значения. В теле процедуры при каждой ссылке на формальный параметр происходит вызов санка, после чего для обращения к нужному значению используется переданный санком адрес.
Ниже поясняется программа для вызова (8.7.2). Следует упомянуть, что для правильной работы санка необходимо еще несколько неявных параметров; они будут описаны в разд. 8.9.
А1:
А2:
L: RET
Переход к L
санк, дня вычисления адреса B[3*2J
санк для вычисления адреса J
вызов R с этим списком
Список параметров для вызова из L
RET (адрес возврата) А1 (адрес первого санка) А 2. (адрес второго санка)
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 221
Различие между вызовом по ссылке и вызовом по имени заключается в следующем: адрес фактического параметра, вызываемого по ссылке, вычисляется только один раз, перед фактическим входом в процедуру, в то время как для вызова по имени адрес вычисляется всякий раз, когда в теле процедуры есть обращение к формальному параметру.
Имя массива в качестве фактического параметра
В этом случае и фактический, и формальный параметр должны быть массивами. Процедуре передается адрес первого элемента массива (для языков, которые не требуют информационных векторов) или адрес информационного вектора, и этот адрес используется процедурой для ссылки на элементы массива.
Имя процедуры в качестве фактического параметра
Передаваемый адрес — это адрес первой команды процедуры, которая встретилась в качестве фактического параметра.. Если это имя само является формальным параметром, то адрес был передан вызывающей процедуре, когда она вызывалась.
Это вся необходимая информация для вызовов в ФОРТРАНе. Вызовы в АЛГОЛе и PL/1 реализуются сложнее, и мы отложим их рассмотрение до разд. 9.8 об управлении памятью при выполнении программы на АЛГОЛе.
УПРАЖНЕНИЯ К РАЗДЕЛУ 8.7
1.	Напишите процедуру EXCHANGE(X, У) на языке высокого уровня для того, чтобы обменять значения фактических параметров X и Y (которые должны быть переменными). Какой тип должен быть у параметров X и У? Можно ли написать такую процедуру на АЛГОЛе? Указание: рассмотрите два вызова EXCHANGE(A[I], I) и EXCHANGER, А[11).
2.	Рассмотрите следующую программу на языке типа АЛГОЛ:
BEGIN INTEGER I; INTEGER ARRAY В [1:2];
PROCEDURE Q(X); INTEGER X;
BEGIN I : = 1; X :=X-f-2; В [I] : = 10;
I : =2; X : = X 4-2
END;
B[l] : = 1; В [2] : =1; I: = 1; Q (В [I])
END
222
ГЛАВА 8
Выполните программу 5 раз (и укажите, какие окончательные значения получат ВЦ] и BI2]), рассматривая формальный параметр X как параметр, вызываемый (1) по ссылке, (2) по значению, (3) как VALUE RESULT, (4) как RESULT и (5) по имени. Указание: в результате одного из вызовов получится неопределенное значение; во всех других случаях результаты различны!
3.	Выполните программу из упражнения 2, предполагая, что X — аргумент типа PL/1. Затем замените вызов процедуры Q(B[I]) на Q((B[I])) и снова выполните программу. Отличаются ли конечные значения I, ВЦ] и В[2]?
8.8.	УПРАВЛЕНИЕ ПАМЯТЬЮ ДЛЯ ФОРТРАНа
Язык ФОРТРАН не допускает вложенных подпрограмм, рекурсивных подпрограмм или динамических массивов. Так как вложенные подпрограммы отсутствуют, единственные переменные, к которым можно обращаться,— это переменные, локализованные в программе (в ее области данных) и переменные в области COMMON. Область COMMON статическая, ей отводится фиксированная последовательность ячеек перед выполнением программы; следовательно, ссылки на переменные в области COMMON могут быть абсолютными ссылками на машинные ячейки.
Каким же образом обращаться к локализованным переменным? Отсутствие рекурсивных подпрограмм означает, что каждая подпрограмма должна быть завершена перед тем, как она может быть снова вызвана. Поэтому ее область данных тоже может быть статической. Таким образом, все адреса переменных в ФОРТРАНе могут быть абсолютными адресами машинных ячеек.
Так как динамическое распределение памяти для массивов не допускается (все границы, указанные в инструкции DIMENSION, должны быть константами), нет необходимости в управлении памятью при выполнении программы. Компилятор может разместить массивы в области данных фиксированной длины.
Из всего этого следует, что дисплеи фактически не нужны. Вспомним, что дисплеи необходимы, чтобы сохранить адреса областей данных, на которые могут быть ссылки в любой точке программы. Но мы только что увидели, что в ФОРТРАНе все ссылки — абсолютные.
Рисунок 8.4 иллюстрирует типичную статическую область данных в ФОРТРАНе. Неявные параметры включают в себя адрес возврата и то, что диктуется конструкцией машины (регистры, сумматоры и т. д.). Вызов подпрограммы состоит из следующих действий:
1.	Вычислить адреса фактических параметров и запомнить их в списке параметров (в области данных вызывающей подпрограммы).
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 223
2.	Передать адрес списка параметров в некоторой согласованной глобальной ячейке (например, в регистре).
3.	Занести адрес возврата в список и передать управление.
Перед выполнением подпрограмма должна:
1.	Перенести неявные параметры в свою область данных.
Неявные параметры
Фактические параметры
Простые переменные, массивы, временные переменные
Рис. 8.4. Типичная область данных подпрограммы на ФОРТРАНе.
2.	Перенести адреса фактических параметров в свою область данных.
Возврат из подпрограммы состоит в восстановлении сохраненных значений неявных параметров и переходе по адресу возврата.
8.9.	УПРАВЛЕНИЕ ПАМЯТЬЮ ДЛЯ АЛГОЛа
АЛГОЛ имеет блочную и процедурную структуру, которая требует управления памятью при счете. Однако оно может быть реализовано с помощью довольно простой схемы. АЛГОЛ не допускает указателей в качестве переменных, что является одной из главных причин возрастания сложности распределения памяти. Кроме того, вложенные блоки и процедурная структура позволяют использовать простой механизм для распределения памяти — хорошо знакомый нам стек.
В общих чертах работа ведется так. В качестве стека используется последовательность смежных ячеек, которые вначале пусты. С каждым выполнением процедуры связана одна динамическая область данных фиксированной длины для ее переменных, информационных векторов и т. д. (Мы рассматриваем главную программу как процедуру без параметров, вызываемую системой.) Когда процедура вызвана, она забирает под свою область данных необходимый объем памяти в текущей вершине стека; когда выполняется возврат из процедуры в место вызова, все использованные ячейки «выталкиваются» из стека- Блоки работают таким же образом, так как они вложены друг в друга и в процедуры. При входе в блок выделяется память из вершины стека для области данных, чтобы разместить элементы каждого массива; при выходе из блока ис
224
ГЛАВА 8
пользованные ячейки освобождаются. Ввиду простоты этой схемы нам в действительности не нужны программы GETAREA и FR EEAREA; выделение и освобождение памяти может быть выполнено с помощью нескольких команд, без обращения к подпрограмме.
Нам необходимо обеспечить, чтобы связь между процедурами и обращения к глобальным переменным выполнялись правильно; именно это мы рассмотрим в данном разделе. Описываемый метод,
BEGIN...
PROCEDURE А (X, Y); PROCEDURE X; INTEGER Y; BEGIN PROCEDURE В (Z); PROCEDURE Z;
BEGIN ARRAY F [1:10];
L4:	Z(F[1]+Y);
END;
L3:	В (X);
END;
PROCEDURE C;
BEGIN ARRAY G [1:10];
PROCEDURE D (W); INTEGER W;
BEGIN
L5:	G[1]:=W;
END;
L2:	A (D, 1);
END;
LI: C;
END;
Рис. 8.5. Структура вложенных и параллельных процедур.
возможно, более сложен, чем другие, встречающиеся в литературе методы. Оправданием этому является наше желание сделать входы в блок и выходы из блока по возможности более эффективными. В упражнениях в конце раздела мы затронем другие схемы.
Чтобы проиллюстрировать работу стека, предположим, что у нас имеется программа, структура которой изображена на рис. 8.5. Сначала выделяется память для области данных главной программы. Предположим, что выполняется вызов процедуры С (метка L1). Тогда выделяется область данных для С, за которой следует область для массива G. При вызове процедуры А (метка L2) мы выделяем память для области данных процедуры А. К моменту, когда про-
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 225
грамма доходит до метки L3 складывается следующее распределение памяти:
Область данных процедуры А	Вершина стека
Область данных для массива G	
Область данных процедуры С	
Область данных главной программы	Дно стена
В этом разделе мы рассмотрим по порядку следующие вопросы:
1)	связь между активным дисплеем и областью данных;
2)	формат областей данных процедуры;
(8.9.1)	3) вход в блок, описания массивов и выход из блока;
4)	вызов процедуры, имя которой не является формальным параметром;
5)	инициализация процедуры и выход из нее;
6)	санки и как их вызывать;
7)	вызов процедуры, являющейся формальным параметром;
8)	фактические параметры — имена процедуры;
9)	передачи управления.
Связь между активным дисплеем и областью данных
В реализации АЛГОЛа для каждого выполнения процедуры выделяется динамическая область данных (главную программу мы также называем процедурой). В любой момент времени в процессе
Адрес области, данных главной, программы (уровень 0)
Адрес области данных для процедуры уровня 1
Адрес области данных для процедуры уровня t
Рис. 8.6. Активный дисплей при выполнении процедуры уровня i.
выполнения могут быть несколько процедур, но только одна будет действительно активной; остальные будут ожидать ее завершения и возврата. Дисплей и область данных, связанные с активной процедурой, также называются активными. Если активная процедура имеет уровень вложения i, она может обращаться, кроме своих
8 Д. Грис
226
ГЛАВА 8
собственных, к переменным в процедурах уровней 1,2, ..., i—1 и в главной программе. Активный дисплеибудет следовательно иметь вид, приведенный на рис. 8.6.
Где должны находиться сами дисплеи? Часто принято иметь для них отдельный стек. Мы выберем другой вариант: так как с каждой областью данных связан дисплей, мы помещаем его в первые несколько ячеек этой области. Таким образом, адрес активного дисплея всегда является адресом активной области данных, и наоборот. Кроме того, мы предположим, что глобальная ячейка (например, индексный регистр), называемая ACTIVE AREA, всегда содержит адрес активного дисплея (и области данных) для выполняемой в настоящее время процедуры.
Все обращения к данным из активной процедуры выполняются с использованием ячейки ACTIVE ARE А и дисплея, на который она ссылается.
Области данных процедуры
Каждая область данных процедуры имеет фиксированную длину, определяемую во время компиляции. Ниже мы приводим список данных в каждой области в порядке расположения. Смысл некоторых элементов списка пока не ясен, в свое время мы их рассмотрим.
L Дисплей для процедуры.
2.	Ячейка, называемая STACKTOP. Она содержит адрес вершины стека, который получается, сразу после выделения области для данной процедуры, т. е. содержит адрес последней ячейки в этой области данных.
3.	Четыре неявных параметра процедуры:
а)	адрес возврата;
Ь)	адрес дисплея фактических параметров;
с)	адрес глобального дисплея;
d)	адрес вершины стека в момент вызова.
4:	Собственно фактические параметры (или их адреса).
5.	Для каждого блока в процедуре:
а)	ячейка STACKTOP, которая должна содержать адрес вершины стека во время работы блока;
Ь)	ячейки для простых переменных;
с)	ячейки информационных векторов для массивов, описан-ч ных в; блоке;
d)	ячейки для необходимых в блоке временных переменных.
Порядок ячеек для простых переменных, информационных векторов и временных переменных в блоке не имеет значения.* Самый легкий способ — это распределить память, следуя порядку описаний в блоке. Так как нельзя обращаться к переменным из
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 227
параллельных блоков в одно и то же время, параллельные блоки могут использовать одни и те же ячейки, но это не обязательно. Рассмотрим процедуру на рис. 8.7 с внутренними блоками и с нумерацией для этих блоков, приведенной справа. Ниже дается
ДИСПЛЕЙ (адреса, областей, данных главной программы а процедуры А)	
STACKTOP для процедуры к (адрес последнего слова в области данных процедуры К )	
Неявные параметры	
Фактические параметры X u Y	
STACKT0P блока 1, переменная Z t информационный вектор для В	
STACKTOP блока, 2, переменные D и Е •	STACKTOP блока 3, информационный вектор для А
	STACKTOP блока 4 t переменная Е
PROCEDURE А( X, Y) ; INTEGER X, Y; Lit BEGIN REAL Z; ARRAY B[X:Y];
L2: BEGIN REAL D, E ;
L3:	: .
END;
L4: BEGIN ARRAY A[ 1 : X];
L5: BEGIN REAL E;
L6:	:
END;
L7: END;
L8: END;
Рис. 8.7; Программа на АЛГОЛе, имеющая блочную структуру.
область данных для нее. Ячейки для переменных в блоке 2 совмещены с ячейками для переменных в блоке 3 и 4.
А дресац ия переменной
Предположим, что в процедуре уровня i встречается обращение к переменной или формальному параметру. Если это обращение к локализованной переменной или параметру, описанному в процедуре, их адрес во время выполнения программы есть
(смещение в области данных)+ACTIVE ARE А
Если переменная или параметр описаны в процедуре уровня к, содержащей данную процедуру, их адрес будет
(смещение в области данных)+CONTENTS( ACT IVEAR ЕА+к)
8*
228
ГЛАВА 8
Мы часто можем использовать номер уровня к вместо номера области данных в описании каждого идентификатора. Процедуры одного и того же уровня будут иметь один и тот же номер, но они никогда не будут обрабатываться компилятором одновременно. .
Вход в блок, описания массивов, выход из блока
Для каждого блока выделяется последовательность ячеек для простых переменных, информационных векторов и временных ячеек, используемых в блоке. Память для массивов должна быть выделена при входе в блок и освобождена при выходе из него» Рассматриваемая схема предназначена для минимизации работы при входе в блок и выходе из него. Действительно, вход в блок состоит из двух команд, в то время как выход из блока не требует абсолютно никаких команд!
Как упоминалось ранее, память для массивов при счете следует размещать в вершине стека. Мы устанавливаем следующее правило: когда выполняется блок, ячейка STACKTOP блока содержит адрес вершины стека для этого, блока. Это означает:
(8.9.2)	Вершина стека во время выполнения программы всегда определяется ячейкой STACKTQP активного блока — блока, который выполняется в настоящее время.
Напомним, что в каждой области данных процедуры имеется ячейка STACKTOP, которая содержит адрес вершины стека после размещения этой области данных. Эту ячейку можно считать ячейкой STACKTOP для фиктивного блока, объемлющего тело процедуры.
Чтобы следовать правилу (8.9.2), мы должны, входя в блок, скопировать ячейку STACKTOP объемлющего блока (фиктивного блока при входе в главный блок процедуры) в ячейку STACKTOP блока, в который мы входим. Это делает вход в блок весьма эффективным.
Следующий шаг состоит в распределении памяти для массивов, описанных в блоке. Для каждого массива выполняется следующее:
1)	вычисляются нижняя и верхняя границы;
2)	вызывается программа размещения массива; ее параметры:
а)	нижняя и верхняя границы;
Ь)	адрес информационного вектора;
с)	адрес ячейки STACKTOP для блока.
Программа размещения массива вычисляет всю необходимую информацию и заносит ее в информационный вектор. Затем она занимает в стеке нужное число ячеек, используя третий параметр, указывающий текущую вершину стека. Наконец, она заносит в третий параметр новый адрес вершины стека.
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 229
Здесь может возникнуть вопрос, почему для каждого блока необходима отдельная ячейка STACKTOP. Это становится ясным при рассмотрении выходов из блоков. Если была бы лишь одна глобальная ячейка STACKTOP, то каждый раз, когда происходил бы выход из блока, эта ячейка должна была изменяться, чтобы вытолкнуть из стека память для описанных в этом блоке переменных. Это вызывает осложнения при переходах через границы нескольких блоков и т. д. Для используемой нами схемы в этом нет необходимости, потому что справедливо (8.9.2); для обычного выхода из блока ничего не нужно делать.
Приведем пример работы этого механизма блочной структуры. На рис. 8.8, а показано состояние стека вто время, когда произошел вызов и инициализация процедуры А (рис. 8.7), а выполнение еще не началось (в метке L1). На рис. 8.8, b показано состояние стека после входа в главный блок, но перед размещением каких-либо массивов. Обе ячейки STACKTOP указывают на вершину стека. Затем (рис. 8.8, с) размещается массив и изменяется ячейка STACK-TOP для блока 1, в нее заносится адрес последней ячейки стека, которая является последней ячейкой массива В. Заметим, что ячейка STACKTOP для процедуры А не изменяется. Когда происходит вход в блок 2 (рис. 8.8, d), STACKTOP для блока 1 копируется в STACKTOP для блока 2. Поскольку в блоке 2 нет описания массивов, то, кроме этого, ничего не надо делать.
Теперь выходим из блока 2. Мы вернемся снова к рис. 8.8, с, так как переменные из блока 2 больше не являются активными. Заметим, что для этого выхода из блока на самом деле никакие команды не выполняются. Значения D, Е и STACKTOP блока Ь2 все еще в области данных, но к ним больше не будет обращейий.
При входе в блок 3 мы копируем ячейку STACKTOP и выделяем память для массива А, как это изображено на рис. 8.8, е. Теперь существуют три ячейки STACKTOP, указывающие на вершины стека: в блоке 3 (активном блоке), в блоке 1 и в блоке 0 — в самой процедуре. При входе в блок 4 мы снова копируем STACKTOP (рис. 8.8, f). Затем мы выходим последовательно из нескольких блоков; из блока 4 (рис. 8.8, е), из блока 3 (рис. 8,8, с) и из блока 1 (рис. 8.8, а).
Вызов процедуры
Код вызова процедуры состоит из санков для параметров, вызываемых по имени, команд, которые формируют список параметров и команд фактического перехода к процедуре. Формат санков мы опишем позднее. Как упоминалось ранее, когда рассматривался дисплей, список параметров состоит из фактических параметров и следующих четырех неявных параметров:
230
Массив В
		-							
				Z ,инф.векторм. В				Z; инф. вектор м. В	
				STACKTOP бл. 1 -				STACKTOP Лт.1 -	
	параметры			Параметры				Параметры	
	STACKTOP пр. А			 STACKTOP пр. А				 STACKTOP лдА	
	ДИСПЛЕЙ			ДИСПЛЕЙ				ДИСПЛЕЙ	
		•			‘ *				
<ь И	6	С L2,L4,L8
Рис. 8.8. Механизм блочной структуры.
Е
Массив А
Массив В
STACKTOP Л7.4-
Инф. вектор м. А
STACKTOP Л?.3
Z , инф.векторм. В
STACKTOP 67.1
Параметры
STACKTOP пр. А
. ДИСПЛЕЙ
е L5, L7
f L6
Рис. 8.8. Механизм блочной структуры (продолжение).
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 23!	*
£
1)	адрес возврата;	J
2)	адрес дисплея фактических параметров;	?
3)	адрес глобального дисплея;
4)	текущее значение STACKTOP.	'	j
Второй неявный параметр — это значение ACTIVEAREA в	j
момент обращения к процедуре. Он определяет область, в которой	J
находятся фактические параметры. Это значение сохраняется и \ используется в санках для обращения к нужным переменным. Это I PROCEDURE В;	PROCEDURE A;	j
BEGIN PROCEDURE A; BEGIN...END;	-
BEGIN...END;	PROCEDURE B;
LI: A;	,	BEGIN...LI: A; ...END; I
‘END; '	.	•	:	I
a	> b	
Рис. 8.9. Процедура В, вызывающая процедуру А.	।
I
же значение используется для восстановления ACTIVE AREA	S
при выходе из процедуры для того, чтобы вернуться *к работе с тем же дисплеем, который был в точке вызова.
Пока мы будем считать, что в данном обращении к процедуре	;
имя ее не является формальным параметром. При этом ограни-	;
чении второй и третий параметры совпадают. Третий параметр —	|
адрес глобального дисплея используется при формировании дисплея	§
для вызываемой процедуры. Поясним это.	[
Предположим, что мы вызываем процедуру А из процедуры В J (или из главной программы). Здесь возникают два случая. Прежде всего процедура А может быть описана внутри процедуры В (рис. | 8.9, а). Предположим, что уровень В равен i. Тогда уровень про- I цедуры А равен i+1. Она может обращаться к любой переменной,	J
глобальной по отношению к В, и к любой переменной, описанной	S
в блоке процедуры В, объемлющем описание А. Таким образом, первые i+1 ячейки дисплея для процедуры А те же, что и для процедуры В. При построении дисплея для процедуры А следует только переписать эти i+1 ячейки, начинающиеся с адреса, заданного в третьем параметре, т. е. адреса глобального дисплея.
Во втором случае (рис. 8.9, Ь) А описана вне В, но возможно - обращение из В к А; следовательно, А описана в блоке, объемлющем и В. Если А имеет уровень i+1, то и из А, и из В возможны обращения к переменным в процедурах уровня 0, 1, ..., i. Как и в первом случае, для построение дисплея процедуры А требуется только переписать первые i+1 ячеек, начиная с адреса глобального дис- ; плея. .	I
232
ГЛАВА 8
Четвертый неявный параметр — текущее значение STACKTOP — является* текущим адресом вершины стека, начиная с которого вызванной процедуре будет выделена память. В предположении, что данный вызов процедуры произошел не из санка, это значение совпадает со значением STACKTOP для блока, в котором возникло обращение к процедуре.
Инициализация процедуры и выход из нее
Когда происходит вызов процедуры, должна быть выделена память для ее области данных, инициализирован ее дисплей и неявные и фактические параметры должны быть перенесены в ее область данных. Точнее, для процедуры i+1-го уровня вложения нужно выполнить следующие действия:
1.	Продвинуть стек на величину области данных процедуры (четвертый неявный параметр указывает текущую вершину стека). Запомнить адрес области данных в ячейке i+1 области данных (в дисплее) и в глобальной ячейке ACTIVEAREA. Запомнить адрес новой вершины стека в STACKTOP для процедуры.
2.	Перенести четыре неявных параметра в эту область данных.
3.	Скопировать i+1 ячейку, начиная с адреса, указанного в третьем неявном параметре, в ячейки.О, 1, ..., i этой новой области данных.
4.	Перенести фактические параметры следующим образом:
а)	для имен массивов: скопировать информационный вектор;
Ь)	для имен процедур: скопировать_ адрес фактического параметра (для чего это делается, будет объяснено позднее);
с)	для параметров, вызываемых по имени: скопировать адрес санка;
d)	для параметров, вызываемых по значению: записать значение Но адресу фактического параметра в новую область данных.
При выходе из процедуры следует восстановить состояние, которое было при вызове, и затем осуществить возврат. Кроме того, если процедура является функцией, значение функции должно быть передано при помощи регистра. Восстановить состояние легко; нужно просто переслать в глобальную ячейку ACTIVE AREA второй неявный параметр процедуры. Отметим снова, что ничего не нужно делать для перенастройки указаний на вершину стека. Казалось, что перенастройка нужна, поскольку область данных процедуры больше не используется, но ячейка STACKTOP, содержащая адрес вершины стека (по правилу (8.9.2)), определена активным блоком, а он изменен.
ОРГАНИЗАЦИЯ ПАМЯТИ ВО ВРЕМЯ ВЫПОЛНЕНИЯ ПРОГРАММЫ 233
Санки и как их вызывать
Как описывалось в разд. 8.7, санк — это программа, которая вычисляет и передает адрес фактического параметра для соответствующего формального параметра, вызываемого по имени. Так как фактический параметр — это выражение, санк не содержит ни описаний, ни блоков. Следовательно, он может использовать для своих временных переменных ячейки в том блоке, где появилось обращение к процедуре. Особое внимание следует обратить на вызовы функций, появляющиеся в санке (в выражении, которое представляет собой фактический параметр), и на этом мы остановимся. Но вначале давайте рассмотрим вызов санка и собственно санк.	•	v.	_
Для каждого обращения к формальному параметру X, вызываемому по имени, порождается вызов санка со следующим списком параметров:
1)	адрес возврата;
2)	текущее значение в ACTIVEAREA;
3)	значение второго неявного параметра процедуры с формальным параметром X;
4)	текущий адрес вершины стека.
Как вы_ можете видеть, это очень похоже на вызов пр