Текст
                    THE SYSTEMS PROGRAMMING SERIES
СомрИег Design Theory
PHILIP M. LEWIS II
DANIEL J. ROSENKRANTZ
RICHARD E. STEARNS
General Electric Company
k ADDISON-WESLEY PUBLISHING COMPANY
* Reading, Massachusetts • Menlo Park, California
London • Amsterdam • Don Mills, Ontario • Sydney
1976


Ф. ЛЬЮИС, Д. РОЗЕНКРАНЦ, Р. СТИРНЗ Теоретические основы проектирования компиляторов Перевод с английского В. А. Исаева В. С. Нумерова Н. П. Терновой Под редакцией В. Н. Агафонова ИЗДАТЕЛЬСТВО «МИР» Москва 1979
УДК 519.685.1 В книге ичг-естных американских специалистов излагаются математические понятия и методы теории автоматов и формальных грамматик, лежащие в основе проектирования компиляторов, и показывается, как их применять на практике. Применение теории детально продемонстрировано на примере компилятора для учебного языка программирования. Разработанный авторами метод позволил им включить в синтаксический блок значительную часть того, что обычно относится к семантике (генерации кода). Изложение строгое, но не формальное, доступное читателю, не имеющему специальной математической подготовки. Книга рекомендуется широкому кругу системных программисте!1 и студентов соответствующего профиля (особенно инженерных вузов). Редакция литературы по математическим наукам 2405000000 20205-027 © 1976by Addison-Wesley Publishing Company, In 041 (01)-79 © Перевод на русский язык, «Мир», 1979 •
От редактора перевода Приятно рекомендовать читателю хорошую книгу о том, как математическая теория может служить основой практических разработок (в данном случае связанных с проектированием трансляторов). Ее авторы одновременно и теоретики, известные первоклассными работами по теории формальных языков, и практики, построившие не один компилятор. Таким образом, читатель получает «из первых рук» и теоретическую модель, и рекомендации, как ее воплотить в программу. Этим книга отличается от всех других, посвященных проблемам трансляции. Она написана как учебник для студентов, обладающих скромными математическими навыками. Начиная всегда с примеров и содержательных пояснений, авторы очень заботливо подводят читателя к математически точным понятиям, не пугая его изощренным формализмом. С методической точки зрения книга поучительна и для преподавателей вузов, особенно технических. Что касается специалистов-практиков, им будет интересно прочесть о разработанном авторами нисходящем методе обработки языков на основе L-атрибутной грамматики, о восходящих методах, основанных на понятиях простой ССП-грам- матики и SLR (1)-грамматики. Каждому методу сопутствует обсуждение техники обработки ошибок, которая до сих пор остается скорее искусством, чем наукой. Специалисту-теоретику будет интересно посмотреть, как теоретический «костяк» обрастает программной «плотью», в которой постепенно проявляются контуры работающей системы. Несколько слов о терминологии. Во-первых, мы решили переводить на русский названия процедур, переменных и т. п., что, возможно, облегчит работу с ними. Насколько удачно это сделано, можно судить по предметному указателю. Во-вторых, говоря об автоматах и грамматиках, мы следовали терминологии, принятой п теории формальных языков. Поэтому, в частности, популярный у программистов стек в большей части книги называется магазином. Мотивировка некоторых терминов дана в примечаниях. Работа над переводом была распределена так: Н. П. Терновая перевела гл. 1 — 6 и приложение А, В. А. Исаев —гл. 7—10 и разделы В.1—В.7, В. С. Нумеров— гл. 11—15, приложение Б и разделы В.8—В.11. В. Н. Агафонов
От редакционного бюро IBM1' Область системного программирования возникла как результат усилий многих программистов и менеджеров, чья творческая энергия воплотилась в практически полезных системных программах, потребность в которых остро ощущалась в быстро развивающейся вычислительной индустрии. Программирование было искусством — каждый программист решал стоящие перед ним задачи по-своему, влияние со стороны других специалистов, занимавшихся аналогичными вопросами, было незначительным. В 1968 г. покойный Эшер Оплер, работавший тогда в IBM, высказал мнение, что знания, накопленные в программировании, необходимо объединить в форме, приемлемой для всех системных программистов. Изучив состояние дел в этой области, он пришел к выводу, что попытка систематического обобщения материала будет вполне оправданна. По его рекомендации фирма IBM приняла решение финансировать издание «Серии системного программирования». Цель этого долгосрочного проекта — собрать, систематизировать и опубликовать те принципы и методы, которые надолго сохранят свою актуальность в вычислительной технике. Серия будет состоять из взаимосвязанных книг учебно-справочного характера. Содержание каждой книги должно отражать точку зрения индивидуального автора, которая не обязательно совпадает с точкой зрения корпорации IBM. Каждая книга организуется как учебный курс, однако материал описывается достаточно подробно, чтобы ей можно было пользоваться как справочником. Серия имеет три уровня: нижний составляют тома, содержащие вводный материал, средний — тома, посвященные математическому обеспечению и содержащие материал более узкого профиля, и, наконец, верхний уровень — специальные теоретические работы. Организованная таким образом Серия будет полезна и новичкам, и опытным программистам, и теоретикам. В целом Серия отражает положение дел в области системного программирования и может послужить хорошей базой для этой дисциплины. ]) Состав редакционного бюро IBM: Joel D. Агоп, Chairman Paul S. Herwitz Richard P. Case James P. Morrissey Gerhart Chroust Asher Opler Edgar F. Codd George Radin Robert H. Glaser David Sayre Charles L. Gold Norman A. Stanton (Addison-Wesley) James Greismer Heinz Zemanek
7 Серия включает следующие книги: Уровень 1: Агоп Joel D., The Program Development Process. Part 1 — The Individual Programmer. Агоп Joel D., The Program Development Process. Part II — The Programming Team. Nichols John E., The Design and Structure of Programming Languages. Beckman Frank, Mathematical Background of Programming. Mills Harlan D., Linger Richard C, Structured Programming. Withington Frederic G., Gardner George, The Environment for Systems Programs, Уровень 2: Date С J., An Introduction to Database Systems. Van Dam Andries, Interactive Computer Graphics. Lorin Harold, Sorting and Sort Systems. Lewis Philip P>\., Rozenkrantz Daniel J., Stearns Richard E., Compiler Design Theory- Уровень 3: Burge William, Recursive Programming Techniques. Sowa John F,, Conceptual Structures: Information Processing in Mind and Machines,
Нашим женам — Предисловие Эта книга задумана как учебное пособие для полугодового или годового курса по проблемам построения компиляторов. В ней излагается математическая теория, лежащая в основе построения компиляторов и других процессоров, предназначенных для обработки языков, и показывается, как применять эту теорию на практике. Применяемые математические понятия взяты из теории автоматов и формальных грамматик. Эти понятия излагаются строго, но неформально, чтобы сделать их доступными широкому кругу, читателей, включая тех, кто не привык к математическому стилю изложения. Мы считаем, что идеи теории автоматов и формальных языков служат прекрасной основой как для обучения построению компиляторов, так и для реальной их разработки. Мы сами построили два компилятора, основываясь на этой теории. При отборе и изложении материала большое внимание было уделено именно «переводу», а не просто «разбору». Для описания того, как при обработке языков различными процессорами вход преобразуется в выход, используется формальное понятие синтаксически управляемого атрибутного перевода. Другое понятие, на котором мы заостряем внимание,— это понятие «автомата». Такие автоматы, как конечный автомат или автомат с магазинной памятью, служат основными строительными блоками при создании компилятора. Мы описываем процедуры синтеза автомата, который должен выполнять требуемый перевод (трансляцию). Материал данной книги содержит достаточно полную теорию, необходимую для построения лексического и синтаксического блоков компилятора. Применение атрибутной трансляции позволило нам включить в построение синтаксического блока значительную часть того, что часто называют «генерацией кода» или «семантикой». В книгу включен также дополнительный материал по генерации кода и краткий обзор по оптимизации кода. Хотя проблемы организации рабочей программы очень важны при решении вопроса о том, каким должен быть код, генерируемый компилятором, мы не рассматриваем их в нашей книге, так
Предисловие 9 как считаем, что они не относятся к «теории построения компиляторов», а являются, скорее, темой отдельного курса, в котором специально рассматриваются структуры языков программирования. В этой книге излагаются лишь те аспекты теории автоматов, которые имеют отношение к построению компиляторов, поэтому некоторые из ее основных понятий опущены, и, следовательно, книгу нельзя использовать как исчерпывающий курс по теории автоматов. Однако студенты, прослушавшие курс по этой книге, впоследствии гораздо легче воспримут курс теории автоматов. И наоборот, знание основ теории автоматов поможет студентам быстрее освоить материал, изложенный в этой книге; таким образом, данный учебник можно использовать как до, так и после того, как прослушан вводный курс по теории автоматов. Для понимания книги требуется лишь знакомство с языками программирования и математические навыки, какими обычно владеют студенты технических вузов. После вводной главы в гл. 2—4 рассматриваются конечные автоматы, а также другие вопросы, имеющие отношение к лексической обработке языков. В гл. 5 и 6 вводятся автоматы с магазинной памятью и контекстно-свободные грамматики. Если студенты уже прослушали вводный курс по теории автоматов, большую часть материала, содержащегося в гл. 2—6, можно опустить, а остальную часть изложить очень быстро, акцентируя внимание на применении теории к построению компиляторов. В любом случае можно опустить разд. 2.7—2.11. В гл. 7 вводятся понятия перевода (трансляции) и атрибутного перевода; этим материалом нужно овладеть, прежде чем двигаться дальше. В гл. 8—10 рассматриваются нисходящие методы обработки языков (сверху вниз по дереву разбора), а гл. 11—13 посвящены восходящим методам (снизу вверх). Эти части книги независимы, и можно ограничиться изучением лишь одной из них. В любом случае можно опустить разд. 8.7, 10.5, 12.6 и 13.6. Чтобы продемонстрировать применение теории в «реальной» ситуации, в книге проводится построение компилятора для подмножества языка BASIC'). С одной стороны, избранный нами язык достаточно сложен, чтобы проиллюстрировать на нем приведенные в книге понятия, с другой стороны, можно обойтись при этом тривиальной организацией рабочей программы (поскольку эффективная реализация всех свойств языка, как уже отмечалось, яв- 1) Язык BASIC был создан специально для обучения студентов программированию и получил широкое распространение в вузах США. Изучить его можно по книге: Кет ков Ю. Л. Программирование на БЭЙСИКе, Практическое пособие.— М.: Статистика, 1978,— Прим. ред.
10 Предисловие ляется предметом отдельного разговора). Этот язык обладает разнообразными синтаксическими и семантическими свойствами, включая синтаксически рекурсивную структуру управления (цикл). В гл. 4 мы строим лексический блок компилятора, а в гл. 10 и 12 — синтаксические блоки, осуществляющие обработку нисходящим и восходящим методами соответственно. В гл. 14 разрабатывается генератор кода. На практических занятиях можно построить компилятор в данном или расширенном виде. В гл. 15 приведен краткий обзор по оптимизации кода. В книге имеется также три приложения: приложение А — это руководство по языку MINI-BASIC; в приложении Б рассматриваются некоторые математические отношения, необходимые для различных процедур проверки и построения; в приложении В содержатся несколько методов приведения грамматики данного языка программирования к одной из указанных в книге специальных форм. Материал, изложенный з данной книге, в течение нескольких лет использовался при чтении полугодового курса в Ренсселеров- ском политехническом институте (г. Трои, штат Нью-Йорк) и в Университете штата Нью-Йорк (г. Олбани). Он читался также как полугодовой спецкурс учащимся Объединенного колледжа в Скенектади (штат Нью-Йорк). Нам хочется поблагодарить слушателей, чьи недоумевающие взгляды заставили нас несколько раз переделывать курс. Хочется также выразить признательность тем, кто читал первоначальные варианты рукописи и сделал полезные замечания: Джону Хатчисону, Майклу Хэммеру, Стефену Моурзу, Джону Джонстону, Донне Филипс, Даниэлю Берри, Алисе Орн, Гэри Фишеру, Уолтеру Стоуну, Джеймсу Робертсу и Роберту Блину. Мы благодарны руководству Исследовательского центра компании Дженерал электрик, и в особенности Ричарду Л. Шайе и Джеймсу Л. Лоусону, которые создали прекрасные условия для нашей работы и предоставили нам время и возможность для написания этой книги. Скенектади Декабрь 1975 г. Авторы
1 Введение 1.1. Программы для обработки языков В общении человека и машины существуют естественные трудности. Машины на атомарном уровне оперируют битами и регистрами, а люди изъясняются на естественных языках (например, русском или английском) или пользуются математическими обозначениями. Обычно этот разрыв преодолевается с помощью искусственного языка, позволяющего употреблять строго определенное множество слов, предложений и формул, которые машина может «понять». Чтобы общение с машиной стало возможным, человек получает руководство для пользователя, в котором объясняются допустимые в языке конструкции и значения, а для вычислительной машины создается программное обеспечение, с помощью которого она может воспринимать последовательности битов, представляющие команды или программы, написанные человеком на искусственном языке, и переводить их во внутренние битовые структуры, необходимые для исполнения того, что было задумано человеком., Существующие языки для общения с вычислительными машинами очень различны по сложности, например: машинный язык — набор команд конкретной вычислительной машины, который интерпретируется на аппаратном уровне или с помощью микропрограмм самой машины; языки ассемблера, или языки «низкого уровня», которые в значительной мере отражают набор команд некоторой конкретной машины; языки управляющих карт и директивные языки, которые используются для связи с операционной системой; языки «высокого уровня», такие, как Фортран, Алгол, ПЛ/1, Лисп и т. д., которые имеют сложную структуру и не зависят ни от набора команд, ни от операционной системы конкретной машины. Программу для вычислительной машины, позволяющую ей «понимать» директивы и предложения входного языка, используемого программистом, мы будем называть «языковым процессором».
12 Гл. 1. Введение Вообще говоря, существует два типа таких программ для обработки языков: интерпретаторы и трансляторы. Интерпретатор — это программа, которая допускает в качестве входа исходную программу, записанную на языке, называемом исходным языком, и производит вычисления, предписываемые этбй программой. Транслятор — это программа, которая допускает в качестве входа программу на исходном языке, а в качестве выхода выдает другую версию этой программы, написанную на другом языке, который называется объектным языком. Объектный язык обычно является машинным языком некоторой вычислительной машины, причем в этом случае программу можно сразу же выполнять. Существует довольно условное деление трансляторов на ассемблеры и компиляторы, которые транслируют соответственно языки низкого и высокого уровней. В основе всех процессов обработки языков лежит теория автоматов и формальных языков. Поскольку в этой книге обсуждаются в основном проблемы построения компиляторов, мы изложим те разделы теории, которые имеют к этому наиболее прямое отношение, и опишем практические методы, посредством которых можно применять эти математические построения. Хотя теория излагается здесь в контексте компиляторов, ее можно применять при построении любого языкового процессора. 1.2. Упрощенная модель компилятора Работа компилятора состоит в том, чтобы перевести наборы битов, представляющие программу, написанную на некотором исходном языке программирования, в последовательность машинных команд, которые выполняют то, что задумал программист. Это настолько сложная задача, что понимание или построение компилятора как единого целого является нелегким и трудоемким занятием. Поэтому лучше рассматривать процесс компиляции как взаимодействие небольших процессов, задачи которых описать гораздо легче. Выбор таких подпроцессов для каждого конкретного компилятора может зависеть от особенностей обрабатываемого языка, но в любом случае его лучше сделать с учетом соответствующей теории построения компиляторов. Поэтому мы не предлагаем никакого конкретного множества подпроцессов. С другой стороны, нельзя говорить о теории построения компиляторов, не имея некоторого представления о возможной внутренней организации компилятора. Поэтому в качестве основы дальнейшего изложения мы введем очень упрощенную, но характерную модель компилятора. Согласно этой модели, компиляция осуществляется тремя последовательно соединенными блоками, которые мы будем назы-
1.2. Упрощенная модель компилятора 13 вать лексическим блоком, синтаксическим блоком и генератором кода. Эти три блока имеют доступ к общему набору таблиц, куда можно помещать долговременную или глобальную информацию о программе. Одна из них, например,— это таблица имен (называемая также таблицей идентификаторов или таблицей символов), в ко- Пексический блок i i - Синтаксичес кий блок 11 ,, ■ i ■ ' Таблицы Рис. 1 1. н Генератор кода и торой накапливается информация о каждой переменной или. идентификаторе. Связи между этими блоками и таблицами показаны на рис. 1.1. Теперь опишем блоки более детально. Лексический блок. Входом компилятора служит набор битов, представляющий цепочку символов (литер). Лексический блок предназначен для того, чтобы разбивать цепочку символов на слова, ..из которых она состоит. Например, цепочка символов может быть такой: IFB1 = 13G0T04 Лексический блок устанавливает, что цепочка символов представляет слово IF, за которым следуют переменная В1, знак равенства, число 13, слово GOTO и метка 4. Таким образом, двенадцать входных символов преобразуются в шесть новых единиц. Эти единицы часто называют лексемами, и мы тоже будем пользоваться этим термином. Каждая лексема состоит из двух частей: класса и значения. Первая часть означает, что лексема принадлежит одному из конечного множества классов, и указывает характер информации, включенной в значение лексемы. Возвращаясь к нашему примеру, заметим, чго переменная В1 может принадлежать классу «переменная» и иметь значение, которое служит указателем на элемент таблицы имен для В1. Этот указатель на таблицу имен фактически является внутренним именем переменной В1. Лексема 13 может принадлежать классу «константа» и иметь в качестве значения набор битов, изображающий число 13. Знак равенства может относиться к классу «знак отношения», а его значение может указывать на то, какого именно. Лексема IF может принадлежать классу «IF», и информация о ее значении не требуется.
14 Гл. 1. Введение Если рассматривать таблицу имен как словарь, то лексическая обработка в какой-то степени аналогична группировке букв в слова и нахождению этих слов в словаре. Таким образом, слово «лексический» в названии этого блока вполне оправдано. Синтаксический блок. Этот блок переводит последовательность лексем, построенную лексическим блоком, в другую последовательность, которая более непосредственно отражает порядок, в котором, по замыслу программиста, должны выполняться операции в программе. Например, если программирующий на Фортране пишет А+В*С он подразумевает, что числа, представленные идентификаторами В к С, будут перемножены, и к результату будет прибавлено число, представленное идентификатором А. Указанное выражение можно перевести так: УМНОЖ (S, С, R\) СЛОЖ(Л, Я1, /?2) где УМНОЖ (£, С, R\) интерпретируется как «умножить В на С и заслать результат в RI», а СЛОЖИ, RI, R2) интерпретируется как «сложить А и RI и заслать результат в R2». Таким образом, пять лексем, выданных лексическим блоком, преобразуются в две новые единицы, которые описывают то же действие. Эти новые единицы называются атомами и образуют выход синтаксического блока. Преимуществом здесь является то, что последовательность атомов отражает порядок, в котором должны выполняться действия. Программист поставил знак умножения после знака сложения, но синтаксический блок должен сначала поместить умножение. Предположим, как и раньше, что каждый атом состоит из класса и значения. Тогда атом УМНОЖ(В, С, RI) может принадлежать классу «УМНОЖ» и иметь значение, состоящее из трех указателей на элементы таблицы: для В, С и RI соответственно. Внутри компилятора атом будет представлен целым числом, обозначающим «УМНОЖ», и тремя указателями, обозначающими его значение. Выполняя необходимые преобразования, синтаксический блок должен учитывать структуру языка, так же, как при переводе с естественного языка учитываются его грамматические особенности. Трансляцию рассмотренного выше выражения можно сравнить с переводом с английского языка (где глаголы стоят обычно в середине предложения) на немецкий (где глаголы часто стоят в конце). Таким образом, название «синтаксический блок» является здесь вполне подходящим. Генератор кода. Этот блок «развертывает» атомы, построенные синтаксическим блоком, в последовательность команд вычислительной машины, которые выполняют соответствующие действия. Точный характер этого развертывания может зависеть от элементов таблицы, на которые ссылаются атомы, и от ожидаемого состоя-
1.2. Упрощенная модель компилятора 15 ния вычислительной машины в момент фактического выполнения команд. В случае таких атомов, как УМНОЖ(В, С, R\), развертка может зависеть от типа операндов В и С, места, где хранятся операнды, и содержимого регистров машины. Целочисленные операнды требуют умножения с фиксированной точкой, операнды с плавающей точкой — умножения с плавающей точкой, а смешанные операнды требуют дополнительных команд преобразования типа. В некоторых машинах для вычисления произведения должен использоваться регистр. Чтобы порождаемый код был эффективным, часто требуется, чтобы генератор кода основательно анализировал содержимое различных регистров машины в период выполнения программы,— это позволяет избежать повторной загрузки уже доступной информации и выбрать наиболее подходящие регистры для хранения переменных, промежуточных результатов и разнообразной изменяемой информации. Выбор конкретной схемы работы с регистрами в большей степени должен зависеть от машины, для которой порождается код, так как в разных машинах число регистров и возможности, предоставляемые ими, могут существенно различаться. Та часть работы компилятора, которая связана со смыслом лексем, иногда называется семантической обработкой. Семантика идентификатора, например, может включать его тип, а в случае, если это массив,— его размерность. Один из видов семантической обработки включает занесение в таблицу имен свойств отдельных идентификаторов по мере их выявления. Другой вид включает действия, зависящие от типа данных. Например, мы предполагали, что при развертывании атома УМНОЖ(В, С, R\) генератор кода порождает команды либо с фиксированной, либо с плавающей точкой. Поскольку выбор зависит от типа операндов, его можно назвать семантическим. В некоторых компиляторах определенные семантические действия выполняются отдельным семантическим блоком, который помещается между синтаксическим блоком и генератором кода. Та часть работы компилятора, которая, строго говоря, не является необходимой, но позволяет получать более эффективные объектные программы, часто называется оптимизацией. В некоторых компиляторах роль оптимизации так велика, что между синтаксическим (или семантическим, если он есть) блоком и генератором кода помещают специальный блок оптимизации. Например, присутствие такого блока может быть желательным, если нужно выделять расположенные внутри цикла вычисления, результаты которых в ходе выполнения цикла не меняются, и выполнять эти вычисления один раз до входа в цикл. Эффект блока оптимизации часто состоит в переупорядочении атомов. Всего мы обсудим пять видов действий, выполняемых компиля-
16 Гл. 1. Введение тором, а именно: лексическую обработку, синтаксическую обработку, семантическую обработку, оптимизацию и генерацию кода. Пять этих эвристических понятий полезны и, возможно, обязательны для понимания и организации построения компилятора. Тем не менее эту классификацию не надо воспринимать слишком серьезно, так как: 1) выделение некоторых этапов работы компилятора можно подвергнуть сомнению; 2) необходимые действия должны выполняться там, где это наиболее удобно, а не в «блоке», выделенном на основе топ или иной классификации; 3) соглашения относительно лексики и синтаксиса, принятые внутри компилятора, могут в какой-то степени отличаться от того, что написано в руководстве по языку для пользователя, хотя общий результат, разумеется, должен быть таким, какого ожидает программист. 1.3. Блоки и проходы компилятора Обсуждая упрощенную модель компилятора, изображенную на' рис. 1.1, мы не касались того, как управление передается от одного блока к другому. Рассмотрим, например, взаимодействие между лексическим и синтаксическим блоками Здесь возможен выбор по крайней мере из двух типов взаимодействия. Один тип предполагает, что каждый раз, когда лексический блок выдает лексему, управление передается синтаксическому блоку для обработки этой лексемы. Когда возникает необходимость е следующей лексеме, управление возвращается в лексический блок. При другом типе взаимодействия лексический блок выдает всю цепочку лексем до того, как управление передается синтаксическому блоку. В этом случае говорят, что работа лексического блока образует отдельный проход. В упрощенной модели компилятора проходы можно организовать четырьмя разными способами. Если модель организована как однопроходный компилятор, управление передается из блока в блок всякий раз, когда требуется или когда производится лексема или атом. Если она организована, как трехпроходный компилятор, то лексический блок подготавливает всю последовательность лексем, которая затем используется синтаксическим блоком при порождении всей последовательности атомов, которая в свою очередь используется генератором кода при порождении машинного кода, возможны также два типа двухпроходной организации. В одном случае лексический и синтаксический блоки работают одновременно в течение одного прохода, заготавливая полную последовательность
1.3. Блоки и проходы компилятора 17 атомов для генератора кода. В другом случае в один проход организуются синтаксический блок и генератор кода. Вообще чем больше блоков в компиляторе, тем больше существует возможностей для его многопроходной реализации. Разбиение на проходы может привести к дополнительным затратам памяти, так как каждое взаимодействие между проходами требует, чтобы сохранялась вся цепочка выходных символов. Однако разбиение на проходы часто мотивируется убедительными доводами. Среди них можно привести следующие: Логика языка Иногда сам исходный язык наводит на мысль о том, что компилятор должен иметь не менее двух проходов. Такая потребность возникает, если в какой-то момент компилятору нужна информация из еще не просмотренной части программы. Например, если описание идентификатора или переменной может появляться в тексте программы после их использования, то может случиться, что код нельзя выдать до тех пор, пока не будет частично обработана вся исходная программа. В этом случае для генерации кода требуется отдельный проход. Оптимизация кода Иногда объектный код получается более эффективным, если генератору кода доступна информация обо всей программе. Например, согласно некоторым методам оптимизации, нужно знать все те места программы, где используются переменные и где могут изменяться их значения. Поэтому, прежде чем начать оптимизацию, необходимо просмотреть всю программу до конца. Экономия памяти Обычно многопроходные компиляторы занимают в памяти меньше места, чем компиляторы с одним проходом, так как код каждого прохода может вновь использовать память, занимаемую кодом предыдущего прохода. Каждый проход компилятора можно организовать в виде одного блока или комбинации нескольких блоков. С точки зрения теории построения компиляторов блок — это просто часть компилятора, которая мыслится и строится как одно целое. Так как блоки играют основную роль при конструировании компиляторов, мы будем заниматься главным образом описанием и построением блоков. С точки зрения теории построения компиляторов не имеет значения, будет ли блок реализован как отдельный проход ил«-как частв'тгекото^ого прохода. Так, работу лексического
18 Гл. 1. Введение блока, состоящую в порождении лексем, можно обсуждать вне зависимости от того, помещаются ли эти лексемы в промежуточный файл или направляются сразу в синтаксический блок. 1.4. Организация рабочей программы Обычно задача построения компилятора ставится перед разработчиком не вполне определенно. Сам исходный язык часто задается весьма точно, но то, что компилятор должен выдавать в качестве выхода, зачастую не определяется вовсе. Разработчику известно лишь, что выход должен соответствовать «семантике» языка и удовлетворять определенным требованиям, касающимся, например, скорости выполнения или затрат памяти. Поэтому на первом этапе построения нужно решить, что должно быть на выходе компилятора. Сюда входит определение того, какие структуры данных и механизмы управления будут необходимы во время выполнения программы для реализации различных свойств языка; как, например, будут размещены в памяти массивы и организован доступ к ним, как будет реализован вызов процедур и как будет обрабатываться рекурсия, если в языке допустимы рекур-' сивные процедуры. Выбор этих структур данных и механизмов управления относится к организации рабочей программы при реализации языка. Решения по организации рабочей программы служат важной частью всей работы по построению компилятора. Однако они не являются предметом изучения в данной книге, поскольку здесь делается акцент на самой проблеме перевода. Мы предполагаем, что нам известны как вход, так и желаемый выход, и будем заниматься осуществлением самого перевода. 1.5. Математические модели перевода Теория построения компиляторов, излагаемая в этой книге, основана на математической теории переводов и трансляторов (устройств, выполняющих перевод). Сам компилятор, а также каждый из трех блоков упрощенной модели компилятора являются трансляторами. Однако рассматриваемые в теории трансляторы — это не обязательно блоки упрощенной модели, а скорее «машины» или «автоматы», которые выполняют основные действия, связанные с переводом, но достаточно просты, что позволяет весьма подробно изучить их теоретически. Эти автоматы служат «строительными блоками» нашей теории в том смысле, что мы хотим строить компиляторы в виде систем таких взаимосвязанных автоматов. Теория построения компиляторов состоит из двух частей:
1.6. Компилятор для языка MINI-BASIG 19 1. Математическое изучение этих автоматов, включая а) их возможности в качестве трансляторов языков, б) их синтез по заданному переводу, который они должны осуществлять. 2. Применение теории к построению компиляторов, включая: а) представление компилятора (или некоторого блока компилятора) в виде системы взаимосвязанных моделей автоматов, б) реализацию или моделирование этих автоматов в виде программ для вычислительной машины. Математическая дисциплина, имеющая дело с автоматными моделями такого типа, называется теорией автоматов. На самом деле существует целая иерархия моделей теории автоматов, применимых при построении компиляторов. Обычно оказывается, что с возрастанием мощности этих моделей, выступающих в роли трансляторов, возрастают затраты как времени, так и памяти при их реализации в виде программ для вычислительных машин. Таким образом, разработчику следует выбирать наиболее простой автомат, который может выполнить поставленную задачу. Материал в книге организован так, что сначала описываются более простые модели автоматов. Даже эти очень простые модели применимы на практике при решении многих проблем, связанных с обработкой языков. 1.6. Компилятор для языка MINI-BASIC В первую очередь в этой книге рассматриваются те аспекты теории автоматов, которые полезны при построении компиляторов. Чтобы продемонстрировать их полезность и показать, как теорию можно применять в практических ситуациях, мы будем по мере продвижения по книге строить некоторый компилятор. В качестве языка, для которого будет строиться компилятор, мы выбрали подмножество языка BASIC, названное MINI-BASIC (Manifestly-Imaginatively-Named-Illustrative-BASIC). Описание этого языка приведено в приложении А. Хотя язык, по-видимому, слишком прост, чтобы получить широкое распространение, построение компилятора для него иллюстрирует многие проблемы и решения, которые действительно возникают при построении компиляторов на практике. Компилятор для MINI-BASIC'a это — однопроходный компилятор, в основе которого лежит упрощенная модель из трех блоков, приведенная в разд. 1.2. Он состоит из лексического блока, синтаксического блока и генератора кода. Предполагается, что каждый блок — это независимая часть компилятора. Сначала лексический блок обрабатывает символ из входной цепочки. Когда это сделано,
20 Гл. 1. Введение он либо переходит к другому входному символу, либо выдает некоторые выходные символы, инициируя тем самым работу синтаксического блока. Аналогичным образом, когда синтаксический блок заканчивает обработку полученных символов, он может либо обратиться к' лексическому блоку за новым входным символом, либо выдать некоторые выходные символы для генератора кода. Генератор кода в свою очередь обрабатывает эти символы, образуя окончательный выход компилятора, а затем может обратиться к синтаксическому блоку за новыми входными символами. Построение лексического блока описано в гл. 4, а синтаксические блоки строятся в гл. 10 и 12. Построение генератора кода описано в гл. 14.
2 Конечные автоматы 2.1. Введение В основе излагаемой в этой книге теории построения компиляторов лежит теория автоматов. Поэтому мы начнем с конечного автомата, одного из основных ее понятий. Под автоматом мы, конечно, подразумеваем не реально существующее устройство, а некоторую математическую модель, свойства и поведение которой можно изучать и которую можно имитировать с помощью программы на реальной вычислительной машине. Конечный автомат является простейшей из моделей теории автоматов и служит управляющим устройством для всех остальных изучаемых в ней автоматов. Помимо того что они служат основой теории автоматов, конечные автоматы находят непосредственное применение в ряде ситуаций, возникающих при построении компиляторов, благодаря следующим их свойствам: 1. Конечный автомат может решать (по крайней мере в первом приближении) ряд легких задач компиляции. В частности, лексический блок почти всегда строится на основе конечного автомата. 2. Поскольку при моделировании конечного автомата на вычислительной машине обработка одного входного символа требует небольшого количества операции, программа работает быстро. 3. Моделирование конечного автомата требует фиксированного объема памяти, что упрощает проблемы, связанные с управлением памятью. 4. Существует ряд теорем и алгоритмов, позволяющих конструировать и упрощать конечные автоматы, предназначенные для тех или иных целен. Термин «конечный автомат» в действительности употребляется в разных смыслах — в зависимости от подразумеваемых приложений. В литературе по теории автоматов существует несколько различных формальных определений. Общим в этих определениях является то, что они моделируют вычислительные устройства с фиксированным и конечным объемом памяти, которые читают последовательности входных символов, принадлежащих некоторому конечному множеству. Принципиальные различия в определениях
22 Гл. 2. Конечные автоматы связаны с тем, что автоматы делают на выходе. Следующий раздел начинается с рассмотрения конечного автомата, единственным «выходом» которого является указание на то, «допустима» или нет данная входная цепочка (последовательность символов). «Допустимой» мы называем «правильно построенную» или «синтаксически правильную» цепочку; например, цепочка, которая должна изображать числовую константу, построена не правильно, если содержит две десятичные точки. 2.2. Конечные распознаватели Конечный распознаватель — это модель устройства с конечным числом состояний, которое отличает правильно образованные или «допустимые» цепочки от недопустимых. Хотя это понятие чисто математическое, определяемое в терминах множеств, последовательностей (цепочек) и функций, лучше представлять его себе в виде вычислительной машины. Примером задачи распознавания может служить проверка нечетности числа единиц в произвольной цепочке, состоящей из нулей и единиц. Соответствующий конечный автомат будет «допускать» все цепочки, содержащие нечетное число единиц, и «отвергать» цепочки с четным их числом. Назовем этот автомат «контролёром нечетности». Мы будем строить его по мере введения терминологии, связанной с конечными распознавателями. На вход конечного автомата подается цепочка символов из конечного множества, называемого входным алфавитом автомата и представляющего собой совокупность символов, для работы с которыми он предназначен. Как допускаемые, так и отвергаемые автоматом цепочки состоят только из символов входного алфавита. Символы, не принадлежащие входному алфавиту, нельзя подавать на вход автомата. Входной алфавит контролера нечетности состоит из двух символов: 0 и 1. Представим себе, что в каждый момент времени конечный автомат имеет дело лишь с одним входным символом, а информацию о предыдущих символах входной цепочки сохраняет с помощью конечного множества состояний. Согласно этому представлению, автомат помнит о прочитанных ранее символах только то, что при их обработке он перешел в некоторое состояние, которое и является памятью автомата о прошлом. Контролер нечетности будет построен так, чтобы он умел запоминать, четное или нечетное число единиц встретилось ему при чтении отрезка входной цепочки. Поэтому множество состояний нашего автомата содержит два состояния, которые мы будем называть ЧЕТ и НЕЧЕТ.
2.2. Конечные распознаватели 23 Одно из этих состояний должно быть выбрано в качестве начального. Предполагается, что автомат начинает работу в этом состоянии. Начальным состоянием контролера нечетности будет ЧЕТ, так как на первом шаге число прочитанных единиц равно нулю и нуль — четное число. При чтении очередного входного символа состояние автомата меняется, причем новое его состояние зависит только от входного символа и текущего состояния. Такое изменение состояния называется переходом. Может оказаться, что новое состояние совпадает со старым. Работу автомата можно описать математически с помощью функции б, называемой функцией переходов. По текущему состоянию sren и текущему входному символу х она дает новое состояние автомата sH0B. Символически эта зависимость описывается так: Учитывая, что состояния ЧЕТ и НЕЧЕТ означают соответственно четное и нечетное число прочитанных единиц, определим функцию переходов контролера нечетности следующим образом: б (ЧЕТ, 0)=ЧЕТ б(ЧЕТ, 1) = НЕЧЕТ S (НЕЧЕТ, 0)=НЕЧЕТ б(НЕЧЕТ, 1)=ЧЕТ Эта функция переходов отражает тот факт, что четность меняется тогда и только тогда, когда на входе читается единица. Некоторые состояния автомата выбираются в качестве допускающих или заключительных. Если автомат, начав работу в начальном состоянии, при прочтении всей цепочки переходит в одно из допускающих состояний, говорят, что эта входная цепочка допускается автоматом. Если последнее состояние автомата не является допускающим, говорят, что автомат отвергает цепочку. Контролер нечетности имеет единственное допускающее состояние — НЕЧЕТ. Суммируя все сказанное, можно дать следующее определение конечного расюзнавателя. Конечный автомат задается: 1) конечным множеством входных символов, 2) конечным множеством состояний, 3) функцией переходов 6\ которая каждой паре, состоящей из входного символа и текущего состояния, приписывает некоторое новое состояние, 4) состоянием, выделенным в качестве начального, и 5) подмножеством состояний, выделенных в качестве допускающих или заключительных.
24 Гл. 2. Конечные автоматы Переход автомата из состояния sieK в состояние sH0B при чтении входного символа х будем наглядно изображать так: х "тек лнов Для контролера нечетности, например, можно написать НЕЧЕТ Л ЧЕТ Аналогичным образом можно изобразить последовательность переходов. Например, запись ЧЕТ Л НЕЧЕТ Л ЧЕТ Л ЧЕТ Л НЕЧЕТ показывает, как автомат в состоянии ЧЕТ применяется к цепочке 1101. Первый символ 1 меняет состояние ЧЕТ на НЕЧЕТ, так как б (ЧЕТ, 1) = НЕЧЕТ. Следующая единица меняет НЕЧЕТ на ЧЕТ. Нуль оставляет автомат в состоянии ЧЕТ. Последняя единица изменяет состояние на НЕЧЕТ. Так как ЧЕТ — начальное, а НЕЧЕТ— допускающее состояние, цепочка 1101 допускается нашим автоматом. Входную цепочку 101 автомат отвергает, так как она переводит его из начального состояния в состояние, не являющееся допускающим. Символически это изображается так: ЧЕТ Л НЕЧЕТ Л НЕЧЕТ Л. ЧЕТ Иногда мы хотим говорить о множестве всех цепочек, распознаваемых некоторым конечным автоматом. Например, контролер нечетности распознает множество цепочек, состоящих из нулей и единиц и содержащих нечетное число единиц. Такие множества обычно называют «регулярными». Регулярным множеством называется множество цепочек, которое распознается некоторым конечным распознавателем. Таким образом, множество цепочек из нулей и единиц с нечетным числом единиц может служить примером регулярного множества. 2.3. Таблица переходов Один из удобных способов представления конечных автоматов — таблица переходов. Для контролера нечетности такая таблица изображена на рис. 2.1. Информация размещается в таблице переходов в соответствии со следующими соглашениями: 1. Столбцы помечены входными символами. 3. Строки помечены символами состояний.
2.3. Таблица переходов 25 3. Элементами таблицы являются символы новых состояний, соответствующих входным символам столбцов и состояниям строк. 4. Первая строка помечена символом начального состояния. 5. Строки, соответствующие допускающим (заключительным) состояниям, помечены справа единицами, а строки, соответствующие отвергающим состояниям, помечены справа нулями. О 1 I——■«--■ - ЧЕТ НЕЧЕТ 0 ' НЕЧЕТ ' ЧЕТ j Рис. 2.1. Таким образом, таблица переходов, изображенная на рис. 2.1, задает конечный автомат, у которого: входное множество={0, 1}, множество состояний = {ЧЕТ, НЕЧЕТ}, переходы S (ЧЕТ, 0)=ЧЕТ и т. д., начальное состояние=ЧЕТ, допускающие состояния = {НЕЧЕТ}. х у г 1 входное множество ={х, у,г] о множество состояний={1,2,3,4} переходы 8 (/, х) = 1,8(l,y)=3 и т.д. начальное состояние = / допускающие состояния =[1,3] Рис. 2.2. Еще один автомат изображен на рис. 2.2. Входная цепочка хугг допускается этим автоматом, так как 1Д|Л3^4^3 и 3 является допускающим состоянием, тогда как цепочка гух отвергается, потому что 1Д4ЛЗЛ2 ЧЕТ НЕЧЕТ 1 13 4 2 2 13 3 2 4 4 4 3 3 3 и 2 — отвергающее состояние.
26 Гл. 2. Конечные автоматы 2.4. Концевые маркеры и выходы из распознавания Конечный распознаватель лежит в основе процессов распознавания цепочек,в компиляторе. Один из способов использования такого распознавателя — поставить его под контроль некоторой управляющей программы, которая определяет момент, когда входная цепочка прочитана, и по состоянию распознавателя выясняет, допустима она или нет. Однако во многих приложениях, связанных с разработкой компиляторов, желательно, чтобы распознаватель играл более активную роль и в большей степени выполнял функции управляющей программы. Требуется, чтобы он сам узнавал момент, когда задание выполнено, и соответствующим образом выходил из процесса распознавания. Поэтому таблицы переходов в том виде, в каком они были рассмотрены выше, требуют небольших, но существенных изменений. Рассмотрим автомат, изображенный на рис. 2.3. Он допускает множество цепочек в алфавите {а, Ь}, таких, что символы Ь в них либо не встречаются, либо встречаются парами. Например, этот автомат допускает цепочки abb, abba, aaa, abbbbabb, bba, но отвергает baa, abbb и abbab. Состояние 1 «помнит», что обработанная часть цепочки допустима. Если после допустимой части цепочки следует символ Ь, автомат переходит в состояние 2. В состояние Е он переходит, когда входная цепочка окончательно испорчена вхождением символа а вслед за «неспаренным» Ь. Таким образом, состояние Е можно назвать «состоянием ошибки», которое запоминает, что обнаружена ошибка. Пусть теперь нам нужно построить программу или «компилятор», который, прочитав цепочку или «программу» в алфавите {а, Ь), вызывает процедуру «ДА», если цепочка принадлежит множеству, распознаваемому автоматом рис. 2.3, и процедуру «НЕТ» в случае, когда цепочка не принадлежит этому множеству. Хотелось бы, конечно, чтобы наш компилятор имитировал работу автомата простым и естественным способом, поскольку именно это мы имели в виду, когда утверждали, что конечный автомат лежит в основе теории построения компиляторов. Однако очевидно, что в модели автомата, изображенного на рис. 2.3, не отражена идея «окончания» входной цепочки. Например, должен ли компилятор после прочтения символов abb выйти из процесса распознавания и перейти на процедуру «ДА», поскольку обработанная часть цепочки принадлежит заданному множеству, или он должен ждать появления дальнейших символов? Если за abb следуют символы Ьа, то, обра- 1 2 В 1 Е Е 2 1 Е
2.4. Концевые маркеры и выходы из распознавания 27 ботав всю цепочку abbba, автомат должен выйти на процедуру «НЕТ». На практике компилятор справляется с этой проблемой, используя информацию о конце файла, предоставляемую вычислительной системой, на которой он реализован. Так, в пакетном режиме программа abb может быть отперфорирована на картах и подана на вход компилятора между управляющими картами, причем листинг выглядит так: $BEGIN а b b $END Если работа идет в режиме разделения времени, то маркер конца файла устанавливается операционной системой. Если весь вход задается на одной перфокарте, то конец файла можно обнаружить, используя тот факт, что число символов на карте не больше 80. а ь и а b -н 1 2 Е 1 Е Е ,2 1 Е ДА НЕТ НЕТ 1 2 ДА НЕТ 1 НЕТ Рис. 2.4. В описанных выше ситуациях будем считать, что цепочка, подаваемая на вход автомата, имеет концевой маркер. Пусть это будет символ —|. Тогда цепочка abb поступит на вход автомата в виде abb-\ Автомат, изображенный на рис. 2.3, нужно изменить так, чтобы он умел обрабатывать дополнительный символ —|. Преобразованный автомат изображен на рис. 2.4, а. Символ «ДА» — это сокращенное указание на то, что работа закончена и автомат должен выйти на процедуру «ДА». Новое состояние не наступает, так как на этом автомат свою работу заканчивает. С введением концевого маркера необходимо заметить, что следует различать алфавит обрабатываемого языка и входной алфавит автомата, осуществляющего обработку. В рассматриваемом примере
28 Гл. 2. Конечные автоматы алфавит языка по-прежнему {а, Ь) и концевой маркер в описании языка не участвует. Входным алфавитом автомата, распознающего этот язык (рис. 2.3), также остается {а, Ь), тогда как входным алфавитом автомата, обрабатывающего тот же язык (рис. 2.4, а), будет {а, Ь, н \ Метод, с помощью которого мы получили обрабатывающий автомат из распознающего, очень прост. Мы добавили столбец, помеченный концевым маркером, и поместили «ДА» в строки, соответствующие допускающим состояниям, и «НЕТ» — в строки, соответствующие отвергающим состояниям. Ясно, что этим же способом каждый распознаватель может быть преобразован в обрабатывающий автомат. Поэтому очевидно, что любая техника построения конечных распознавателей потенциально применима и при построении обрабатывающих автоматов (процессоров). До сих пор мы имели дело с автоматами, которые были обязаны просматривать всю входную цепочку до конца. На практике многие конечные процессоры, применяемые в компиляторах, выполняют свою работу раньше, чем прочитана вся цепочка, и прекращают свою деятельность, не доходя до концевого маркера. В программистских терминах это означает, что в компиляторе могут быть процедуры, которые заканчивают работу, не дочитав до конца своей входной цепочки. Допустим, нам хочется построить автомат, который при обнаружении первой же ошибки передает управление некоторой внешней процедуре, обрабатывающей ошибки. Эта процедура либо прерывает процесс обработки цепочки, либо восстанавливает нужное состояние и возобновляет обработку с целью обнаружения дальнейших ошибок. Возвращаясь к рис. 2.4, а, напомним, что если автомат попал в состояние Е, то входная цепочка будет отвергнута. Поэтому первый же переход в это состояние означает, что обнаружена ошибка. Если мы решили прерывать процесс, как только обнаружена ошибка, то все переходы в состояние Е в таблице переходов нужно заменить вызовами процедуры «НЕТ». Результат замены изображен на рис. 2.4, б. Состояние Е удалено из автомата, так как отсутствие переходов в него означает, что автомат никогда не достигнет этого состояния, исходя из начального. Первоначально состояние Е было введено, чтобы обеспечить дочитывание входной цепочки после того, как обнаружена ошибка. Теперь в нем нет надобности, так как мы решили, что при обнаружении ошибки автомат должен выходить из процесса распознавания. Мы допускаем теперь, что любой элемент таблицы переходов конечного процессора может быть не переходом, а выходом из распознавания. Этот аспект процесса обработки входных цепочек назовем обнаружением (или детекцией). Автомат обнаруживает некоторую ситуацию до того, как прочитана вся цепочка, и прекращает свою работу.
2.5. Пример построения автомата 29 Детекция может встречаться при обработке как допустимых, так и ошибочных цепочек. Рассмотрим, например, задачу распознавания цепочек из нулей и единиц, содержащих хотя бы одну пару стоящих рядом единиц. Соответствующий распознаватель показан на рис. 2.5, а. Он переходит в состояние С при обнаружении пары 11. 0 1 0 1 н А В С А В А С С С а 0 0 1 А В Рис. 2.5. А А В ДА 6 НЕТ НЕТ Если мы хотим получить распознаватель, который выходит на «ДА», как только обнаружена пара единиц, то переходы в состояние С нужно заменить на «ДА». Распознаватель рис. 2.5, а превратится, таким образом, в процессор, изображенный на рис. 2.5, б. В нем нет состояния С, так как переходы в него отсутствуют. 2.5. Пример построения автомата Чтобы продемонстрировать автоматную технику на конкретном примере, построим автомат для распознавания цепочек, которые могут следовать за словом INTEGER в операторах спецификации Фортрана. Примеры таких операторов: INTEGER A INTEGER X, 1(3) INTEGER С (3, J, 4), В Будем считать, что массивы могут иметь любую размерность, хотя в большинстве стандартов и компиляторов с Фортрана она ограничена. Входной алфавит будет состоять из пяти символов: v с, ( ) где v — лексема, означающая произвольную неременную, с — лексема, соответствующая целочисленной константе (см. разд. 1.2). Будем считать, что эти лексемы порождаются каким-то другим автоматом. Построение автомата будем осуществлять, используя эвристический прием, который мы назовем «разметкой символов». На первом шаге построения из определяемого множества надо выбрать
30 Гл. 2. Конечные автоматы одну или более типичных цепочек и пометить входящие в них символы. Если на интуитивном уровне ясно, что множество цепочек, которые могут следовать за некоторым символом, совпадает с множеством цепочек, следующих за другим символом, то этим символам приписываются одинаковые метки. Эти метки отражают одно эвристическое понятие, которое мы будем называть «ролью». Впоследствии «роли» станут состояниями нашего конечного распознавателя. В нашем примере на первом шаге мы выписали цепочку, изображенную на рис. 2.6, и пометили ее символы номерами от 2 до 8. Номер 1 зарезервирован для начального состояния автомата и помещен перед началом цепочки, чтобы мы нагляднее представляли переходы автомата при построении таблицы. INTEGER А ( 3 , J , 4 ) , В 12345654782 Рис. 2.6. Заметим, что две запятые помечены номером 5. Действительно, цепочка 4), В которая следует за второй запятой, могла встретиться и после первой запятой, образуя допустимую цепочку INTEGER A(3, 4), В И вообще после этих запятых могут следовать одни и те же цепочки, т. е. две запятые выполняют одну и ту же роль. С другой стороны, запятая, помеченная номером 8, играет другую роль. Так, если цепочку, идущую за этой запятой, поместить после запятой, играющей роль 5, то мы получим недопустимую цепочку INTEGER A(3, В Аналогичным образом оба целых числа помечены номером 4, поскольку они допускают одинаковые продолжения. Роли, встречающиеся в нашем примере, словами могут быть описаны так: 2 — имя переменной, описываемой как INTEGER; 3 — левая скобка; 4 — целое число, задающее размерность; 5 — запятая, разделяющая размерности; 6 — переменная, задающая переменную размерность; 7 — правая скобка; 8 — запятая, разделяющая объекты, описываемые как целые. Так как мы уверены, что каждому символу, встречающемуся в любой другой допустимой цепочке, подходит одна из указанных
2.5. Пример построения автомата 31 ролей, то мы не будем рассматривать другие цепочки в поисках новых ролей. После того как все роли опознаны, дальше автомат можно строить по следующему алгоритму: 1. Ввести начальное состояние и состояние ошибки 2. Ввести состояние для каждой роли. 3. Поместить в таблицу переходы из одного состояния в другое, если соответствующие роли могут следовать одна за другой. 4. Пополнить таблицу переходами в состояние ошибки. 5. Выбрать в качестве допускающих те состояния, роли которых появляются в конце допустимой цепочки. В нашем примере на первом шаге вводится начальное состояние 1 и состояние ошибки Е. На втором шаге мы вводим состояния 2—8, соответствующие ролям, выявленным при интуитивном анализе. Затем на шаге 3 получаем все нужные переходы. Заметим, что первым символом каждой допустимой цепочки должна быть переменная в роли 2, поэтому заносим в таблицу переход от начального состояния 1 в состояние 2 при чтении входного символа v. Этот элемент мы видим в таблице, изображенной на рис. 2.7, где показан конечный результат нашего построения. За вхождением символа с ролью 2 может следовать либо вхождение с ролью 3 (как в цепочке на рис. 2.6), либо с ролью 8 (как в цепочке INTEGER А, В). Поэтому для соответствующих входных символов помещаем в таблицу переходы из 2 в 3 и из 2 в 8. Завершая этот анализ, во все остальные ячейки таблицы заносим переходы в состояние Е. И наконец, выполняем шаг 5. Заметим, что символы, стоящие в конце допустимых цепочек, могут иметь роль 2 или 7, поэтому объявляем состояния 2 и 7 допускающими и на этом завершаем построение таблицы. Одно из возражений против предложенного метода состоит в том, что построенный автомат не обязательно будет наилучшим 2 Е 6 Е 6 Е Е 2 Е Е Е 4 Е 4 Е Е Е Е Е 8 Е 5 Е 5 8 Е Е Е <з"~ Е Е Е Е Е Е Е Е Е Е 7 Е 7 Е Е Е 0 1 0 0 0 0 1 0 0
32 Гл. 2. Конечные автоматы в том смысле, что может найтись другой автомат с меньшим числом состояний, который определяет то же множество цепочек. Позднее мы убедимся в том, что автомат, изображенный на рис. 2.7, действительно не является наилучшим. Это возражение будет устранено в разд. 2.11, где будет дан алгоритм приведения произвольного конечного автомата к оптимальному виду. Более серьезное возражение заключается в том, что описанный метод явно не применим в случаях, когда за символом в некоторой роли может следовать один и тот же символ в двух разных ролях. Это возражение будет снято в разд. 2.13, где мы покажем, что в одну ячейку таблицы можно помещать два или более переходов, а затем преобразовывать полученную таблицу в новую, каждый элемент которой содержит один переход. Таким образом, творческая часть построения автомата заключается в определении ролей и переходов. Затем с помощью указанного метода и описанных далее процедур можно автоматически перейти к наилучшему конечному распознавателю. В гл. 6 будет дано другое определение регулярного множества цепочек и показано, как с его помощью можно автоматически построить соответствующий конечный автомат, не прибегая к выявлению ролей. 2.6. Пустая цепочка До сих пор мы молчаливо предполагали, что читатель имеет интуитивное представление о том, что такое цепочка. В большинстве случаев это предположение оправдано, и дать простое объяснение этого понятия нелегко. Можно сказать, например, что цепочка 1) состоит из следующих друг за другом символов, 2) образуется сцеплением символов. Однако все это в сущности сводится к определению: «цепочка — это последовательность символов». Поэтому будем считать, что читатель не раз имел дело с цепочками и знает, что это такое. Возможное упущение в таком предположении заключается в том, что многие не осознают важности понятия пустой (или нулевой) цепочки. Цепочка нулевой длины, называемая пустой цепочкой, часто встречается в теоретико-автоматных рассуждениях и действительно имеет большое практическое значение. Чтобы лучше познакомиться с этим понятием, установим его связь с введенными ранее понятиями. Во-первых, рассмотрим пустую цепочку как программу вычислительной машины. Если заключить ее в управляющие карты и ввести в вычислительную машину для компиляции, то листинг будет выглядеть так: $BEGIN $END
2.6. Пустая цепочка 33 Если мы хотим, чтобы пустую цепочку обработал один из автоматов разд. 2.4, необходимо добавить к ней справа концевой маркер, т. е. на вход автомата поступит цепочка н Автомат допустит эту цепочку, если элемент таблицы переходов, соответствующий начальному состоянию и концевому маркеру, содержит «ДА», и отвергнет ее, если этот элемент содержит «НЕТ». Если рассмотреть конечные распознаватели разд. 2.2, то станет ясно, что Конечный распознаватель допускает пустую цепочку тогда и только тогда, когда его начальное состояние является допускающим. В терминах переходов это означает, что пустая цепочка, примененная к начальному (или к любому другому состоянию), не вызывает никаких переходов, т. е. оставляет состояние неизменным. Таким образом, под действием пустой цепочки, примененной к начальному состоянию, распознаватель заканчивает работу в начальном состоянии, которое и определяет допустимость цепочки. Последовательность переходов для пустой цепочки, примененной к произвольному состоянию s, выглядит так: s Следовательно, если первое состояние в этой последовательности (s) является начальным, а последнее (тоже s) — допускающим, то пустая цепочка допускается. Пустая цепочка часто встречается в описаниях языков программирования. Так, в Алголе 60 допустим пустой оператор, т. е. пустая цепочка. Одно из неудобств, связанных с пустой цепочкой, заключается в том, что ее очень трудно изобразить графически в печатном тексте. Мы изображали пустую цепочку, помещая ее между управляющими картами или снабжая концевым маркером, но изобразить ее непосредственно невозможно, так как она присутствует в предложении незримо. Решим эту проблему, обозначая пустую цепочку через е. Символически е определяется следующим равенством: е= Это обозначение настолько полезно, что мы сохраним е в этой роли до конца книги. Оно широко используется в литературе по теории автоматов, хотя иногда пустую цепочку обозначают еще символами Я, или е. Пустую цепочку путают иногда с пустым множеством, хотя цепочка и множество совершенно разные понятия. Пустым (или нулевым) называют множество, не содержащее ни одного элемента; Ф. Льюис и др.
34 Гл. 2, Конечные автоматы его часто обозначают через <р или Ф. Чтобы пояснить это различие, сравним пустое множество { } и множество {е}. Пустое множество, понятно, не содержит элементов, тогда как множество {е} содержит один элемент, а именно пустую цепочку е. На рис. 2.8, а изображен автомат с алфавитом {а, Ь), распознающий множество { }. Рис. 2.8, б изображает автомат с тем же входным алфавитом, допускающий множество {е}. Ясно, что эти автоматы различны. Заметим, что е не является входом автомата, распознающего {е}. а S ь S 0 S Т т т т т 1 0 а б Рис. 2.8. (а) Распознаватель множества { }; (б) Распознаватель множества {е}. Символ е используется в описаниях и сам по себе не является входным символом автомата. Если мы говорим «пусть у—abba», то это не означает, что мы добавим у к входному алфавиту автомата. Аналогично не надо думать, что е принадлежит входному множеству только потому, что мы сказали «пусть е=». Другое заблуждение связано с отождествлением пустой цепочки и знака пробела. Однако выражение 1е0 обозначает цепочку 10, а не 1 0. Поскольку пустая цепочка не содержит ни одного символа, ее длина равна 0. Длина же цепочки, состоящей из одного символа пробела, равна 1. 2.7. Эквивалентность состояний Для каждого конечного автомата существует бесконечное число других конечных автоматов, которые распознают то же множество цепочек. При конструировании распознавателя для данной проблемы естественно учитывать возможность существования какого- то другого распознавателя для того же множества, который определяется проще, и при реализации в качестве программы для вычислительной машины требует меньших затрат памяти. В этом и в последующих четырех разделах мы развиваем некоторую теорию, касающуюся этого вопроса. Один из ее результатов заключается в том, что для каждой задачи распознавания существует единственный автомат, свойства которого полностью соответствуют нашим представлениям об автомате, «имеющем простейшее определение» и «требующем минимальных затрат памяти при реализации». Кроме
2.7. Эквивалентность состояний 35 того, мы покажем, как можно получить такой автомат по произвольному исходному автомату. В частности, в разд. 2.7—2.11 устанавливается следующий факт: Для каждого конечного распознавателя существует единственный конечный автомат, распознающий то же самое множество цепочек, при этом число его состояний не больше числа состояний любого другого конечного распознавателя для этого множества. Употребление в предыдущем утверждении слова «единственный» требует некоторого пояснения. Для любого автомата можно получить новый автомат с таким же числом состояний, просто переименовав его состояния. Однако имена состояний не имеют никакого значения для распознавания цепочек или для реализации автомата как программы вычислительной машины. Поэтому на практике автоматы, которые различаются лишь именами состояний, можно считать «одинаковыми». В данном контексте слово «единственный» следует понимать как «единственный с точностью до имен состояний». Этот единственный автомат будем называть минимальным автоматом. При изложении теории станет очевидным, что минимальный автомат для заданной проблемы распознавания является на самом деле результатом приведения (или редукции) более громоздких автоматов, решающих ту же задачу. Точное описание характера приведения будет дано ниже. Суть в том, что минимальный автомат — это компактный вариант автоматов большего объема, а не просто еще один автомат, у которого случайно оказалось меньше состояний. Этот факт усиливает довод в пользу выбора минимального автомата в качестве главного кандидата на реализацию. В гл. 3, где детально рассматриваются несколько методов реализации конечных автоматов как программ для вычислительных машин, значение минимального автомата станет еще более очевидным. Первым шагом в нашем изложении теории минимизации и приведения автоматов будет введение понятия эквивалентности состояний. Неформально два состояния эквивалентны, если они одинаково реагируют на все возможные продолжения входной цепочки. Это понятие применимо и к состояниям одного и того же автомата, и к состояниям разных автоматов. Применительно к конечным распознавателям, назначение которых — допускать цепочки, эквивалентность состояний можно определить так: Состояние s конечного распознавателя М эквиваленте состоянию t конечного распознавателя N тогда и только тогда, когда автомат М, начав работу в состоянии s, будет допу-
36 Гл. 2. Конечные автоматы екать в точности те же цепочки, что и автомат N, начавший работу в состоянии t. Если два состояния s и t одного автомата эквивалентны, то автомат можно упростить, заменив в таблице переходов все вхождения имен этих состояний каким-нибудь новым именем, а затем удалив одну из двух строк, соответствующих $и/. Например, состояния 4 и 5 автомата, изображенного на рис. 2.9, а, явно имеют одинаковые функции, так как оба они являются допускающими, 1 2 3 4 5 1 4 3 5 5 1 2 3 2 3 а 0 1 0 1 1 1 2 3 X X , X 3 X X 1 2 3 2 3 .6 Рис. 2.9. 0 1 0 1 1 1 2 3 X \ 1 X I 3 X \ X 1 2 3 I в 0 1. 0 1 оба переходят в состояние 2 при чтении входного символа а и оба переходят в состояние 3 при чтении Ь. Поэтому мы объединяем со стояния 4 и 5 в одно состояние, для которого выбираем имя X Заменяя в таблице состояний каждое вхождение имен 4 и 5 именем X, мы получаем тем самым таблицу, изображенную на рис. 2.9, б Две ее строки помечены X; удалив одну из них, получаем упрощен ную таблицу состояний на рис. 2.9, в. Обычно эквивалентность менее очевидна, чем в данном примере поэтому нам придется обращаться к тесту на эквивалентность излагаемому в следующем разделе. Вторая цель проверки состояний на эквивалентность состоит в выяснении того, делают ли два автомата одно и то же, т. е. совпадают ли множества допускаемых ими цепочек. Это достигается просто проверкой эквивалентности начальных состояний автоматов. Если они эквивалентны, то по определению эквивалентности состояний оба автомата допускают и отвергают одни и те же цепочки. Таким образом, понятие эквивалентности состояний приводит нас к понятию эквивалентности автоматов, а именно: Автоматы М и N эквивалентны тогда и только тогда, когда эквивалентны их начальные состояния.
2.8. Проверка эквивалентности двух состояний 37 Если два состояния не эквивалентны, то любая цепочка, под действием которой одно из них переходит в допускающее состояние, а другое — в отвергающее состояние, называется цепочкой, различающей эти два состояния. Состояния а и х на рис. 2.10 не эквивалентны, так как их различает цепочка 101. Символически 1 0 1 а—*с—*о—*с 1 0 1 х—уу—<• г—>г причем состояние с — допускающее, а состояние z — нет. Поскольку а и х — начальные состояния соответствующих автоматов, 0 1 0 1 0 0 1 Рис. X У Z 2.10. мы показали также, что автоматы, изображенные на рис. 2.10, не эквивалентны. Мы видим, что Два состояния эквивалентны тогда и только тогда, когда не существует различающей их цепочки. Заметим, что понятие эквивалентности состояний является отношением эквивалентности в математическом смысле; это отношение рефлексивно (каждое состояние эквивалентно себе), симметрично (из того, что s эквивалентно /, следует, что / эквивалентно s) и транзитивно (если s эквивалентно /, а / эквивалентно и, то s эквивалентно и). 2.8. Проверка эквивалентности двух состояний Договоримся, что в этом разделе будут рассматриваться автоматы с одним и тем же входным множеством. Построим метод проверки эквивалентности состояний, основанный на следующем факте: Состояния s и / эквивалентны тогда и только тогда, когда выполняются следующие два условия: 1) условие подобия — состояния s и t должны быть либо оба допускающими, либо оба отвергающими, 2) условие преемственности — для всех входных символов состояния s и t должны переходить в эквивалентные состояния, т. е. их преемники эквивалентны.
38 Гл. 2. Конечные автоматы Теперь покажем, что эти два условия выполняются тогда и только тогда, когда s и / не имеют различающей цепочки. Сначала заметим, что если нарушено хотя ,бы одно из них, то существует цепочка, различающая эти два состояния. Если не выполняется условие подобия, то различающей цепочкой является пустая цепочка. Если нарушено условие преемственности, то некоторый входной символ х переводит состояния s и t в неэквивалентные состояния. Поэтому х с приписанной к нему цепочкой, различающей эти новые состояния, образует цепочку, различающую sat. Теперь убедимся, что если состояния s и t различаются некоторой цепочкой, то хотя бы одно из этих условий должно быть нарушено. Если их различает пустая цепочка (нулевой длины), то не выполняется условие подобия. Если длина различающей цепочки больше нуля, то ее первый символ переводит s и / в пару состояний, которые не эквивалентны, так как различаются оставшейся частью цепочки, различающей s и /. Таким образом, мы видим, что оба условия выполняются, если два состояния эквивалентны, и что хотя бы одно из них нарушается в случае неэквивалентности состояний. Условия 1 и 2 можно использовать в общем методе проверки на эквивалентность произвольной пары состояний. Этот метод, вероятно, лучше понимать как проверку на неэквивалентность и рассматривать его как метод поиска различающей цепочки. Проиллюстрируем его на примере автомата, изображенного на рис. 2.11, прежде чем формулировать правила проверки в общем виде. Для записи необходимых данных мы будем строить таблицы нового типа, которые назовем таблицами эквивалентности состояний. Сначала мы проверяем на эквивалентность состояния 0 и 7 рис. 2.11. Таблица эквивалентности состояний для этой проверки содержит по одному столбцу для каждого входного символа, а именно столбец для у и столбец для г. Строки будут добавляться в ходе проверки. Первоначально имеется одна строка, которая помечена парой состояний, подвергаемых проверке, а именно парой 0,7. Результат изображен на рис. 2.12, а. Сначала мы надеемся продемонстрировать неэквивалентность состояний 0 и 7, показав, что нарушается условие подобия. К сожалению, это условие выполняется, так как оба состояния являются отвергающими. О 1 2 3 4 5 6 7 0 2 2 6 1 6 6 6 3 5 7 7 6 5 3 3
2.8. Проверка эквивалентности двух состояний 39 Теперь нам остается надеяться на то, что будет нарушено условие преемственности. Чтобы исследовать эту возможность, рассмотрим, как действует на данную пару состояний каждый входной символ, и запишем результат в соответствующую ячейку таблицы. Так как состояния 0 и 7 под действием входного символа у перехо- б Рис. 2.12. дят в состояния 0 и 6 соответственно, мы записываем 0,6 в столбец таблицы, соответствующий символу у. Так как оба состояния 0 и 7 переводятся символом г в состояние 3, запишем 3 в столбец для г. Теперь мы получили рис. 2.12, б. Чтобы нарушалось условие преемственности, должны быть неэквивалентными либо состояния О и 6, либо состояния 3 и 3. Так как каждое состояние эквивалентно самому себе, состояния 3 и 3 автоматически эквивалентны. Чтобы исследовать на неэквивалентность состояния 0 и 6, добавляем к таблице эквивалентности состояний новую строку и помечаем ее этой парой. Результат показан на рис. 2.12, в. Процесс теперь повторяется с этой новой строкой. Сначала мы проверяем условие подобия для состояний 0 и 6. Обнаруживаем, что 0 и 6 неэквивалентны, так как 6 — допускающее, а 0 — отвергающее состояние. Проверка закончена, и мы убедились, что исходные состояния 0 и 7 неэквивалентны. Таблицу эквивалентности состояний можно использовать для построения различающей цепочки. Строка 0,6 появилась как результат применения входного символа у к паре 0,7, поэтому у является различающей цепочкой. Теперь проверим, эквивалентны ли состояния 0 и 1. Начнем построение таблицы эквивалентности состояний с пары 0,1. Эти состояния подобны, поэтому мы вычисляем результат применения к ним каждого входного символа и помещаем полученные состояния в таблицу. Получаем рис. 2.13, а. Наши надежды на неэквивалентность состояний 0 и 1 оправдаются, если будет установлена неэквивалентность состояний 0,2 или 3,5. Поэтому мы добавляем в таблицу строку для каждой из этих пар. Результат изображен на рис. 2.13, б. Обратившись к строке 0,2, мы замечаем, что эти состояния подобны, и поэтому надо вычислить следующие пары состояний, чтобы проверить условие преемственности. Результат показан на
40 Гл. 2. Конечные автоматы рис. 2.13, в. Из двух новых элементов таблицы лишь один дает новую строку, а именно пара 3,7. Другой элемент, пара 0,2, уже имеется в таблице, и нет надобности его повторять. Тот факт, что пара 0,2 порождается алгоритмом дважды, означает, что имеются две вход- 0,1 0,2 3,5 0,1 0,2 3,5 0,2 3,5 _^ 0,1 0,2 3,5 0,2 0,2 3,5 3,7 0,1 0,2 3,5 3.7 5,7 0,2 0,2 6 3,5 3,7 5,7 0,1 0,2 3,5 3,7 5,7 0,2 0,2 6 6 6 3.5 3,7 5,7 3,7 3,5 Рис. 2.13. ные цепочки, которые ведут из исходной пары 0,1 в пару состояний 0,2. Любая из этих цепочек с приписанной к ней цепочкой, различающей состояния 0 и 2, образует различающую цепочку для состояний 0 и 1. Однако, поскольку для доказательства неэквивалентности состояний 0 и 1 достаточно одной различающей их цепочки, достаточно одного вхождения в таблицу пары 0,2. Следовательно, единственной новой строкой в таблице будет строка 3,7. Идя вниз по списку строк, рассмотрим теперь строку 3,5. Устанавливаем, что состояния 3 и 5 подобны. Вычисляем следующие пары, а именно 6,6 и 5,7. Так как состояние 6 эквивалентно самому себе, единственной новой строкой будет 5,7. В этот момент наша таблица выглядит так, как изображено на рис. 2.13, г. Продолжая процедуру, мы не обнаруживаем ни одной пары неподобных состояний и ни одной новой пары, которую надо проверять на подобие.
2.8. Проверка эквивалентности двух состояний 41 Таблица заполнена, как показано на рис. 2.13, д, и поиск различающей цепочки окончился неудачей. Поэтому состояния 0 и 1 должны быть эквивалентными. Общую процедуру можно описать следующим образом: 1. Начать построение таблицы эквивалентности состояний с отведения столбца для каждого входного символа. Пометить первую строку парой состояний, подвергаемых проверке. 2. Выбрать в таблице эквивалентности состояний строку, ячейки которой еще не заполнены, и проверить, подобны ли состояния, которыми она помечена. Если они не подобны, то два исходных состояния неэквивалентны, и процедура оканчивается. Если они подобны, вычислить результат применения каждого входного символа к этой паре состояний и записать полученные пары состояний в соответствующие ячейки рассматриваемой строки. 3. Для каждого элемента таблицы, полученного на шаге 2, существует три возможности. Если элементом таблицы является пара одинаковых состояний, то для этой пары не требуется никаких действий. Если элементом таблицы является пара состояний, которые уже использовались как метки строк, то для нее также не требуется никаких действий. Если элемент таблицы — это пара разных состояний, которая еще не использовалась как метка, то для этой пары состояний нужно добавить новую строку. В данном случае порядок состояний в паре не важен, и пары s, t и t, s считаются одинаковыми. После того как произведены необходимые действия для каждой пары состояний в данной строке, перейти к шагу 4. 4. Если все строки таблицы эквивалентности состояний заполнены, исходная пара состояний и все пары состояний, порожденные в ходе проверки, эквивалентны, и проверка закончена. Если таблица не заполнена, нужно обработать еще по крайней мере одну ее строку, и применяется шаг 2. Так как каждая пара, появившаяся в заполненной таблице эквивалентности состояний, содержит эквивалентные состояния, эт-iT метод проверки часто дает больше информации, чем предполагалось вначале. Возвращаясь к нашему последнему примеру, на рис. 2.13, д мы видим, что кроме эквивалентности пары (0, 1), которая подвергалась проверке, мы попутно доказали эквивалентность пар (0,2), (3,5), (3,7) и (5,7). По свойству транзитивности из эквивалентности пар состояний 0,2 и 0,1 следует эквивалентность А 6 А 6 В В 6 В
42 Гл. 2. Конечные автоматы пары 1, 2. Таким образом, состояния 0, 1 и 2 эквивалентны друг другу. Аналогично эквивалентны друг другу состояния 3, 5 и 7. Информацию об эквивалентности состояний можно использовать для упрощения автомата. Мы объединяем состояния 0, 1 и 2 в одно Состояние, которое называем А, а состояния 3, 5 и 7 — в состояние, которое называем В. Подставляя эти новые имена в рис. 2.11 и удаляя лишние строки, мы получаем более простой эквивалентный автомат, изображенный на рис. 2.14. 2.9. Недостижимые состояния Среди состояний автомата могут быть такие, которые не достижимы из начального состояния ни для какой входной цепочки. На рис. 2.15, а таким состоянием является s4, так как в таблице нет переходов в s4. Такие состояния, как s4, называются недостижимыми. Строки, соответствующие этим состояниям, можно удалить из таблицы *0 *1 *2 h и h *б h s8 *i Н *z % *5 *3 s8 *0 *3 а % *7 *S h s6 «1 s0 s1 0 1 1 0 0 0 1 1 0 so *i s2 h H J6 «7 *8 *1 % h h h h S5 S-, h *\ ss *o s0 «1 h *e 6 Рис. 2.15. 0 1 1 0 0 1 1 0 so *i h h % J7 *t h s2 S5 S3 s0 в h sl S5 h *, *1 0 1 1 0 0 1 переходов, получив тем самым таблицу переходов автомата, который эквивалентен исходному, но имеет меньшее число состояний. Это сделано на рис. 2.15, б. Для любого заданного автомата довольно просто составить список достижимых состояний. 1. Начать список начальным состоянием. 2. Для каждого состояния, уже внесенного в список, добавить
2.10. Приведенные автоматы 43 все еще не занесенные в него состояния, которые могут быть достигнуты из этого нового состояния под действием одного входного символа. Если эта процедура перестает давать новые состояния, то все достижимые состояния получены, а все остальные состояния можно удалить из автомата. Так как на каждом шаге процедуры к списку достижимых состояний добавляется хотя бы одно новое состояние, число шагов процедуры ограничено числом состояний данного автомата. В качестве примера рассмотрим автомат на рис. 2.15, а. Начав с состояния So, мы видим, что состояния Si и s6 наступают под действием одного входного символа. Из состояния st есть переход в Sj и s7; из состояния s5 — в ss и st. Таким образом, нам известно, что s0, Si, s5, s2, s, и ss достижимы, и нужно посмотреть, есть ли переходы в какие-нибудь новые состояния из s2, s, и s3. Проверка этих состояний показывает, что никакие новые состояния не достигаются, и, следовательно, оставшиеся состояния s4, se и s8 недостижимы. Таким образом, эти три состояния можно удалить, получив тем самым эквивалентный автомат, изображенный на рис. 2.15, в. 2.10. Приведенные автоматы Мы говорим, что автомат приведенный, если он не содержит недостижимых состояний и никакие два его состояния не эквивалентны друг другу. Если автомат не приведенный, то можно получить эквивалентный ему автомат с меньшим числом состояний, либо путем выбрасывания недостижимых состояний, либо путем объединения двух эквивалентных состояний в одно, как было показано в двух предыдущих разделах. Процесс приведения можно повторять до тех пор, пока не получится приведенный автомат. Таким образом, для каждого конечного автомата существует эквивалентный ему приведенный автомат. Приведенный автомат, полученный таким способом, имеет меньшее число состояний, чем исходный (если исходный не был уже приведенным), и может быть более компактно реализован на вычислительной машине. Осуществляя приведение различными способами или начиная с разных эквивалентных автоматов, можно предположить, что полученные приведенные автоматы окажутся разными. Однако эти автоматы будут фактически одинаковыми во всех отношениях, за исключением имен, которыми названы их состояния. Чтобы проиллюстрировать это на примере, мы изобразили на рис. 2.16, а и 2.16, б два приведенных автомата. Применяя тест из разд. 2.8 к паре начальных состояний (Л, 1), строим таблицу
44 Гл. 2. Конечные автоматы эквивалентности состояний на рис. 2.17 и устанавливаем, что следующие пары эквивалентны: (А, 1), (В, 3), (С, 4), (D, 2). Это значит, что автоматы эквивалентны, и по парам можно установить, какие имена обозначают эквивалентные состояния. Подстав- 0 1 о о А k С D В С 0 А В В С 0 а 1 0 1 0 1 2 3 4 3 4 4 2 2 1 3 3 6 Рис. 2.1 1 0 0 1 6. (А) (В) (С) (D) 1 3 4 2 3 2 3 4 Я 4 1 3 2 1 0 1 0 АЛ 6,3 С.4 0,2 О ляя в рис. 2.16, а новые имена, мы получаем рис. 2.16, в, который совпадает с рис. 2.16, б во всем, кроме порядка строк. Таким образом, эти два автомата идентичны с точностью до имен состояний. Чтобы убедиться, что это справедливо для любой пары эквивалентных приведенных автоматов М и N, посмотрим, что происходит при применении к М и /V метода проверки эквивалентности из разд. 2.8. Сначала в таблицу эквивалентности состояний включается пара, состоящая из начального состояния автомата М и начального состояния автомата N. Последующие пары, порождаемые при проверке, будут обязательно содержать одно состояние из М и одно состояние из N. Каждое состояние автомата М может входить в пару не более чем с одним состоянием автомата N, и, наоборот, так как если бы состояниет автоматам было эквивалентно двум состояниям tii и п2 автомата N, то состояния пх и п2 были бы эквивалентны друг другу, что противоречит нашему предположению о том, что N — приведенный автомат. Кроме того, каждое состояние т автомата М образует пару хотя бы с одним состоянием автомата N, и, наоборот, так как входная цепочка, 6,3 0,2 6,3 С, 4 С А А,1 6,3 0,2 Рис. 2.17.
2.11. Получение минимального автомата 45 которая переводит автомат М в состояние т, переведет также пару начальных состояний из таблицы эквивалентности в пару, содержащую состояние т. Отсюда мы заключаем, что каждое состояние автомата М эквивалентно в точности одному состоянию /V, и наоборот. Это означает, что автоматы М и N одинаковы go всем, кроме имен состояний. Мы показали, что если не придавать значения именам состояний, то для каждой проблемы распознавания можно найти только один приведенный автомат. Это означает, что, какой бы распознаватель мы ни выбрали для некоторой проблемы распознавания и каким бы образом ни происходило приведение его состояний, мы можем построить только один приведенный автомат. Этот автомат является минимальным автоматом, о существовании которого говорилось в начале разд. 2.7. 2.11. Получение минимального автомата Произвольный конечный автомат можно превратить в эквивалентный ему минимальный, выбрасывая недостижимые состояния и объединяя эквивалентные состояния. В разд. 2.9 дан эффективный метод нахождения недостижимых состояний. Однако метод проверки эквивалентности состояний, описанный в разд. 2.8, использовать для приведения автоматов неудобно, так как он позволяет обрабатывать одновременно только два состояния. Здесь мы приводим более эффективный метод нахождения и объединения эквивалентных состояний. Он имеет большое практическое значение, так как минимальные конечные автоматы используются в большинстве приложений с целью минимизации затрат памяти. Этот новый способ будем называть «методсм разбиения», так как он заключается в разбиении множества состояний на непересекающиеся подмножества или блоки, такие, что неэквивалентные состояния попадают в разные блоки. Применение этого способа продемонстрируем на автомате рис. 2.18, а. Сначала состояния разбиваются на два блока; один содержит допускающие состояния, а другой — отвергающие состояния. В нашем примере это начальное разбиение Р0 выглядит следующим образом: Я.= ({1, 2, 3, 4}, {5, 6, 7}), так как 1, 2, 3 и 4 — отвергающие состояния, а 5, 6 и 7 — допускающие состояния. Ни одно из состояний первого блока не эквивалентно ни одному состоянию второго блока, поскольку такие пары состояний нарушают условие подобия (см. разд. 2.8). Теперь посмотрим, что происходит с состояниями блока {1,2, 3, 4} под действием входного символа а. Состояния 3 и 4 переходят
46 Гл. 2. Конечные автоматы в состояния, принадлежащие первому блоку (а именно в состояния 1 и 4 соответственно), тогда как состояния 1 и 2 переходят в состояния, принадлежащие второму блоку (а именно в состояния 6 и 7 соответственно). Это означает, что для любого состояния из множества {1„2} и любого состояния из {3, 4} соответствующие состоя- a b a b 1 2 3 4 5 6 7 & 3 7 3 t 5 4 6 7 3 4 1 4 2 a 0 0 0 0 1 1 1 (1,2) {3} (4) {5} {6,7} . Рис. 2.18. {6,7} {1.2} {4} {6,7} {4} 6 {3} {5} {6,7} {3} {1,2} 0 0 0 1 1 ния-преемники по входу а будут неэквивалентны. Это нарушает условие преемственности, и потому мы можем заключить, что ни одно состояние из множества {1, 2} не эквивалентно ни одному состоянию из множества {3, 4}. Это позволяет произвести новое разбиение: Л=({1, 2}, {3,4}, {5,6,7}), причем состояния из разных блоков всегда не эквивалентны. Мы говорим, что получили Рх из Ре, разбивая блок {1, 2, 3, 4} относительно входа а. Теперь попытаемся найти такой входной символ и такой блок в Ри чтобы его можно было разбить относительно этого входного символа и получить тем самым новое разбиение. Для этого разбиения также будет выполняться свойство неэквивалентности состояний, принадлежащих разным блокам. Повторяем процесс до того момента, когда дальнейшее разбиение становится невозможным. Так как мы не устанавливаем порядок, в котором входные символы и блоки проверяются на возможность разбиения, последовательность разбиений можно порождать многими способами. Последнее разбиение, однако, будет одним и тем же во всех случаях. В нашем примере после Pi можно продолжать процесс следующим образом:
2.11. Получение минимального автомата 47 Разбивая блок {3, 4} из Pi относительно а, получаем /\=({1, 2}, {3}, {4}, {5, 6, 7}). Разбивая {5, 6, 7} из Р2 относительно а или Ь, получаем Я.= ({1, 2}, {3}, {4}, {5}, {6, 7}). Р3 не допускает дальнейшего разбиения. Чтобы убедиться в этом, заметим, что все состояния блока {1,2} переходят относительно а в состояния блока {6, 7}, а относительно Ь — в состояния блока {3}. Аналогично блок {6, 7} переходит в блоки {4} и {1, 2} относительно а и Ъ соответственно. Оставшиеся блоки имеют по одному элементу и поэтому автоматически исключают дальнейшее разбиение. Когда процедура закончена, состояния внутри каждого блока эквивалентны. В нашем примере эквивалентны состояния 1 и 2, 6 и 7. Чтобы понять, почему эти состояния должны быть эквивалентными, можно рассуждать следующим образом. Поскольку дальнейшее разбиение невозможно, входные символы, примененные к состояниям одного блока, переводят их в состояния, которые снова принадлежат одному блоку. Так как это положение справедливо для всех блоков и всех входных символов, оно должно выполняться и тогда, когда входные символы образуют входные цепочки. Вследствие того, что Рй было разбиением на допускающие и отвергающие состояния, каждый блок любого последующего разбиения содержит либо только допускающие, либо только отвергающие состояния. Таким образом, если для пары состояний существует различающая цепочка, то она переводит их в разные блоки. Отсюда мы заключаем, что состояния, принадлежащие одному блоку в последнем разбиении, не могут иметь различающих цепочек и должны быть эквивалентными. Блоки последнего разбиения можно использовать для построения нового автомата, который эквивалентен исходному и не содержит эквивалентных состояний. Такой автомат для нашего примера изображен на рис. 2.18, б. Множество состояний нового автомата — это множество блоков последнего разбиения. Переходы нового автомата мы получили из старого, прослеживая переходы из блока в блок для каждого входного символа. Так, на рис. 2.18, б элементом таблицы для состояния {1, 2} и входа а является состояние {6, 7}, потому что состояния блока {1, 2} последнего разбиения Р3 переходят в состояния блока {6, 7} при входе а. Начальным состоянием нового автомата будет просто блок, содержащий начальное состояние исходного автомата, а допускающими состояниями будут те блоки, которые содержат допускающие состояния исходного автомата. Автомат, изображенный на рис. 2.18,6, не имеет недостижимых состояний и является поэтому минимальным для автомата рис. 2.18, а.
48 Гл. 2. Конечные автоматы В общем случае процедура разбиения должна сопровождаться выбрасыванием недостижимых состояний в целях получения минимального автомата. Не имеет значения, которая из двух процедур выполняется первой. Теперь попытаемся привести автомат, изображенный на рис. 2.7 и предназначенный для распознавания цепочек, которые могут следовать за словом INTEGER в операторе Фортрана. Прежде всего замечаем, что автомат не имеет недостижимых состояний. Затем применяем процедуру разбиения. Сначала строим разбиение Р0, разделяя допускающие и отвергающие состояния: Ро=({1, 3, 4, 5, 6, 8, Е), {2, 7}). Разбивая {1, 3, 4, 5, 6, 8, Е} относительно входа v получаем Р,= ({1, 8}, {3, 4,5, 6, Е}, {2,7}) Разбивая {3, 4, 5, 6, Е) относительно входа ) получаем /V=({1,8}, {3, 5, Е), {4, 6}, {2,7}). Разбивая {3, 5, Е) относительно входа v получаем Р3=({1,8},{3, 5}, {Е}, {4, 6}, {2, 7}). Разбивая {2, 7} относительно входа ( получаем Я«=({1,8}, {3,5}, {Е}, {4,6}, {2}, {7}). Никакое дальнейшее разбиение невозможно, поэтому Р4 — последнее разбиение, которое выявляет эквивалентные состояния. v с , ( ) {1,8} А 2 (3,5) В {4,6} С 7 Е 2 Е С Е Е Е Е Е С Е Е Е Е А Е В А Е Е В Е Е Е Е Е Е Е 7 Е Е Рис. 2.19. О 1 О О 1 О Обозначая символами А, В и С блоки {1, 8}, {3, 5} и {4, 6} соответственно, а символом Е — соответствующий одноэлементный блок, мы получаем на рис. 2.19 таблицу переходов, которая изображает минимальный автомат для автомата рис. 2.7. Таким образом
2.12. Недетерминированные автоматы 49 получен автомат с шестью состояниями, который выполняет ту же работу, что и построенный нами сначала автомат с девятью состояниями. Вспомнив толкование состояний, данное в разд. 2.5, можно следующим образом интерпретировать полученные классы эквивалентности : {1, 8}—должен начаться непустой список объектов, {3, 5} — должна появиться размерность, {4, 6} — только что задана размерность. Попрактиковавшись, можно научиться опознавать роли, которые приводят к одной и той же ситуации, и получать тем самым исходные автоматы меньшего объема. Однако, каким бы ни было число включенных в исходный автомат ненужных состояний, с помощью описанного алгоритма его можно привести к минимальному конечному автомату. 2.12. Недетерминированные автоматы Теперь мы введем принадлежащее теории автоматов понятие не- детерминированного автомата. Этот термин может привести в недоумение, так как в русском и других языках понятие автомата, в сущности, не позволяет применять к нему прилагательное «недетерминированный». Поэтому в первую очередь надо иметь в виду, что недетерминированный автомат неудобно интерпретировать как модель поведения некоторого физического устройства. Еще одним поводом для недоразумений является то, что термин «недетерминированный» наводит на мысль о чем-то случайном, однако в недетерминированном автомате ничего случайного нет. Таким образом, второе, что нужно иметь в виду,— это то, что никакие вероятности здесь ни при чем. Недетерминированный автомат — это просто формализм для определения множеств цепочек. Слово «автомат» присутствует в его названии потому, что этот формализм является обобщением формализма, используемого для определения обычного детерминированного автомата. Недетерминированные конечные распознаватели важны, поскольку: 1) для заданного множества иногда легче найти недетерминированное описание; 2) существует процедура для превращения произвольного не. детерминированного конечного распознавателя в обычный конечный распознаватель. Недетерминированный конечный распознаватель представляет собой обычный распознаватель с той разницей, что значениями его
50 Гл. 2. Конечные автоматы функции переходов являются множества состояний, а не отдельные состояния, и вместо одного начального состояния задается множество начальных состояний. Таким образом, можно дать следующее определение: Недетерминированный конечный распознаватель задается: 1) конечным множеством входных символов, 2) конечным множеством состояний, 3) функцией переходов 6, которая каждой паре, состоящей из состояния и входного символа, ставит в соответствие множество новых состояний, 4) подмножеством состояний, выделенных в качестве начальных, 5) подмножеством состояний, выделенных в качестве допускающих. Если состояние sH0B принадлежит множеству новых состояний, приписанному функцией переходов «текущему» состоянию sTeK и входному символу х, то мы пишем х с .. о •'тек °нов Этим обозначением можно, конечно, пользоваться и в том случае, когда мы предпочитаем не интерпретировать его как переход некоторого реального автомата. Говорят, что автомат допускает входную цепочку, если она позволяет связать одно из его начальных состояний с одним из допускающих. Так, если для некоторого автомата справедливо *, *8 *3 s0 >• s1 »■ s2 »■ s3 где So — начальное, a s3 — допускающее состояние, то мы будем говорить, что входная цепочка XiX2x3 допускается этим автоматом. Перефразировав последний абзац несколько более формально, мы получаем следующее определение: Входная цепочка длины п допускается недетерминированным конечным распознавателем тогда и только тогда, когда можно найти последовательность состояний s0 ... sn, такую, что So —«начальное состояние, sn — допускающее состояние, и для всех i, таких, что 0<t^n, состояние st принадлежит множеству новых состояний, приписанных функцией переходов состоянию Si-! для i-ro элемента входной цепочки. Способ представления конечных распознавателей с помощью таблицы переходов легко распространяется и на представление недетерминированных конечных распознавателей. Необходимо сделать лишь два изменения. Во-первых, каждый элемент таблицы должен содержать множество состояний. Мы указываем это множе-
2.12. Недетерминированные автоматы 51 ство, просто перечисляя его элементы и не заключая их в скобки. Второе изменение состоит в том, что начальные состояния указываются с помощью стрелок, расположенных перед метками соответствующих строк. Если таких стрелок нет, подразумевается, что есть только одно начальное состояние, а именно состояние, соответствующее первой строке. На рис. 2.20 изображена таблица переходов, представляющая недетерминированный конечный распознаватель. Множество состояний— {Л, В, С}, входное множество— {0, 1}, допускающие состояния — {В, С} и начальные состояния — {А, В). Переходы такие: б(Л, 0) = {Л, В}, б (В, О) = {0}, б (С, 0)={ }, б (Л, 1)={С}, Ь(В, 1)={С}, б(С, 1) = {Л, С}. Цепочка 11 — одна из допускаемых автоматом цепочек, так как B-Lc\c, причем В — начальное состояние, а С — допускающее. Существования одной этой последовательности переходов достаточно для того, чтобы показать допустимость входной цепочки 11, и существование другой последовательности переходов из начального состояния в отвергающее, например вХс-^А на это не влияет. Один из переходов недетерминированного распознавателя, а именно б (С, 0) является переходом в пустое множество. Это попросту означает, что для состояния С и входа 0 дальнейшие переходы не' возможны. Такой элемент таблицы переходов может препятствовать существованию последовательности переходов для некоторой входной цепочки. В данном примере такой цепочкой является 10; так как 1 переводит оба начальных состояния в состояние С, множество преемников которого пусто. Такие входные цепочки просто отвергаются наряду со всеми прочими цепочками, которые не могут перевести начальное состояние в заключительное. «Работу» недетерминированного автомата можно интерпретировать двояким образом. Покажем это на примере автомата, приведенного на рис. 2.20. Пусть автомат находится в состоянии Л, и к нему применяется входная цепочка, начинающаяся с 0. Тогда . можно представить себе один из следующих вариантов: А В С А.В С В С А,С Рис. 2.20. 0 1 1
52 Гл. 2. Конечные автоматы 1. Автомат осуществляет выбор, переходя либо в А, либо в В, т. е. в одно из новых состояний, соответствующих старому состоянию А и входу 0. Автомат продолжает работать подобным образом, и при этом возможно много выборов. Если имеется какая-нибудь последовательность выборов, при которой автомат под действием входной цепочки заканчивает работу в допускающем состоянии, то говорят, что эта входная цепочка допускается автоматом. Подчеркнем, что достаточно только одной последовательности выборов, приводящей к допускающему состоянию, и автомат допускает данный вход, даже если имеется много других последовательностей выборов, которые не ведут к допускающему состоянию. 2. Автомат распадается на два автомата, один — в состоянии Л, а другой — в состоянии В. При продолжении обработки входа происходит дальнейшее деление каждого автомата в соответствии с возможностями, содержащимися в таблице переходов. Когда вход обработан, цепочка допускается, если один из результирующих автоматов находится в допускающем состоянии. Эти две интерпретации эквивалентны, и обе полезны для понимания недетерминированного автомата. Однако автомат служит не для моделирования этих ситуаций. Его назначение состоит в определении допустимого множества входных цепочек. Пример Построим недетерминированный автомат с входным алфавитом {А, Л, Н, О, С, Ь} который допускает только две цепочки ЛАССО и ЛАНЬ. Применяя технику пометки символов, находим следующие очевидные роли: Со — Лг- Аг- Сг- с2- 0- л2- л2- н- ь — начальное состояние Л в ЛАССО • А в ЛАССО - первое С в ЛАССО - второе С в ЛАССО - О в ЛАССО - Л в ЛАНЬ - А в ЛАНЬ -Н в ЛАНЬ Ь в ЛАНЬ Затем эти роли преобразуются в таблицу переходов, изображенную на рис. 2.21. Недетерминированность проявляется двояким образом. Во-первых, поскольку оба Л — из ЛАССО и из ЛАНЬ —
2.13. Эквивалентность конечных распознавателей 53 могут встречаться сразу после начального состояния, мы просто помещаем как Ль так и Л2 в этот элемент таблицы. Во-вторых, во- многих местах встречается буква, которая не может быть правильным продолжением слова, и эти места просто оставляются незапол- Н С Л О A b "о А, С, С2 О Л2 А2 н Ь л„л2 И о О О О о 1 о ! о ! О ! I ! 1 Рис. 2.21. ненными или пустыми как указание на то, что продолжение невозможно. Это освобождает нас от введения состояния ошибки, что мы делали в разд. 2.5, когда пытались непосредственно по ролям строить детерминированную таблицу. 2.13. Эквивалентность недетерминированных и детерминированных конечных распознавателей Понятие недетерминированного конечного распознавателя приобретает практическое значение благодаря следующему факту: Для каждого недетерминированного конечного распознавателя существует детерминированный конечный распознаватель, который допускает в точности те же входные цепочки, что и недетерминированный. В данном разделе мы покажем, как можно найти этот эквивалентный детерминированный автомат.
54 Гл. 2. Конечные автоматы Основная идея построения заключается в том, что после обработки отдельной входной цепочки состояние детерминированного автомата будет представлять собой множество всех состояний недетерминированного автомата, которые он может достичь из начальных состояний после применения данной цепочки. Переходы детерминированного автомата можно получить из недетерминированных переходов, вычисляя множество состояний, которые могут следовать после данного множества при различных входных символах. Допустимость цепочки определяется по тому, является ли последнее детерминированное состояние, которого он достиг, множеством недетерминированных состояний, включающим хотя бы одно допускающее состояние. Результирующий детерминированный автомат является конечным, так как существует лишь конечное число подмножеств недетерминированных состояний. Если недетерминированный автомат имеет п состояний, то эквивалентный детерминированный автомат, который мы только что описали, может, вообще говоря, иметь 2" состояний, по числу подмножеств исходного множества состояний. На практике многие из этих подмножеств представляют собой недостижимые состояния. В приводимой ниже процедуре переходы строятся только для тех подмножеств, которые действительно необходимы. Процедура задается следующими пятью шагами. Пусть Мв — недетерминированный автомат, а МЛ — эквивалентный ему детерминированный автомат, который нужно построить. 1. Пометить первую строку таблицы переходов для Ма множеством начальных состояний автомата Мя. Применить к этому множеству шаг 2. 2. По данному множеству состояний S, помечающему строку таблицы переходов автомата Мл, для которой переходы еще не вычислены, вычислить те состояния М„, которые могут быть достигнуты из S с помощью каждого входного символа х, и поместить множества последующих состояний в соответствующие ячейки таблицы для Мд. Символически это выражается так. Если б — функция недетерминированных переходов, то функция детерминированных переходов б' задается формулой 6'(S, *)={s|s принадлежит 8(t, х) для некоторого t из S} 3. Для каждого нового множества, порожденного переходами на шаге 2, посмотреть, имеется ли уже в Мд строка, помеченная этим множеством. Если нет, то создать новую строку и пометить ее этим множеством. Если множество уже использовалось как метка, никаких действий не требуется. 4. Если в таблице автомата Мл есть строка, для которой еще не вычислены переходы, вернуться назад и применить к этой строке шаг 2. Если все переходы вычислены, перейти к шагу 5.
2.13. Эквивалентность конечных распознавателей 55 5. Пометить строку как допускающее состояние автомата МЛ тогда и только тогда, когда она содержит допускающее состояние недетерминированного автомата. В противном случае пометить как отвергающее состояние. Проиллюстрируем эту процедуру на примере недетерминированного автомата рис. 2.20. Результат применения шага 1 изображен на рис. 2.22, а. Применяя шаг 2 к {А, В}, обнаруживаем, что б'({Л, В}, 0)={Л, В} и 8'({А, В}, 1)={С}. См. рис. 2.22, б. Применяя шаг 3, мы видим, что уже имеется строка для {А, В}, но не для {С}. Поэтому создаем новую строку для {С}, получая тем самым конфигурацию на рис. 2.22, в. Переходя к шагу 4, обнаруживаем, что надо применить шаг 2 к {С}. После того как это сделано, на шаге 3 выясняется, что нужны еще две строки (рис. 2.22, г). Применение шага 2 к {А, С} и к пустому множеству { }дает нам переходы в множества, которые уже являются именами состояний. Этот результат изображен на рис. 2.22, д. Теперь шаг 4 предписывает нам перейти к шагу 5. Состояние {А, В} отмечается как допускающее, поскольку оно содержит допускающее состояние В; состояния {С} и {Л, С} отмечаются как допускающие, так как содержат допускающее состояние С. Пустое множество, разумеется, не содержит допускающего состояния и поэтому помечается как отвергающее. Результат приведен на рис. 2.22, е, где изображен окончательный вариант детерминированного автомата, эквивалентного исходному недетерминированному. Чтобы напомнить, что множества на рис. 2.22, е — это просто имена состояний нового автомата, мы подставляем новый набор имен, получая на рис. 2.22, ж таблицу состояний, которая задает тот же самый автомат, но в более простых обозначениях. В качестве еще одного примера применим процедуру к автомату на рис. 2.21. Результат изображен на рис. 2.23. Так как большинство переходов приводят к пустому множеству, мы обозначаем это состояние в таблице пробелом, чтобы не загромождать ее символами { }. Состояние, соответствующее пустому множеству, на самом деле является состоянием ошибки, введенным в разд. 2.5; это отвергающее состояние, которое переходит само в себя для всех входов и имеет место только тогда, когда для данной входной цепочки нет допустимых продолжений. Теоретически недетерминированный автомат с десятью состояниями, изображенный на рис. 2.21, может иметь детерминированный вариант с 1024 состояниями, в соответствии с числом подмножеств множества, содержащего десять состояний, но на самом деле потребовалось только девять состояний. Это меньше исходного числа. Таким образом, мы видим, что порождение только необходимых подмножеств — чрезвычайно полезный прием.
56 Гл. 2. Конечные автоматы {А,В}, {А,В\ 0 ;,Г 0 {А,В} 6 1 1 {С} {А.В) {С} {} {А.С} 0 {А,В} О {) {А,В} д 1 {С) {А.С} {} И,с} {А,В} {С} {А.В} б {С} {А,В) {С} {) {А.С) {А,В) {С} { ) {А,С}. {) (} {А.В) {А.С} е 1 1 0 1 {AS} {С} {} {Л,С} {AS} U {С} {АС} 1 2 3 4 1 3 3 1 2 4 3 4 1 1 0 1 2 Рис. 2.22.
2.14. Пример: константы языка MINI-BASIC 57 Глядя на таблицу переходов, изображенную на рис. 2.23, мы замечаем, что не надо делать различия между ролями Лг и Л2, а также Л, и А2. Алгоритм позволяет нам не заботиться об этом при создании исходного распознавателя. Н Л {с0} {лм {АиА2) {С,} {С2) W) {Н} {Ь} {} <«} {ДМ {А„Аг) {С,} {С2} {0} Ь] о о о о о 1 о 1 о предполагается, что незаполненные элементы содержат {} Рис. 2.23. Хотя процедура гарантирует, что детерминированный автомат не содержит недостижимых состояний, детерминированный автомат может оказаться не минимальным. В последнем примере состояния {О} и {Ь} явно эквивалентны и могут быть объединены. 2.14. Пример: константы языка MINI-BASIC Мы уже знаем, как использовать конечные автоматы для определения множеств цепочек и как превращать эти автоматы в процессоры, которые могут выдавать «ДА», когда входная цепочка допускается, или «НЕТ», когда цепочка отвергается. На практике конечные процессоры должны делать нечто большее, чем просто выдавать положительный или отрицательный ответ. Теория автоматов не может сказать нам, как расширять конечные распознаватели до процессоров специального назначения, поскольку требования, предъявляемые к таким процессорам, меняются от приложения к приложению. Таким образом, мы достигли того момента, когда разработчик ком-
58 Гл. 2. Конечные автоматы пиляторов должен применить свои творческие способности, чтобы довести дело до конца. Этот последний шаг — от распознавателя к практическому процессору — состоит обычно в дополнении переходов короткими процедурами. Чтобы посмотреть, как можно расширить распознаватель до практического процессора, мы построим процессор, распознающий константы языка MINI-BASIC и превращающий их в последовательности битов, которые являются их внутренним представлением в виде чисел с плавающей точкой. В гл. 4 эта конструкция включена в лексический блок компилятора для языка MINI-BASIC. На первом шаге вычисления последовательности битов число представляется в виде двух целых чисел, которые мы будем называть значащей частью и порядком *). Порядок — это степень десяти, на которую нужно умножить значащую часть, чтобы получить фактическое число. Так, число 12.35Е14 будет представлено значащей частью 1235 и порядком 12. Затем мы преобразуем эту пару целых чисел в соответствующую последовательность битов. Так как детали этого шага существенно зависят от особенностей конкретной машины, мы просто предположим, что такая процедура у нас имеется, и больше не будем о ней говорить. Мы предполагаем также, что разработчик может видоиз: менять наши процедуры с учетом разного рода переполнений, которые могут иметь место при построении целых чисел. Так как лексический анализатор для языка MINI-BASIC будет трактовать знак унарной операции + или — как отдельный символ, то в данном примере мы полагаем, что такой знак не может встретиться перед константой. Мы строим автомат для обработки констант без знака. В этом примере входное множество распознавателя не совпадает с множеством символов языка MINI-BASIC и состоит лишь из четырех символов, а именно ЦИФРА • Е ЗНАК Мы предполагаем, что имеется препроцессор, переводящий символы MINI-BASIC'a в эти четыре входные символа, играющие роль лексем. Конкретно имеется в виду такой перевод символов языка MINI-BASIC: 1. Каждая из цифр 0, 1,2, 3, 4, 5, 6, 7, 8 и 9 переводится в пару, состоящую из символа ЦИФРА и значения этой цифры. 2. Каждый знак + или — переводится в пару, состоящую из символа ЗНАК и указания на то, какой это знак. 3. Буква Е переводится в символ Е; точка переводится в точку, причем обе эти лексемы не имеют значений. 1) В оригинале integer part и exponent part,— Прим. ред.
2.14. Пример: константы языка MINI-BASIC 59 Лексический блок MINI-BASlC'a, построенный в гл. 4, включает аналогичную предварительную обработку множества символов. Выписав несколько^цепочек, мы можем, выделить семь ролей: 3 8 • 7 Е —' 3 • 9 Е 2 1 11234 56 73466 Эти роли можно описать так: 2 1) цифра перед возможно присутствующей десятичной точкой, =. 2) необязательно присутствующая десятичная точка, ■-, 3) цифра после десятичной точки, ^ 4) буква Е, 5) знак порядка, 1 6) цифра порядка, ^ 7) десятичная точка, требующая десятичных цифр. ' Одна из особенностей этого списка ролей состоит в необходимости различать роли 2 и 7, что может показаться неочевидным. Отличие заключается в том, что после вхождения символа в роли 2 может следовать не только цифра, но и Е, а также может не следовать ничего, как в цепочках 38.Е — 21 и 38. После вхождения символа в роли 7 должна следовать цифра, так как цепочки, вроде.Е— 21, или одна десятичная точка не считаются правильно построенными константами. Затем мы вводим начальное состояние 0 и используем это состояние вместе с ролями для построения таблицы переходов. Как обычно, переходы заполняются в соответствии с тем, какие роли могут следовать одна за другой, а в случае переходов из начального состояния — с учетом того, какие роли могут встречаться в начале цепочек. Роли, которые могут завершать цепочку, отмечаются как допускающие состояния. Результат показан на рис. 2.24, а. Полученный автомат фактически детерминированный, если пробелы в таблице интерпретировать как переходы в состояние ошибки, не включенное в список. Очевидно, этот автомат не является приведенным, так как состояния 2 и 3 эквивалентны, поэтому следует объединить их в одно состояние, назовем его 23. Результат показан на рис. 2.24, б. Просмотр этой таблицы переходов показывает, что она приведенная. Следующий шаг построения процессора состоит в добавлении к входному множеству концевого маркера и присвоении каждому переходу отдельного имени. Результат показан на рис. 2.25. Так как в исходной таблице переходов было два перехода в состояние 1, мы назвали эти переходы 1а и lb, чтобы различать их в процедурах обработки, описываемых ниже. Аналогично различаются переходы в 23, 4 и 6. Допускающим состояниям, в которые автомат переходит при чтении концевого маркера, также даны разные имена.
60 Гл. 2. Конечные автоматы ЛУеханическая часть построения процедуры завершена, и начинается специальная часть. Переходы автомата интерпретируются как вызовы процедур или частей объектного кода, которые должны выполняться при переходах. Задача разработчика — описать эти Цифра Е Знак Цисрра £ Знак 0 1 2 3 4 5 6 1 1 1 3 3 6 6 6 3 4 4 4 а 7 2 5 0 1 Т 1 0 0 1 0 Рис. 0 1 23 4 5 6 7 2.24. 1 1 23 6 6 6 23 4 4 7 23 5 6 0 1 1 0 0 1 0 процедуры так, чтобы получить желаемый результат, который заключается в данном случае в переводе входной цепочки в пару, состоящую из значащей части и порядка. Цифра Знак 0 1 23 4 5 6 7 1а 1/; 23а 6э 6Ь 6с 23Ь 4а 4Ь 7 23с 5 ДА 1 ДА2 ДАЗ Рис. 2.25. Чтобы выполнить это преобразование, заведем четыре переменные: РЕГИСТР ЧИСЛА, РЕГИСТР ПОРЯДКА, РЕГИСТР СЧЕТЧИКА и РЕГИСТР ЗНАКА. Назначение этих переменных таково:
2.14. Пример: константы языка MINI-BASIC 61 РЕГИСТР ЧИСЛА — для накопления значащей части, РЕГИСТР ПОРЯДКА — для накопления порядка, РЕГИСТР СЧЕТЧИКА — для подсчета разрядов, следующих за десятичной точкой, РЕГИСТР ЗНАКА — для запоминания знака порядка. Теперь обеспечим каждый переход процедурой. Здесь мы ограничимся словесным описанием процедур. Разработчик может превратить их в программу для своей конкретной машины. Для понимания этих процедур нужно помнить интерпретацию состояний и расположение переходов в таблице. Например, \а — это действие, предпринимаемое, когда встречается первая цифра, alb — когда встречается следующая цифра. Процедуры переходов таковы: 1а: Поместить значение цифры в РЕГИСТР ЧИСЛА. Для очередного входного символа сделать переход из состояния 1. lb: Умножить содержимое РЕГИСТРА ЧИСЛА на 10. Прибавить значение цифры и запомнить результат в РЕГИСТРЕ ЧИСЛА. Для очередного входного символа сделать переход из состояния 1. 23а: Увеличить РЕГИСТР СЧЕТЧИКА на 1. Умножить содержимое РЕГИСТРА ЧИСЛА на 10. Прибавить значение цифры и запомнить результат в РЕГИСТРЕ ЧИСЛА. Для очередного входного символа сделать переход из состояния 23. 236: Инициализировать РЕГИСТР СЧЕТЧИКА единицей '). Поместить значение цифры в РЕГИСТР ЧИСЛА. Для очередного входного символа сделать переход из состояния 23. 23с: Инициализировать РЕГИСТР СЧЕТЧИКА нулем. Для очередного входного символа сделать переход из состояния 23. 4а: Инициализировать РЕГИСТР СЧЕТЧИКА нулем. Для очередного входного символа сделать переход из состояния 4. 4Ь: Для очередного входного символа сделать переход из состояния 4. *) «Инициализировать икс игреком» на программистском жаргоне означает «дать иксу начальное значение игрек» или «установить икс в начальное состояние игрек»,— Прим, ред.
62 Гл. 2. Конечные автоматы 5: Если знак +, заслать +1 в РЕГИСТР ЗНАКА. Если знак —, заслать —1 в РЕГИСТР ЗНАКА. Для очередного входного символа сделать переход из , состояния 5. 6а: Поместить +1 в РЕГИСТР ЗНАКА. Поместить значение цифры в РЕГИСТР ПОРЯДКА. Для очередного входного символа сделать переход из состояния 6. &>: Поместить значение цифры в РЕГИСТР ПОРЯДКА. Для очередного входного символа сделать переход из состояния 6. 6с: Умножить содержимое РЕГИСТРА ПОРЯДКА на 10. Прибавить значение цифры и запомнить результат в РЕГИСТРЕ ПОРЯДКА. Для очередного входного символа сделать переход из состояния 6. 7: Для очередного входного символа сделать переход из состояния 7. ДА1: Заслать 0 в РЕГИСТР ПОРЯДКА. Прекратить работу автомата. ДА2: Заслать в РЕГИСТР ПОРЯДКА значение РЕГИСТРА СЧЕТЧИКА с минусом. Прекратить работу автомата. ДАЗ: Если РЕГИСТР ЗНАКА равен —1, сделать отрицательным РЕГИСТР ПОРЯДКА. Вычесть РЕГИСТР СЧЕТЧИКА из РЕГИСТРА ПОРЯДКА. Прекратить работу автомата. Следует заметить, что, говоря «Для очередного входного символа сделать переход из состояния s», мы не подразумеваем, что это надо реализовать каким-то особым способом. Мы просто имеем в виду, что состояние s и очередной входной символ используются для выбора следующего перехода. Заметим также, что при записи процедур мы не воспользовались тем, что некоторые из них имеют общую часть, как, например, процедуры 6а и 6Ь. Это было сделано для ясности. На практике мы написали бы что-нибудь вроде: 6а: Заслать +1 в РЕГИСТР ЗНАКА. 6Ь: Поместить значение цифры в РЕГИСТР ПОРЯДКА. Для очередного входного символа сделать переход из состояния 6.
2.14. Пример: константы языка MINI-BASIC 63 Чтобы завершить построение процессора, нужно поместить в пустые ячейки таблицы переходов вызовы процедур, обрабатывающих ошибки. Мы не будем здесь обсуждать, какими должны быть эти процедуры. Новые значения регистров Входной i Л ^ символ Переход Число Порядок Счетчик Знак 4 7 • 2 Е - 1 3 н 1а ■\ь 53с 53а АЬ 5 6Ь 6с ДАЗ А 47 47 472 472 472 472 472 472 — - - - - - 1 13 -14 — - 0 — - - - - -1 -1 -1 -1 4 Е 6 Ч 1а 4а 6а ЛАЗ 4 4 4 4 - - 6 б — 0 0 0 - - +1 +1 • 8 н 7 23Ь ДА 2 _ 8 8 — - -1 — 1 1 — - - 3 н 1э ДА1 3 3 - 0 - - - - Рис. 2.26. Чтобы посмотреть, как работает автомат, и приобрести некоторую уверенность в том, что он действительно работает, мы сейчас построим несколько проверочных «программ». Как минимум, под действием этих программ каждый переход будет сделан хотя бы
64 Гл. 2. Конечные автоматы один раз. Один набор программ, вызывающий все переходы, состоит из следующих четырех программ: 47. 2Е-13 4Е6 .8 3 Действие этих программ на процессор показано на рис. 2.26. Каждая строка рисунка изображает конфигурацию, состоящую из введенного символа, перехода, который он вызвал, и новых значений переменных, присвоенных процедурами переходов. Заметим, что если процессор реализован как программа для вычислительной машины, эти тестовые программы можно использовать в целях отладки. На самом деле отладочные программы и их результаты может задавать сам разработчик; при этом, если программы проходят правильно, то возрастает уверенность в том, что процессор также работает правильно. Чтобы проверить процедуры, обрабатывающие ошибки, которые, как мы договорились, печатают сообщения об ошибках для программирующего на языке MINI-BASIC, необходимо придумать входную цепочку для каждого из переходов, приводящих к состоянию ошибки Например, переход из состояния 4 для входа Е можно проверить с помощью входной цепочки ЦИФРА Е Е Идея этого примера состоит в том, что теория позволила нам разбить большую задачу (как обрабатывать цепочки) на ряд небольших задач (как дополнить переходы). Решение каждой из этих маленьких задач можно записать в несколько строчек. 2.15. Замечания по литературе Модель конечного автомата была введена Хафменом [1954], Муром [1956] и Мили [1955], каждый из которых занимался вопросами приведения автоматов. Весьма абстрактная точка зрения на автоматы представлена в работе Рабина и Скотта [1959], в ней содержатся обсуждавшиеся здесь результаты о недетерминированных автоматах. Многие из первоначальных работ по конечным автоматам были переизданы в сборнике под ред. Мура [1964]. В книгах Гинзбурга [1962], Хартманиса и Стирнза [1966] дано детальное математическое изложение некоторых аспектов теории конечных автоматов. Другие книги, содержащие много материала по конечным автоматам, написаны Хенни [1968], Минским [1967], Бутом [1967], Харрисоном 11965] и Гиллом [1962].
Упражнения 65 Упражнения t. Постройте конечный автомат, который будет распознавать слова GO TO причем между ними может быть произвольное (включая нулевое) число пробелов. 2. а) Найдите самую короткую цепочку, распознаваемую автоматом, изображенным на рисунке. б) Найдите четыре других цепочки, распознаваемых этим автоматом. в) Найдите четыре цепочки, которые отвергаются этим автоматом. А В С D Е F 0 D А А В В Е 1 А С F С С А 0 0 0 0 1 1 3. Сначала постройте конечные распознаватели для описанных ниже множеств цепочек из нулей и единиц. Затем превратите каждый распознаватель в процессор с концевым маркером. Наконец сделайте так, чтобы процессор обнаруживал допустимость и недопустимость цепочек как можно скорее. а) Число единиц четное, а число нулей — нечетное. б) Между вхождениями единиц четное число нулей. в) Число вхождений пары 00 нечетное, причем допускаются наложения пар друг на друга. г) За каждым вхождением пары 11 следует 0. д) Каждый третий символ — единица. е) Имеется по крайней мере одна единица. 4. Постройте конечный автомат с входным алфавитом {0, 1}, который допускает в точности такое множество цепочек: а) Все входные цепочки. б) Ни одной входной цепочки. в) Входную цепочку 101. г) Две входные цепочки: 01 и 0100. д) Все входные цепочки, кончающиеся на 1 и начинающиеся с 0. е) Все цепочки, не содержащие ни одной единицы. ж) Все цепочки, содержащие в точности три единицы. з) Все цепочки, в которых перед и после каждой единицы стоит 0. и) Пустую цепочку и 011. к) Все входные цепочки, кроме пустой. 5. а) Постройте конечный автомат, который будет распознавать следующие зарезервированные слова Алгола 60: STEP, STRING, SWITCH б) Оцените, сколько состояний понадобится для распознавания всех зарезервированных слов Алгола 60. 3 Ф. Льюис и др.
66 Гл. 2. Конечные автомоты 6. Опишите словами множества цепочек, распознаваемых каждым из следующих автоматов: А В С В в с А С С 0 0 1 А В С В С С С В С 0 1 0 О 1 А В С D В / D С D С В D D 0 1 1 0 А В С D В D С D А С D D 0 0 1 0 А В С D Е F в F С Е F F F С D С F F F F С С F F 0 0 0 0 (1 0 7. Для каждого из автоматов найдите входную цепочку или цепочки с минимальной суммарной длиной, такие, что под нх действием а) каждое состояние имеет место хотя бы раз, б) каждый переход происходит хотя бы раз.
Упражнения 67 О 1 А В С D Е F С D А А F Е В В Е А Е А О 1 О О 1 1 А В С D Е F А В С D В F Е а F в F с О 0 1 1 1 О 8. Найдите различающую цепочку (если она существует) для такой пары автоматов: 0 1 0 1 А В С D А С в А В D А В 0 0 1 0 А В с D А А В С D D А В i 0 1 0 9. Найдите недостижимые состояния автомата О 1 2 А В С D Е F G Н 1 J С J J F Е D Н G D В E E A A J 1 A J F H G G H G H A J В G G 0 1 0 1 0 1 0 1 0 1
68 Гл. 2. Конечные автоматы 10. Найдите минимальную эквивалентную следующих автоматов: О 1 *1 s2 S3 SA S5 S6 S7 *1 h S6 *1 *1 h h S3 *4 S5 S4 SA S6 h 0 1 0 1 0 1 0 a А С INIT С CA CAL CALL CE CEL CELL Ошибка Ошибка CA Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка С Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка б 11. Превратите автомат, изображенный i маркером, который наиболее быстро 12. Напишите сообщение об ошибке для автомата на рис. 2.19, таблицу переходов для каждого из х у 4 5 4 2 1 1 2 1 1 5 6 7 4 5 6 Е L Ошибка СЕ Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка CAL CALL Ошибка CEL CELL Ошибка Ошибка 0 0 0 0 1 0 0 I 0 ia рис. 2.19, в процессор с концевым обнаруживает ошибки, каждого перехода в состояние ошибки
Упражнения 69 13. Многие компиляторы с Алгола 60 содержат ограничение, состоящее в том, что в описаниях собственных массивов границы массива должны быть целочисленными константами. Типичны описания такого вида: OWN REAL ARRAY Al[4:6j OWN INTEGER ARRAY A2[—1 : 3,12 : +4, 171 : 173] а) Постройте конечный автомат, который распознает часть описания, следующую за идентификатором. Пусть входной алфавит будет [ ]: ,- + ц где ц — лексема цифра. б) Превратите этот автомат в процессор с концевым маркером, который наиболее быстро обнаруживает ошибки. в) Найдите множества цепочек, под действием которых каждый переход автомата происходит хотя бы один раз. 14. Опишите словами множества цепочек, распознаваемые каждым из недетерминированных автоматов, изображенных на рисунке. 0 0 1 А В С В С,А 6 0 0 1 А В С D В С,О В В 0 0 0 1 в 15. Найдите детерминированный автомат, эквивалентный такому недетерминированному автомату: а Ь 1 2 3 2,3 1 2 1 3 0 0 1 16. Постройте конечный автомат, который будет допускать только те цепочки, которые можно построить из подцепочек GO, GOTO, TOO, ON Возможны повторения, но не пересечения. Так, одна из допустимых цепочек GOONGOTOONGOTOOON Можно построить сначала недетерминированный автомат, а затем сделать его детерминированным. 17. Рассмотрим язык, состоящий из арифметических выражений, в которых используются операции + и — (как унарные, так и бинарные), * и /, но нет скобок. Пусть операнды — либо целочисленные константы (например, 314), либо идентификаторы, являющиеся цепочками из букв и цифр, начинающихся с буквы (например, D12).
70 Гл. 2. Конечные автоматы а) Пользуясь методом ролей, постройте минимальный конечный распознаватель для этих выражений. Пусть входной алфавит будет + — * / б ц, где б — обозначает букву, а ц — цифру. б) Пусть арифметические операции выполняются строго слева направо, так что, например, значение выражения 3+5*2 равно 16. Разработайте специальные процедуры, которые нужно добавить к распознавателю, чтобы порождать машинные команды для этих выражений. Адреса идентификаторов в рабочей программе считаются известными. 18. а) Постройте конечный автомат, который будет распознавать изображенные римскими цифрами числа, меньшие 2000. б) Превратите этот автомат в процессор, который распознает римские цифры и переводит их в соответствующие последовательности битов. в) Найдите множество цепочек, под действием которых осуществляются все переходы этого процессора, и опишите работу автомата применительно к этим цепочкам. г) Введите в процессор сообщения об ошибках, желательно на латыни. 19. а) Найдите множество входных цепочек, не совпадающее с приведенным ранее в тексте, под действием которых осуществляются все переходы автомата, изображенного на рис. 2.25. б) Для указанных выше цепочек составьте таблицу, аналогичную изображенной на рис. 2.26. 20. а) Напишите сообщение об ошибке для каждого перехода в состояние ошибки автомата на рис. 2.25. б) Найдите множество входных цепочек, которые приводят в действие все переходы в состояние ошибки автомата рис. 2.25. 21. Напишите программу на любом языке, реализующую автомат, изображенный на рис. 2.25, и все процедуры, необходимые для его переходов. 22. Постройте таблицу переходов конечного автомата, входной алфавит которого состоит из тридцати одного символа: ОДИН, ДВА, ТРИ, . . ., ДЕВЯТЬ, ДЕСЯТЬ, ОДИННАДЦАТЬ ВОСЕМНАДЦАТЬ, ДЕВЯТНАДЦАТЬ, ДВАДЦАТЬ, ТРИДЦАТЬ ДЕВЯНОСТО, СТО, ДВЕСТИ, СТА, СОТ; пусть выходной алфавит состоит из десяти символов 0, 1, 2, ... 9, и пусть он допускает в качестве входа словесную запись любого числа от 1 до 999 (например, ВОСЕМЬСОТ ТРИДЦАТЬ ПЯТЬ) и переводит вход в эквивалентную цифровую запись (835). 23. Два автомата на рис. 2.35 допускают множества St и S2 соответственно. а) Постройте недетерминированные автоматы для i) SiUS2 (объединения множеств St и S2), ii) Sif|S2 (пересечения множеств Sj и S2). б) Найдите минимальные детерминированные автоматы, эквивалентные указанным недетерминированным автоматам. 0 1 0 1 С в с в с с Абтомат для множества S, Автомат для множества s2 D С D D В С D D
Упражнения 71 24. Рассмотрим множество всех распознавателей с двумя состояниями и входным алфавитом {0, 1 \. а) Сколько автоматов а этом множестве? б) Сколько в нем неэквивалентных автоматов? в) Опишите словами множество цепочек, распознаваемое каждым из этих неэквивалентных автоматов. 25. В некоторых приложениях начальное состояние конечного автомата не указывается. Он может начать работу в любом состоянии благодаря какому-либо внешнему воздействию. Два таких автомата эквивалентны, если каждому состоянию одного автомата эквивалентно некоторое состояние другого, и наоборот. Опишите процедуру, которая по такому автомату строит минимальный эквивалентный ему автомат. 26. а) Пусть k — длина наименьшей различающей цепочки для некоторой пары состояний конечного автомата. Покажите, что если k^\, то автомат содержит два других состояния, наименьшая' различающая цепочка которых имеет длину k— 1. б) Покажите, что если два состояния автомата, имеющего п состояний, неэквивалентны, то для них существует различающая цепочка длины п—2 или меньше. в) Постройте таблицу переходов для автомата с шестью состояниями, такого, что длина наименьшей цепочки, различающей два состояния, равна 4. 27. Рассмотрим два конечных автомата Мг и М.г с числом состояний /Vj и /V2 соответственно. Покажите, что недетерминированному конечному автомату, который распознает объединение множеств, распознаваемых автоматами /И] и /W2, требуется не более Л/,-г-Л/2 состояний. 28. Конечный автомат называется сильно связным тогда и только тогда, когда для любых двух его состоянии s, и s2 существует входная цепочка, которая переводит автомат из s, в s2. Покажите, что для автомата с N состояниями эту цепочку всегда можно выбрать такой, что ее длина не превышает N—1. 29. Входная цепочка называется отладочной для некоторого конечного автомата, если она вызывает каждый переход этого автомата хотя бы один раз. Покажите, что для сильно связного (см. упр. 28) автомата с N состояниями и / входными символами длина отладочной цепочки может не превышать /V2/. 30. Покажите, что конечный автомат не может распознавать все цепочки вида 1"0" (т. е. после некоторого числа единиц следует такое же число нулей). Эти цепочки аналогичны левым и правым скобкам в выражениях. Указание: Предположите, что автомат имеет всего г состояний. Заметьте, что если п>г, то при чтении подцепочки из единиц автомат по крайней мере дважды переходит в одно и то же состояние. 31. а) Приведите такой пример эквивалентных автоматов М и /V, чтобы некоторое состояние автомата М было эквивалентно более чем одному состоянию автомата /V. б) Приведите такой пример эквивалентных автоматов М и /V, чтобы для некоторого состояния автомата М в /V не было эквивалентных ему состояний. 32. Как проверить на эквивалентность два состояния недетерминированного автомата? 33. а) Постройте конечный автомат, который распознает химические формулы, составленные из восьми элементов: Н, С, N, О, SI, S, CL и SN. Элементы в формулах разделяются запятыми. Они могут появляться в любом порядке и в любых сочетаниях. Формулы не обязательно представляют реально
2 Гл. 2. Конечные автоматы существующие соединения. Вот несколько образцов формул: Н2, О О, Н7 SN, S, СМ CL N, Н4, С7, Н5, 02 02 Имеется девять входных символов: С Н I L N О S, ц где ц — обозначение цифры, б) Постройте конечный процессор, вычисляющий молекулярный ве нения, представленного входной цепочкой.
3 Реализация конечных автоматов 3.1. Введение В предыдущей главе мы обсуждали конечные автоматы с чисто теоретической точки зрения, лишь слегка касаясь их предполагаемого применения в качестве одного из основных составных блоков компилятора. В этой главе мы рассмотрим некоторые проблемы, связанные с реализацией конечного автомата или процессора как программы или подпрограммы для вычислительной машины. Этот материал относится также к реализации более общих моделей автоматов, обсуждаемых в последующих главах, так как конечный автомат используется в этих моделях как центральное управляющее устройство. На протяжении этой главы рассматриваются три взаимосвязанных вопроса: 1) как представлять входы, состояния и переходы конечного автомата, чтобы удовлетворить зачастую противоречивые требования, предъявляемые к реализации: быстродействие и небольшие затраты памяти, 2) как справиться с некоторыми специфическими проблемами, постоянно возникающими при компиляции, 3) как расчленить задачу построения компилятора, чтобы получить автоматы, допускающие простую реализацию. Некоторые задачи, решаемые с помощью конечных автоматов, заключаются всего лишь в распознавании конечного множества слов. Суть этих проблем в том, что компилятор должен обнаружить появление некоторого слова из такого множества и затем действовать в зависимости от того, какое это слово. Операторы MINI-BASIC'a, например, могут начинаться с одного из девяти слов (LET, IF, GOTO и т. д.), и для компилятора важно установить, с какого из них (если оно есть) начинается строка, и предпринять действия, соответствующие данному типу оператора. Задачу такого характера мы называем проблемой «идентификации», так как действия компи-
74 Гл. 3. Реализация конечных автоматов лятора зависят от идентичности некоторому известному слову данного слова, включенного в входную цепочку автомата. Так как для решения задач идентификации может потребоваться огромное число состояний, в подобных случаях часто приходится пользоваться специальными методами реализации. По этой причине нередко рекомендуется строить компилятор так, чтобы проблема идентификации решалась отдельным подпроцессором, специально предназначенным для этого. Существуют проблемы идентификации, которые, строго говоря, не могут быть решены с помощью конечного автомата. Обратимся к часто встречающейся проблеме распознавания переменных или идентификаторов некоторого языка и соотнесения их с соответствующими элементами таблицы имен. Решение этой проблемы обычным методом с помощью конечного автомата потребует нескольких состояний и элемента таблицы имен для каждого допустимого идентификатора. Однако множество допустимых идентификаторов в большинстве языков бесконечно или так велико, что его вполне можно считать бесконечным. В Алголе, например, где идентификатор может состоять из произвольного числа символов, множество допустимых идентификаторов бесконечно, тогда как в Фортране, где длина идентификатора не больше шести, число допустимых идентификаторов конгчно, но астрономически велико, так что на практике считается бесконечным. (В противоположность этому идентификаторы MINI- BASIC'a могут состоять не более чем из двух символов, поэтому число допустимых идентификаторов конечно и поддается обработке.) Понятно, что для языков, где число допустимых идентификаторов бесконечно или практически бесконечно, невозможно отвести место в памяти или элемент таблицы имен для каждого возможного идентификатора. Это затруднение преодолевается с помощью понятия расширяющегося конечного автомата. При считывании своей входной цепочки этот автомат отводит для идентификатора необходимое место в памяти и элемент в таблице, как только тот впервые встречается в программе. Если этот идентификатор встречается в программе снова, то, чтобы идентифицировать его, автомат использует технику идентификации слов с помощью конечного автомата. Когда появляется какой-то новый идентификатор, автомат снова расширяется и так далее. Хотя этот автомат не является, строго говоря, конечным, к нему применимы многие принципы построения и анализа конечных автоматов. Большинство методов идентификации потенциально полезны при идентификации как фиксированных множеств, для которых память отводится заранее, так и расширяющихся множеств. Поэтому при обсуждении этих специальных методов идентификации, помимо обычных требований, предъявляемых к времени и объему памяти, мы должны учитывать относительную легкость расширения.
3.2. Представление входных символов 75 3.2. Представление входных символов Чтобы смоделировать конечный автомат, мы должны каким-нибудь подходящим способом закодировать его входное множество. Наиболее гибкое решение состоит в представлении входного множества с помощью набора последовательных целых чисел, начиная с О или 1. Это кодирование хорошо работает во всех рассматриваемых Входное множество Конечный Транслитератор автомат а Символ a z 0 9 + и т.д. Перевод (БУКВА,а) (БУКВА,z) (ЦИФРА.О) (ЦИФРА, 9) (ОПЕРАЦИЯ, +) И Т.д. Типичная таблица тоанслитеэсции 6 Рис. 3.1. далее способах реализации переходов автомата. Однако некоторые из этих способов работают также и при произвольном кодировании входного множества. Если входом конечного автомата является выход какого-то другого блока компилятора, то обычно нетрудно построить этот блок так, чтобы он подавал на вход конечного автомата цепочки в наиболее удобном для него виде. Единственное место при построении компилятора, где может возникнуть проблема кодировки,— это процедура чтения символов исходной программы. В оставшейся части данного раздела мы займемся этим вопросом.
76 Гл. 3. Реализация конечных автоматов Если бы множество символов исходной программы непосредственно служило входом для некоторого конечного процессора, содержащего много состояний, то автомат имел бы большую таблицу переходов, так как одни только цифры и латинские буквы образуют множество из 36 символов, обычно же множество символов в не- сколько'раз больше. Удобный способ уменьшения размера таблицы переходов заключается в обработке исходных символов двумя последовательно соединенными автоматами: первый автомат — это транслитератор, единственная задача которого — сократить входное множество до приемлемых размеров; второй автомат выполняет остальную часть работы. Этот вид взаимодействия показан на рис. 3.1, а. Зависимость между входом и выходом транслитератора можно выразить с помощью таблицы, например, такой, как на рис. 3.1, б, которая содержит перевод каждого символа исходного языка. Выход этого транслитератора будем называть символьной лексемой. Такая лексема обычно состоит из двух частей: класса и значения. Так, буква а будет иметь класс БУКВА и значение а, тогда как знак операции + имеет класс ОПЕРАЦИЯ и значение +. Класс лексемы должен служить входом для второго автомата, а ее значение доступно процедурам переходов этого автомата. Потребность в такой пред-' варительной обработке упоминалась в гл. 2. (Вспомним, например, обработку констант). Транлитератор — это просто процессор с одним состоянием, хотя вряд ли имеет смысл рассматривать его именно в этом качестве. Разумеется, он реализуется как процедура, которая находит в таблице перевод для каждого входного символа. На многих машинах это можно сделать одной или двумя командами. Множество классов символьных лексем обычно представляется с помощью последовательных натуральных чисел, так как они являются возможными входными символами для второго автомата. 3.3. Представление состояний Есть два основных способа, с помощью которых программа, моделирующая конечный автомат, может запоминать состояние моделируемого автомата. Первый заключается в запоминании номера, соответствующего текущему состоянию, в некотором регистре или ячейке памяти вычислительной машины. Этот способ мы называем явным, так как состояние явно задается некоторой переменной. Второй основной метод заключается в том, что для каждого состояния имеется отдельная часть программы. Поэтому тот факт, что моделируемый автомат находится в заданном состоянии, «запоминается» тем, что моделирующая программа исполняет часть кода, которая «принадлежит» этому состоянию. Такой метод называется неявным.
3.4. Выбор переходов 77 Явный или неявный метод выбирается исходя просто из удобства программирования. Важно отметить, что слово «состояние» не подразумевает никакой особой техники реализации. 3.4. Выбор переходов Суть программы, моделирующей конечный процессор, состоит в способе выбора переходов, так как для заданного состояния и очередного входного символа программа должна выполнить переход, ука- 12 3 4 5 6 7-1 А В С А) С2 СЗ S3 АЛ въ -46 S6 С\ Адрес М кдрес процедуры обработки ошибки кдрес процедуры обработки ошибки кдрес ВЗ кдрес процедуры обработки ошибки кдрес А6 Адрес процедуры обработки ошибки кдрес процедуры обработки ошибки Входной символ Переход 1 6 4 А\ А6 S3 Переход по неудаче = Процедура обработки ошибки В Рис. 3.2. (а) Построение процессора, (б) вектор переходов, (в) список переходов. занный в таблице переходов. Поэтому информация, содержащаяся в таблице переходов, должна где-то храниться, или же ее надо включить в моделирующую программу. Предположим пока, что состояние запоминается неявно. В этом случае косвенно известна нужная строка таблицы переходов, и задача сводится к нахождению перехода только по входному символу.
78 Гл. 3. Реализация конечных автоматов Зта задача должна, разумеется, решаться для каждого состояния в отдельности. Рассмотрим два метода решения задачи выбора перехода для заданного входного символа. Мы называем их методом «вектора переходов» н методом «списка переходов». Согласно методу вектора переходов, адреса или метки тех процедур переходов, на которые должно передаваться управление, хранятся в виде вектора в последовательных ячейках памяти, по одной ячейке для каждого входного символа. Очередной входной символ служит индексом, по которому выбирается элемент вектора, дающий нужный переход. Чтобы этот метод работал, входное множество должно быть представлено каким-нибудь подходящим образом, например в виде множества последовательных целых чисел. Пример такого вектора приведен на рис. 3.2, б, где изображен вектор переходов для состояния А на рис. 3.2, а. Достоинство метода в том, что нужную процедуру перехода можно выбрать очень быстро. Затраты памяти — по ячейке на каждый элемент строки. В большинстве языков программирования высокого уровня этот метод можно реализовать, используя переключатель или вычисляемый переход. Согласно методу списка переходов, входные символы делятся на два класса: каждому входному символу первого класса приписывается индивидуальный переход, а все символы второго класса имеют общий переход (обычно процедуру, обрабатывающую ошибку). Для первого класса соответствие между входным символом и адресом процедуры перехода задается в виде списка упорядоченных пар. Общий переход для символов из второго класса запоминается отдельно и называется переходом по неудаче. При поступлении нового входного символа происходит поиск в списке этого символа и соответствующего ему перехода. Если поиск заканчивается неудачей, делается переход на процедуру, соответствующую неудаче. Этот метод можно применять, даже если входной символ не является индексом. На рис. 3.2, в показан список переходов для состояния А процессора, изображенного на рис. 3.2, а. Переход по неудаче вызывают входные индексы 2, 3, 5, 7 и концевой маркер, тем самым пять элементов таблицы объединяются в один переход. Среднее время, необходимое для выбора перехода методом списка переходов, зависит от длины списка и относительной частоты, с которой входные символы встречаются в исходной программе. Естественно, более выгодно помещать пары, содержащие наиболее часто встречающиеся входные символы, в начале списка. Тем не менее этот метод всегда работает медленнее, чем метод вектора переходов, а при увеличении длины списка становится еще медленнее. Метод списка требует совсем незначительных затрат памяти, когда список короткий, однако при длинном списке затраты памяти для этого метода больше, чем для метода вектора, так как память отво-
3.5. Идентификация слов: метод автомата 79 дится и для метки процедуры перехода, и для символа, вызывающего этот переход. Таким образом, этим методом лучше всего пользоваться, когда память дорога, а таблица переходов содержит много стандартных переходов, связанных с ошибкой. Разумеется, при моделировании автомата можно пользоваться смешанным методом. В примере на рис. 3.2, а при выборе переходов из состояния В был бы предпочтителен метод вектора переходов, тогда как для состояния С метод списка переходов сэкономил бы много места. Если состояние моделируемого автомата хранится в явном виде, то в этом случае принципы указанных методов векторов и списков по-прежнему применимы, но возможны многочисленные варианты. Одна из возможностей состоит в том, чтобы пользоваться состоянием как индексом для передачи управления одной из уже описанных процедур. Другая заключается в том, чтобы хранить таблицу переходов как единый двумерный массив, индексы которого — это состояния и входные символы. Можно также использовать метод списка для выбора элементов из столбцов, а не из строк таблицы переходов. Мы не будем говорить о преимуществах того или иного метода, разработчику рекомендуется самому выбирать метод в соответствии со своей конкретной задачей и особенностями вычислительной машины. 3.5. Идентификация слов: метод автомата Это первый из шести разделов, в которых пойдет речь об идентификации. Мы начнем с проблемы идентификации слов. Предположим, что заданы конечное множество слов в некотором входном алфавите и некоторое входное слово и нужно установить, какой элемент заданного множества (если такой существует) совпадает с входным словом. В данном разделе мы решаем эту проблему путем построения конечного процессора, который по предъявленному входному слову с концевым маркером устанавливает его идентичность некоторому элементу множества. В последующих разделах рассматриваются другие методы. Наиболее широкое применение автомат, идентифицирующий слова, находит в лексическом блоке компиляторов. Один из способов использования такого автомата в гипотетическом лексическом блоке показан на рис. 3.3. Управляющая программа знает, что некоторая цепочка букв, появившаяся в некотором месте программы, должна быть словом, и подает эти буквы по одной на вход автомата, идентифицирующего слова, для анализа. Когда управляющий автомат обнаруживает, что слово кончилось, он посылает на вход идентифицирующего автомата концевой маркер. Предположим, что буквы уже
80 Гл. 3. Реализация конечных автоматов §■ 6 о 8- 1 Й- Упрадляющий автомат > • 1 Идентифицирующий автомат Рис. 3.3. Возможная организация лексического блока. переведены транслитератором в символьные лексемы типа БУКВА с соответствующими значениями. Под действием входного символа БУКВА управляющий автомат подает значение этой буквы на вход идентифицирующего автомата и сам совершает определенный переход. Если идентифицирующий автомат использовать таким образом, то язык должен быть таким, чтобы управляющий автомат узнавал, какие входные символы, следуя друг за другом, образуют слово. Некоторые проблемы идентификации не так просты, как эта. В разд. 3.10 мы обсудим применение техники идентификации слов к некоторым задачам, решение которых менее очевидно. Автомат, идентифицирующий некоторое множество слов, можно построить путем введения в него состояния для каждой подцепочки, которая может быть префиксом какого-либо слова из этого множества. Множество префиксов включает в себя пустую цепочку, которая служит префиксом любого слова, а также сами заданные слова, так как каждое слово является своим префиксом. Так как эту процедуру легче проделать, чем описать ее словами, продемонстрируем ее на следующем примере. Рассмотрим задачу построения конечного процессора, имеющего входной алфавит {О, Б, С, К, А} плюс концевой маркер и предназначенного для идентификации множества {ОСА, БОК, БОКА, БАК}. Процессор для этого множества изображен на рис. 3.4. Имена состояний автомата соответствуют цепочкам входных символов, просмотренных к этому моменту. Так, начальное состояние названо е, что соответствует пустой цепочке, которая считается просмотренной до того, как используется первый входной символ, а цепочка БО переведет автомат в состояние БО. Такие элементы таблицы, как «ОСА», означают, что автомат идентифицировал соответствующее слово. Пустые ячейки указывают выходы, означающие, что встретилась ошибка, при этом печатается что-нибудь вроде ВХОДНОЕ СЛОВО НЕ ПРИНАДЛЕЖИТ МНОЖЕСТВУ. Автомат может также переходить в состояние ошибки, которое откладывает сообщение об ошибке до тех пор, пока ошибочное слово не будет просмотрено полностью. При этом переходы не сопровождаются никакими действиями, кроме изменения состояния.
3.5. Идентификация слов: метод автомата 81 Так как эта конкретная проблема идентификации слов включает всего пять букв и десять состояний, процессор можно легко реализовать, пользуясь либо методом вектора переходов, либо методом списка переходов. Однако на практике часто возникают проблемы идентификации слов гораздо большего объема. Входное множество, к примеру, обычно включает все буквы латинского алфавита или все буквы и цифры. Число идентифицируемых слов также может быть Рис. 3.4. большим. Поэтому таблица переходов содержит тысячи элементов, и от реализации с помощью метода вектора переходов приходится отказаться из-за недостатка места. Обратившись к методу списка переходов, мы замечаем, что подавляющее большинство элементов таблицы являются переходами в состояние ошибки. Это характерно для большинства задач идентификации слов, встречающихся на практике, и объясняется тем фактом, что каждое состояние представляет собой префикс слова и поэтому может быть достигнуто только одним путем. Состояние БОК, например, может быть достигнуто только из состояния БО и только для входного символа К. Число переходов из некоторого состояния равно или меньше числа слов множества, имеющих префикс, соответствующий этому состоянию. Так, есть два перехода из состояния БОК, один — в состояние БОКА, а другой — в состоя-
82 Гл. 3. Реализация конечных автоматов ние «БОК», тогда как из состояния БО — только один переход, в состояние БОК, хотя имеется два слова с префиксом БО. Таким образом, метод списка переходов можно эффективно применять для решения довольно больших задач идентификации слов независимо от объема входного алфавита. Так как рассматриваемые процедуры переходов просты, автомат можно реализовать еще проще, чем предлагалось ранее при обсуждении метода списка переходов. В случаях, подобных нашему, где переходы — это не процедуры переходов, а просто изменения состояния, удобно представлять состояние в виде указателя на его список переходов. Так как здесь нет процедур переходов, то нет и необходимости снабжать каждый переход меткой процедуры перехода. Вместо этого, используя списки переходов, свяжем с каждым неошибочным входным символом указатель на список переходов для следующего состояния. Переход состоит в простой замене указателя списка переходов для текущего состояния на полученный по таблице указатель списка переходов для следующего состояния. Элементы списков переходов, соответствующие концевому маркеру, должны содержать индекс или указатель таблицы имен, который указывает, какое слово найдено. Применяя эту технику к нашему примеру, мы получаем списочную структуру, изображенную на рис. 3.5. Начальное состояние представляет собой указатель на список 1. Процессор считывает очередной входной символ и просматривает список для текущего состояния, пока не найдет этот входной символ или не натолкнется на символ ОШИБКА. Например, если первый символ — С, процессор сначала сравнивает его с символами О и Б из списка 1. Затем о ь © © ОШИБКА С © "0ШИ.БКА О А © © ОШИБКА А © ОШИБКА К © ОШИБКА К © ОШИБКА —1 ОШ А -4 ОСА ИБКА © БОК ОШИБКА —i БАК ОШИБКА н БОКА ОШИБКА Рис. 3.5.
3.5. Идентификация слов: метод автомата 83 он встречает символ ОШИБКА и выполняет процедуру по обработке ошибки. Если же первый символ — Б, то процессор идентифицирует его с символом Б из списка I и устанавливает указатель состояния на список 3 (соответствующий состоянию Б). Затем, если следующий входной символ — А, процессор устанавливает указатель состояния на список 6 (список для состояния БА). В случае если следующий входной символ — К, указатель состояния устанавливается на список 9 (список для состояния БАК). Наконец, если очередной входной символ — концевой маркер, процессор идентифицирует его с концевым маркером из списка 9, а затем находит указатель таблицы имен или какой-нибудь другой указатель на слово «БАК» в соответствующей таблице. Заметим, что в этой списочной структуре имеется по одному элементу для каждого перехода, отличного от перехода в состояние ошибки, и по одному переходу на ошибку для каждого состояния, из которого есть хотя бы один переход в состояние ошибки. Предположим теперь, что мы хотим расширить таблицу на рис. 3.5 так, чтобы соответствующий ей автомат распознавал также слово ОКО. Пришлось бы добавить еще два списка переходов — для состояния ОК и для состояния ОКО. Кроме того, список 2 увеличился бы за счет внесения в него перехода из состояния О в состояние ОК. Такое расширение легко произвести при построении, но очень трудно — во время компиляции. Поэтому такой способ реализации применим лишь к распознаванию фиксированного множества слов и не может быть эффективно использован для распознавания расширяющегося множества. Имеется, однако, разновидность реализации метода списков, которая идеально подходит для таких расширений во время компиляции. Чтобы реализовать расширяющиеся списки, заметим, что каждому символу в прежних списках на самом деле соответствуют два списка. Один из них — это список переходов, который используется, если этот символ совпадает с входным символом, а другой — список дополнительных символов, которые надо проверять, если входной символ не совпал с рассматриваемым символом таблицы. Адрес первого списка задается соответствующим указателем, а второй список считается начинающимся со следующей ячейки. Чтобы добиться расширяемости, нужно поменять эти соглашения. Будем считать, что список переходов для следующего состояния начинается в следующей ячейке, а начало списка дополнительных символов, которые надо проверять, будет запоминаться указателем. Набор новых списков для нашего примера показан на рис. 3.6, а. В этой новой реализации каждому символу соответствует либо указатель на следующую проверку, либо пустой элемент. Этот пустой элемент указывает, что больше нет символов, с которыми нужно производить сравнение, и что произошла ошибка. Например, если первый символ С, то процессор сравнивает С с О в списке 1. Так как
Гл. 3. Реализация конечных автоматов С не совпадает с О и символу О соответствует указатель на список 2, процессор затем сравнивает С с Б из списка 2. Символы опять не совпадают, но на сей раз символу таблицы соответствует пустой © © © © 0 с А н Б 0 К н А н А К н © 'ОСА" © © 'БОК" "БОКА " "БАК" © © © © 0 с А н Б Q К н А н А К н К 0 н © © "ОСА" © © "£0K " "БОКА " "ВАК" "ОКО" Рис. 3.6. элемент. Процессор заключает, что нет слова, начинающегося с С, и выполняет процедуру обработки ошибки. В отличие от рис. 3.5 концевому маркеру также может соответствовать указатель списка (как, например, в списке 2) или пустой элемент (как, например, в списке 1). Когда обнаруживается совпадение с концевым маркером, то входное слово идентифицируется со следующим словом спи-
3.6. Идентификация слов: метод индексов 85 ска. Заметим, что эта списочная структура содержит по одному элементу для каждого перехода, отличного от перехода в состояние ошибки, и по одному элементу для каждого слова из распознаваемого множества. Теперь посмотрим, что происходит, когда мы хотим расширить наш автомат, чтобы он мог обрабатывать слово ОКО. Это значит, что после входного символа О допускается не только символ С, но и символ К- Для запоминания того, что должно следовать после К, добавляется новый список, а символ пробела, соответствующий в исходной структуре символу С, заменяется на указатель этого списка. Результат показан на рис. 3.6, б. Заметим, что для того, чтобы расширить список, дополнительную память можно добавить к концу уже использованной области памяти, т. е. это изменение не повлечет за собой перестройку всей исходной структуры. Поэтому такая реализация допускает расширение во время компиляции. 3.6. Идентификация слов: метод индексов В предыдущем разделе мы обсуждали решение проблемы идентификации слов с помощью конечного автомата. Теперь мы рассмотрим ряд методов решения этой проблемы без моделирования автомата. Все эти методы основаны на том, что распознаваемое множество слов хранится в виде некоторого списка или таблицы, а затем, когда на вход поступает какое-то слово, выясняется, есть ли данное слово в списке. В этом разделе мы рассмотрим возможность вычисления по входной цепочке индекса, который обеспечивает прямой доступ к таблице. В качестве примера рассмотрим проблему идентификации переменных языка MINI-BASIC. В данном случае переменная — это либо одна английская буква, либо буква, за которой следует цифра. Всего имеется точно 286 допустимых в MINI-BASIC'e переменных. Так как это число невелико, можно позволить себе завести по одному элементу таблицы для каждой допустимой переменной. Тогда проблема идентификации переменных сводится к преобразованию переменной в индекс, который указывает ее место в таблице. Один из способов заключается в присваивании индексу значения от 1 до 26 для каждой входной буквы английского алфавита. Далее, если следующий входной символ — произвольная цифра d, то к индексу прибавляется число 26*(d+l). Это значит, например, что переменная Z получит номер 26, переменная АО — номер 27, а Z9 — номер 286. Предположим в общем случае, что распознаватель может иметь дело лишь с конечным числом допустимых входных слов. Предположим также, что нам хочется выделить память для каждого допустимого слова. И наконец, предположим, что для каждого
86 Гл. 3. Реализация конечных автоматов из допустимых слов мы можем быстро вычислять однозначно соответствующее ему число, скажем от 1 до М. Это число называется индексом слова. Тогда таблицу можно хранить в последовательных ячейках памяти так, чтобы /-и элемент таблицы отводился для слова» с индексом /. Такую таблицу называют иногда индексированной таблицей, она аналогична одномерному массиву, причем индекс слова служит индексом компоненты массива. Обработка входного слова состоит в вычислении его индекса и нахождении элемента таблицы, соответствующего этому индексу. Этот индексный метод можно применять, если выполнены три условия. Во-первых, число слов не должно быть слишком большим. Это условие исключает, например, множество переменных Фортрана, так как последнее содержит более миллиарда элементов. Во- вторых, индекс должен легко вычисляться. Это условие может исключить даже небольшие множества зарезервированных слов, для которых нет хорошего способа построения индекса. В-третьих, объем множества слов фиксируется при построении, так как метод вычисления индекса неудобно изменять во время компиляции. Если учесть эти три условия, становится очевидным, что рассматриваемым методом можно воспользоваться нечасто. Однако, когда он применим, идентификация осуществляется с минимальными затратами времени. Если цель идентификации — связать слово с элементом таблицы имен, то соответствия между индексами и элементами таблицы имен можно достичь по крайней мере двумя способами: 1. Индекс можно использовать для непосредственного доступа к таблице имен. В этом случае слово с индексом i соответствует i-му элементу таблицы имен. 2. Индекс может указывать на таблицу указателей, содержащую указатели на таблицу имен. Чтобы найти элемент таблицы имен для слова с индексом i, нужно взять указатель из 1-го элемента таблицы указателей. Второй метод позволяет заводить элементы таблицы имен только для тех слов, которые действительно имеются в данной программе. Чтобы достичь этого, таблицу указателей инициализируют нулями, которые означают, что ни один элемент в таблице имен еще не отведен. Когда слово встречается впервые, об этом свидетельствует нуль в соответствующей ячейке таблицы указателей. Для этого слова заводится элемент таблицы имен, а указатель на него помещается в таблицу указателей. Исходя из обычно правдоподобных предположений о том, что элементы таблицы имен занимают больше места, чем элементы таблицы указателей, а слова, используемые в конкретной компилируемой программе, составляют лишь небольшую часть всех допустимых слов, можно заключить, что второй метод требует меньше
3.7. Идентификация слов: метод линейного списка 87 памяти. С другой стороны, он может увеличить время доступа к таблице имен. Этот метод позволяет также заводить элементы таблиц переменного размера. 3.7. Идентификация слов: метод линейного списка Вероятно, наиболее прямой метод идентификации слов заключается в том, чтобы, построив копию входного слова, сравнивать ее с каждым элементом хранимого в памяти списка до тех пор, пока не произойдет совпадение (если оно возможно). Этот метод легко приспособить к случаю расширяющихся множеств, так как все, что нужно сделать в подобных случаях,— это добавить в список еще одно слово. Требование, предъявляемое к памяти, состоит только в том, чтобы хватило места для размещения списка слов. У этого метода есть лишь один недостаток: поиск по длинному списку занимает много времени. Предполагая, что слова из списка появляются с примерно одинаковыми вероятностями, можно ожидать, что для обнаружения заданного слова потребуется просмотреть в среднем половину списка. Точнее, если имеется М слов, то ожидаемое число сопоставлений, необходимых для нахождения одного случайно выбранного слова, равно (УИ + 1)/2. Есть два основных способа хранения списков слов — последовательное хранение и связанное хранение. В первом случае, чтобы найти следующий элемент списка, нужно перейти к следующей ячейке памяти. Свободное место для расширения резервируется в конце списка. Во втором случае, согласно методу связанного списка, каждое слово снабжается указателем на следующее слово. Для расширения списка отводится некоторая свободная область, но она не обязана физически располагаться в конце имеющегося списка. Одна и та же свободная область может одновременно обслуживать несколько связанных списков. 3.8. Идентификация слов: метод упорядоченного списка Время поиска по длинному списку значительно уменьшается, если элементы списка упорядочены — например, лексикографически, т. е. по алфавиту. Пусть у нас имеется список из М слов, расположенных в алфавитном порядке и хранящихся в последовательных ячейках памяти, и пусть мы хотим по некоторому входному слову найти ячейку списка (если таковая существует), в которой содержится это слово. Согласно методу, описанному в предыдущем разделе„ для нахождения слова потребуется в среднем (М + \)/2 сравнений, а для слова, которого нет в списке,— М сравнений. Воспользовав-
88 Гл. 3. Реализация конечных автоматов шись упорядоченностью списка, мы можем уменьшить число сравнений примерно до log M. Это делается следующим образом: 1. Поиск начинается сравнением входного слова со словом, расположенным в середине списка. Если число слов списка четно, то сравнение делается с любым из двух элементов списка, расположенных посредине. Если входное слово и элемент списка совпадают, поиск окончен. В противном случае вследствие упорядоченности списка сравнение показывает, где следует искать входное слово — до или после середины списка. 2. Если, согласно заданному порядку, входное слово предшествует середине списка, то имеются две возможности. Если среднее слово является также и первым словом, что может случиться в списках длины 1 или 2, то мы заключаем, что входного слова в списке нет. Если среднее слово не первое в списке, то поиск должен продолжаться в новом списке, а именно в списке, начинающемся с первого слова и кончающемся словом, расположенным непосредственно перед серединой исходного списка. Длина этого нового списка не превышает половины длины исходного списка, таким образом, сделав всего одно сравнение, мы сократили задачу вдвое. Теперь мы снова применяем шаг 1, осуществляя поиск в этом новом списке. 3. Если, согласно данному порядку, входное слово расположено после середины, то список слов, стоящих после середины, подвергается такому же анализу, что и на шаге 2. К этому подсписку снова применяется шаг 1. В качестве примера на рис. 3.7 показан поиск слова STEP в списке из 18 идентификаторов, который успешно завершается на пятом сравнении. Так как в списке 18 элементов, серединой можно считать девятое или десятое слово. Мы произвольно выбираем девятое слово INTEGER и сравниваем его с входным словом. Поскольку входное слово «больше» среднего слова в смысле алфавитного порядка, мы переходим к анализу нового списка — с девятого по восемнадцатое слово. Теперь мы сравниваем входное слово со словом STRING, серединой нового списка. Так как входное слово «меньше», мы переходим к новому списку, состоящему из слов с десятого по тринадцатое. Таким образом, каждое последующее сравнение уменьшает список в два раза, и поиск продолжается до тех пор, пока после пятого сравнения не обнаруживается слово STEP. Если вместо этого входного слова взять слово STOP, процедура будет работать таким же образом с той лишь разницей, что, когда мы переходим к последнему списку длины I (состоящему из слова STEP), входное слово оказывается «больше» среднего слова этого списка. Так как среднее слово является также и последним, нового списка не будет. Отсюда мы заключаем, что этого входного слова нет в исходном списке.
3.8. Идентификация слов: метод упорядоченного списка 89 Разумеется, некоторые слова могут быть найдены в списке менее чем за пять сравнений. Слово INTEGER, например, будет найдено при первом же сравнении. Вообще на шаге 1 либо обнаруживается совпадение, либо список, в котором надо продолжать поиск, сокращается вдвое. Пусть Весь список Первое сровнение Третье сравнение Четвертое сравнение Пятое сравнение Второе сравнение >- У. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. IS. 16. 17. 18. ARRAY BEGIN BOOLEAN DO ELSE END FOR GOTO INTEGER OWN PROCEDURE REAL STEP STRING THEN UNTIL VALUE WHILE Рис. З.7. исходный список состоит из М слов, тогда можно уменьшать его вдвое не более чем \og2M раз, при этом получится список, состоящий из одного элемента. Понятно, что к этому моменту либо слово будет найдено, либо выяснится, что его нет в списке. Точная граница числа сравнений равна 1 + наибольшее целое число, которое меньше или равно log2^W. Эту процедуру называют методом логарифмического поиска. Ее часто называют также методом бинарного поиска. При реализации этой процедуры требуются некоторые действия для вычисления середины списка. Поэтому на каждом шаге уходит несколько больше времени, чем в случае непосредственного доступа к списку. Однако благодаря тому, что вместо М или М/2 шагов надо выполнить лишь \ogzM шагов, логарифмический метод работает быстрее даже при не очень большом М. Основной недостаток логарифмической процедуры состоит в том, что неудобно расширять список. Дело в том, что новые слова нужно
90 Гл. 3. Реализация конечных автоматов не просто добавлять к концу списка, а вставлять их туда, где они обязаны находиться в соответствии с выбранным порядком. Эту процедуру легко применить к связным спискам, если снабдить каждое слово двумя указателями. Один из них указывает на середину списка слов, которые идут после данного слова, а другой — на середину списка слов, которые предшествуют ему. Такое пред- DO / ч BEGIN Ч ARRAY BOOLEAN END Ч ELSE INTEGER FOR GOTO STRING 4 PROCEDURE 4 OWN REAL STEP UNTIL 4 THEN VALUE WHILE Рис З.8. ставление списка рис. 3.7 показано на рис. 3.8. Заметим, что этот список размещается в памяти в виде дерева. При поиске слова FOR оно сравнивается со словами INTEGER, DO, END и FOR именно в этом порядке. Разместив список в памяти в виде дерева, можно добавлять к нему слова, не нарушая этого размещения. Слово ASK, например*- присоединяется к ARRAY, а слово FOX, присоединяется к GOTO. К сожалению, результирующее время поиска может превышать логарифмическую границу. Для слова FOX, например, потребуется 6 сравнений. Если желательно расширить множество слов, сохранив логарифмическое время поиска, нужно провести реорганизацию всей структуры дерева, так как в середине помещаются другие ело-
3.9. Идентификация слов: метод расстановки 91 ва. Таким образом, такой метод использования связанных упорядоченных списков допускает расширение только в некотором ограниченном смысле. 3.9. Идентификация слов: метод расстановки Идеальным методом решения проблем идентификации больших и расширяющихся множеств слов был бы такой, который позволял легко расширять множества, требовал умеренных затрат памяти и гарантировал обнаружение заданного слова (в среднем) при небольшом числе сравнений. Каждый из методов, описанных в трех предыдущих разделах, не удовлетворяет хотя бы одному из этих требований. Сейчас будет рассмотрен некоторый гибридный метод, обладающий всеми указанными достоинствами. Из множества методов, называемых методами расстановки (хеширования) или перемешанных таблиц, мы рекомендуем именно этот. Идентификация осуществляется за два шага следующим образом: 1. По входному слову вычисляется индекс, как в разд. 3.6, где описан индексный метод. Однако в данном случае многие слова могут иметь один и тот же индекс. Этот индекс затем используется для нахождения указателя списка в таблице указателей списков. Если этот указатель равен нулю, т. е. указывает на нулевой список, то входное слово не принадлежит множеству. В противном случае применяется шаг 2. 2. Найденный элемент таблицы указателей списков указывает на некоторый связанный список слов, а именно тех слов, индекс которых совпадает с вычисленным индексом. Поиск в этом списке происходит до тех пор, пока не произойдет совпадение входного слова с элементом списка или будет достигнут конец списка. В последнем случае множество не содержит входного слова, и это слово можно добавить, просто связав его с последним элементом списка. В литературе метод расстановки (хеширования) иногда описывается в терминах «гроздей» (buckets). Говорят, что каждый индекс указывает на некоторую гроздь, и все слова, имеющие этот индекс, принадлежат одной грозди. Для примера мы взяли слова из списка на рис. 3.7 и изобразили возможную реализацию методом расстановки на рис. 3.9. Для того чтобы вычислить индекс, в этом примере нужно сложить номера (в алфавитном порядке) двух первых букв и взять остаток от деления результата на 7. Например, индекс слова ARRAY получается сложением \(=А) и 18 ( = Я) и последующим делением результата (19) на 7, остаток равен 5. Это значит, что указатель списка, содержащего слово ARRAY (если такой список существует), находится в ячейке 5 таблицы указателей списков.
92 Гл. 3. Реализация конечных автоматов Вообще реализация этой процедуры расстановки зависит от: 1) метода вычисления индекса, 2) объема таблицы указателей списков. Таблица указателей списка FOR GOTO *■ INTEGER * WHILE ¥ STEP * DO * PROCEDURE BEGIN VALUE BOOLEAN STRING ARRAY Рис. З.9. THEN REAL ELSE END UNTIL OWN Поскольку эти факторы могут оказать существенное влияние на скорость и затраты памяти процедуры (а фактически и всего компилятора), мы остановимся на этих вопросах подробнее. Единственная цель вычисления индекса — это уменьшение длины списков, в которых должен производиться поиск. В идеале нам
3.9. Идентификация слов: метод расстановки 93 хотелось бы, чтобы списки были одинаковой длины. К сожалению, разработчик должен выбрать метод вычисления индекса до того, как он узнает, какие слова появятся в списке в ходе данной компиляции. Поэтому единственное, на что можно рассчитывать,— это что новые слова будут попадать в заданные списки с одинаковой вероятностью, и выбор списков не зависит от каких бы то ни было соглашений, использованных при написании исходной программы. Перед разработчиком стоит задача найти метод выбора индекса, удовлетворяющий этим свойствам псевдослучайности. В качестве примера процедуры не такого уж случайного выбора индекса посмотрим, что происходит, если при обработке идентификаторов мы относим каждое слово к одному из 26 списков по первой букве слова. Так как в английском языке гораздо больше слов, которые начинаются с буквы R, чем слов, начинающихся с буквы О, следует ожидать, что ^-список будет содержать гораздо больше элементов, чем О-список. Такая неравномерность еще более усилится, если программирующий на Фортране решит, что в его программе все идентификаторы целых переменных будут начинаться'с буквы /. Хотя с точки зрения разбиения по спискам этот метод получения индекса не является идеальным, в некоторых приложениях им можно пользоваться, так как индекс легко вычислять. Метод индексации по двум буквам, примененный в примере на рис. 3.9, лучше, чем метод индексации по одной букве, так как он ведет к более равномерному заполнению списков. Однако программист может употреблять имена с одинаковым началом (например, ALPHA 1, ALPHA 2, ALPHA 3 и т. д.), что отрицательно влияет на работу компилятора. Можно добиться более быстрого поиска, если вычислять индекс по сумме всех букв. Однако затраты при вычислении индекса могут перекрыть выигрыш во времени поиска. Таким образом, нужно найти компромисс между временем поиска и временем вычисления индекса. На практике процедуры наиболее случайного выбора индекса получают, интерпретируя двоичный код входного слова как одно число или (для длинных слов) как ряд чисел, которые каким-нибудь образом комбинируются (например, складываются), чтобы получилось одно число. Затем это число каким-нибудь способом уменьшают, чтобы получить индекс желаемого размера. Один из способов состоит в использовании остатка от деления числа на некоторую константу. Так как деление на небольшие числа может давать в какой- то степени предсказуемые остатки (например, все они могут быть четными), важно, чтобы делитель, с помощью которого осуществляется рандомизация, не был кратным какому-нибудь маленькому числу. Поэтому в качестве рандомизирующего делителя обычно выбирают простое число. Другой метод получения индекса состоит в возведении числа в квадрат и выделении средних двоичных разрядов. Средние разряды используются для того, чтобы гарантировать
94 Гл. 3. Реализация конечных автоматов участие в вычислении всех разрядов исходного числа. Методы вычисления индекса, подобные указанным и обладающие свойствами псевдослучайности, называются иногда методами функции расстановки. Теперь мы переходим к вопросу о том, насколько большой н\жно делать таблицу указателей списков. Считая, что слова на самом деле помещаются в списки некоторым случайным образом, можно изучить вопрос об объеме таблицы чисто количественными методами. Пусть в таблице указателей есть место для С указателей, и пусть случайным образом введены М слов. Если входное слово случайно выбргп о из М слов, то ожидаемое число сравнений, необходимых для нахождения входного слова, равно 1+(УИ—1)/2С (Чтобы установить, что некоторого нового слова в множестве нет, нужно произвести М/С сравнений.) Если нам известно число слов, с которыми придется иметь дело, эта формула говорит нам, как ^_- * бпш to е с- 10 50 ЮО 200 500 ЮОО 20 1.95 1.19 1,10 1,05 1,02 1.01 Число 100 5,95 1,99 1,50 1.25 1.Ю 1.05 слов, 500 25,95 5,99 3,50 2,25 1,50 1,25 М 1000 50,95 10.99 5,60 3,50 2,00 1,50 5000 250,95 50,99 26,00 13,50 6,00 3,50 Рис. 3.10. Значения функции 1+(УИ—1)/2С. именно число сравнений зависит от объема таблицы. На рис. 3.10 помещены результаты вычисления формулы для некоторых значений М и С. Пусть нас интересует эффективность обработки 500 разных слов, и мы хотим узнать, сколько списков выгоднее завести: 500 или 100. Преимуществом 100 списков является то, что экономится место, необходимое для хранения еще 400 указателей, а недостатком — то, что для идентификации слова необходимо (в среднем) большее число сравнений. На рис. 3.10 можно видеть, что эта разница равна точно 2 сравнениям (т. е. 3.50 минус 1.50). Решение сводится к выбору между 400 дополнительными указателями или 2 дополнительными сравнениями, необходимыми для идентификации каждого слова. Дополнительные сравнения должны, разумеется, оцениваться временем, необходимым для их выполнения на вычислительной ма-
3.10. Обнаружение прификсов 95 шине, на которой будет реализован компилятор. Выигрыш во времени будет иметь место при каждом поиске в ходе компиляции, который осуществляется зачастую по нескольку раз для каждой строки программы. Сэкономленное время вполне может составлять значительную часть всего времени компиляции. Выигрыш будет, конечно, меньше, когда число слов меньше 500, и больше для большего множества слов. Поэтому рекомендуется проделывать указанный анализ для нескольких разных чисел слов, чтобы посмотреть, какой выигрыш во времени можно получить в разных случаях. Вообще говоря, неизбежно напрашивается вывод, что на размере таблицы экономить не нужно. 3.10. Обнаружение префиксов Как отмечалось в разд. 3.5, процедуры идентификации слов удобно применять вместе с управляющим автоматом, который знает, какие символы могут принадлежать распознаваемому слову и когда это слово заканчивается. Другими словами, их можно применять, когда границы предполагаемого слова могут быть установлены управляющим автоматом. Четкие границы слов имеются, например, в естественных языках, где в каждом предложении слова отделяются друг от друга пробелами или другими средствами пунктуации. Четкие границы слов есть также во многих языках программирования, где последовательности букв и цифр отделяются пробелами. Если слова разграничены подобным образом, то идентифицирующий их под- процессор играет в общем процессе ту же роль, что и словарь при чтении текста на естественном языке. Примером проблемы идентификации слов с нечеткими границами является проблема идентификации зарезервированных слов языка MINI-BASIC. Эта проблема возникает в связи с соглашением о том, что пробелы в MINI-BASIC'e ничего не значат. Рассмотрим оператор MINI-BASIC'a 10 FORK = STOP Границы слов здесь можно установить, только зная множество зарезервированных слов: по тому факту, например, что есть зарезервированное слово FOR и нет зарезервированного слова FORK- На самом деле эта же цепочка символов является правильным оператором Фортрана, но с другими границами слов. Если бы пробелы были средствами пунктуации, этот оператор был бы записан так: 10 FOR К = S ТО Р Чтобы справиться с проблемами идентификации такого рода, мы приступим теперь к изучению специфической задачи идентификации, которую будем называть задачей обнаружения префиксов. Пусть
96 Гл. 3. Реализация конечных автоматов € д л до ло д л 0 до ло л л "ДОЛ" М -ч "ДОМ" "ЛОМ" задано конечное множество слов в некотором входном алфавите, и нам дают входную цепочку, которая, возможно, начинается с некоторого слова, принадлежащего этому множеству. Требуется найти элемент цепочки (если таковой существует), на котором это слово кончается, и выйти на процедуру, соответствующую найденному слову. Заданное множество слов можно понимать как множество допустимых префиксов, с которых может начинаться правильная входная цепочка. Мы хотим обнаружить конец префикса и установить, какой это префикс. Чтобы задача была корректной, предположим, что ни одно слово заданного множества не является префиксом другого слова этого множества. Сначала рассмотрим возможность обнаружения префиксов с помощью конечного процессора. Как и для задачи идентификации слов, такой процессор можно построить путем введения состояния для каждой подцепочки, являющейся префиксом некоторого слова множества. Однако в данном случае нет необходимости вводить состояния, соответствующие самим словам. Лучше продемонстрировать этот способ на примере. Рассмотрим задачу обнаружения префиксов из множества {ДОЛ, ДОМ, ЛОМ} в цепочках, образованных буквами алфавита {Д, Л, М, О}. Соответствующий конечный процессор, обнаруживающий префиксы, изображен на рис. 3.11, а. В нем имеется три вида переходов: переходы в новые состояния, выходы для слов, принад- е д л DP ЛО дол ЛОМ ЛОМ д д О До ЛО Л Л ДОЛ м ч дом ЛОМ ■дол- "ДОМ" "ЛОМ" Рис. 3.11. (а) Процессор, обнаруживающий префиксы; (б) Процессор, идентифицирующий слова.
3.10. Обнаружение префиксов 97 лежащих заданному множеству, такие, как «ДОМ», и выходы на процедуру обработки ошибок, обозначенные в таблице пробелами. Если автомат, обнаруживающий префиксы, управляется некоторым другим процессором, то он не обязан обрабатывать концевой маркер, и этот столбец можно выкинуть из таблицы. Для сопоставления задач обнаружения префиксов и идентификации слов, на рис. 3.11, б показан соответствующий процессор, идентифицирующий слова. При реализации автомата, обнаруживающего префиксы, остается в силе почти все, о чем говорилось в разд. 3.5 по поводу реализации автомата, идентифицирующего слова. Различие между этими двумя задачами становится более очевидным, если рассмотреть решение задачи обнаружения префиксов методом списка. Основная идея этого метода заключается в том, чтобы, прочитав вход, строить потенциальное слово и затем осуществлять его поиск в списке, чтобы узнать, есть ли оно в списке идентифицируемых слов. В случае префиксов это невозможно, так как конец префикса нельзя обнаружить, не зная, какой именно это префикс. Поэтому необходима некоторая модификация метода, и очевидное изменение состоит в том, чтобы производить поиск после каждого входного символа. В этом случае каждый входной сим' вол рассматривается как возможный конец префикса, и как только это подтверждается, префикс обнаружен. Чтобы не просматривать длинный список снова и снова, можно завести отдельные списки префиксов каждой возможной длины. Так, получив первый входной символ, компилятор осуществляет поиск в списке префиксов длины один. Когда получен второй символ, компилятор просматривает список префиксов длины два, и т. д. В этом случае нужно будет выполнить не более одного сравнения с каждым из возможных префиксов. Как и раньше, для ускорения поиска можно также применять расстановку и упорядочение. При попытке применения метода индексов для решения проблемы обнаружения префиксов возникают трудности такого же характера, Трудно решить, когда нужно вычислять и использовать индекс. Можно вычислять индекс после каждого входного символа, однако трудно представить, чтобы на практике встретилось множество цепочек, для которого такая процедура была бы оправдана. Мы убедились в том, что задача обнаружения префиксов по существу является разновидностью проблемы идентификации слов, для которой начало слова известно, но конец можно определить лишь путем сравнения его с отдельными словами из множества префиксов. Этой проблемы и ее решения достаточно для описания лексического блока компилятора языка MINI-BASIC. Однако в других языках встречаются проблемы идентификации слов с еще менее четкими границами. 4 Ф Льюис и др.
98 Гл. 3. Реализация конечных автоматов 3.11. Замечания по литературе Многие методы, приведенные в этой главе, относятся к программистскому «фольклору» и использовались еще до их появления в литературе. Хорошее изложение способов расстановки, применявшихся до 1968 года, дано в работе Морриса [1968]. Метод связывания в цепочки для преодоления коллизий, возникающих при использовании расстановки, дается в работе Джонсона [19611. Обзор методов решения проблемы идентификации слов, главным образом для нерасширяющихся множеств, дан в работе Прайса [1971]. Решение проблемы идентификации методами, аналогичными методу списка переходов, описано у Фрэдкина [1960], Скидмора и Вайнберга [19631, а также у Сассенгута [1963]. Метод бинарного поиска для расширяющегося множества слов обсуждается в работах Хиббарда [1962], Фостера [1965], Мартина и Несса [1972]. Очень глубоко методы поиска обсуждаются Кнутом [1973]. Смотрите также работы Северанса [1974) и Нивергельта [1974]. Упражнения 1. Опишите, как бы вы реализовали транслитерацию, используя следующие языки: а) Фортран, б) АПЛ, в) язык ассемблера машины, которая вам больше нравится. 2. Постройте списки переходов для автомата рис. 2.19. 3. Опишите, как бы вы реализовали метод вектора переходов, используя следующие языки: а) Фортран, б) Алгол 60, в) ПЛ/1, г) АПЛ, д) язык ассемблера машины, которая вам больше нравится. 4. Можно строить векторы и списки переходов не для состояний, а для входных символов. Найдите такой автомат, чтобы затраты памяти при его реализации были различными в зависимости от того, для состояний или для входных символов построены списки переходов. {Указание. Переход по Неудаче для списка не обязан быть переходом в состояние ошибки). .5. а) Измените автомат на рис. 3.5 и рис. 3.6, а так, чтобы распознаваемое им множество пключало еще слова КАССА, БО и ОСАКА, б) Добавьте к этим словам слово е. 6. Почему способ реализации, показанный на рис. 3.6, применим лишь к задачам идентификации слов, а не к произвольному конечному автомату? .7. Оцените объем памяти, затрачиваемой при распознавании зарезервированных слов Алгола 60 методами: а) списка переходов, б) линейного списка, в) вектора переходов. •8. Нужно распознать следующее множество: МАССИВ, МАТРОС, ГЦ, П2, ПЗ, ДОМ, СОН, ДОМАШНИЙ.
Упражнения 99 Нарисуйте в виде диаграммы структуру распознавателя этого множества, построенного а) методом списка переходов, б) методом списка переходов для расширяющихся множеств, в) методом упорядоченного списка. • 9. Расширение языка BASIC включает следующие типы переменных: Л) простые переменные — одна буква или буква, за которой следует цифра; 2) переменные с индексами — буква, за которой следуют левая скобка, выражение и правая скобка; 3) переменные для строк — одна буква, за которой следует знак $. Заметьте, что одна и та же буква может использоваться r именах переменных всех трех типов (например, А, А1, AS и А(1)). Составьте схему индексации, которая позволяла бы по символам имени г временной строить индекс, однозначно соответствующий каждой допустимой переменной. 10. Видоизмените рис. 3.9 так, чтобы можно было осуществлять логарифмический поиск в каждом списке. П. Для каждого из следующих методов опишите, что нужно сделать, чтобы удалить некоторое слово из распознаваемого множества слов: а) метод списка переходов, б) метод списка переходов для расширяющихся множеств, в) метод индексов, г) метод линейного списка, д) метод упорядоченного списка. е) метод расстановки. 12. Рассмотрим схему расстановки дли дешификаторов, поступающих в следующем порядке: АБВГ, АБГВ, ГВБА, ЕЛЬ, СОН, ДЕЛО, ДУБ, КОНЬ, ОКНО, ЛОЖЬ, ГРОМ, 0\-а. Мы хотим применить функцию расстановки R mod 13 где R — число, шестнадцатеричной записью которого янляется код ASCII данного слева1'. Нарисуйте таблицу расстановки после введения всех двенадцати слов. 13. Постройте эффективную схему идентификации для зарезервированных слов ягыка Кобол. 14. В некоторой гипотетической автоматизированной системе управления таблица имен содержит элемент для каждого предприятия. Разработайте процедуру поиска, которая по двух- или трехбуквенному коду, такому, как ЗИЛ или БАМ, находит элемент таблицы имен для соответствующего предприятия. Оцените среднее время поиска для вашей процедуры. 15. .Предположим, что в данном языке допустимы п идентификаторов, но в некоторой конкретной программе используются только к. Предположим, что элемент таблицы имен, отведенный для каждого из k идентификаторов, занимает Ь ячеек памяти. Предположим также, что каждый указатель занимает одну ячейку памяти. Подсчитайте, какой объем памяти потребуется для идентификации с помощью индексированных таблиц, описанных в разд. 3.6, если она осуществляется: а) прямым методом, по которому для каждого индекса в индексированной таблице отводится место соответствующему элементу таблицы имен; б) непрямым методом, в соответствии с которым в индексированной таблице содержится указатель на элемент таблицы имен. 1J См. Джермейн К. Программирование на IBM/360. M.: Мир, 1973,— с. 55.— Прим. ред.
100 Гл. 3. Реализация конечных автоматов Покажите, что при ft/n> 1—1/6 метод (а) требует меньших затрат памяти, чем метод (б). 16. Пусть имеется такая схема расстановки, что как только таблица расстановки заполняется на 80%, мы удваиваем ее объем, применяем к новой таблице новую функцию расстановки и заново расставляем все старые элементы в новой таблице. Покажите, что для программы с т идентификаторами (каждый из которых встречается в программе один раз) среднее число сравнений, производимых в соответствии с этой схемой (включая время, затрачиваемое на расстановку), пропорционально т. 17. Если объем таблицы указателей списков Сив связанные списки включены k разных слов, то при использовании метода расстановки среднее количество слов, находящихся в связанном списке, равно kIC. Используя этот результат, выведите формулу 1+(УИ—1)/2С приведенную в разд. 3.9. 18. Рассмотрим разновидность метода логарифмического поиска, в которой оставшаяся часть списка на каждом этапе делится на три равные части; входное слово сравнивается со словами, стоящими в конце первой и второй трети, и та треть списка, в которой находится входное слово, используется на следующем этапе. Найдите максимальное число сравнений при поиске одного слова, использующем этот метод, и сравните его с бинарным логарифмическим методом, описанным в разд. 3.8.
Лексический блок для языка MINI-BASIC 4.1. Множество лексем В этой главе мы проведем довольно детальное построение лексического блока для языка MINI-BASIC на основе конечного автомата. На первом шаге определяется взаимодействие между лексическим ЛЕКСЕМА г КЛАСС СТРОКА ОПЕРАНД АРИФМЕТ. ОПЕРАЦИЯ ОТНОШЕНИЕ КОНЕЦ ЦИКЛА ПРИСВОИТЬ для ПЕРЕХОД НА ПОДПР ЛЕВАЯ СКОБКА ПРАВАЯ' СКОБКА ЕСЛИ ВОЗВРАТ КОНЕЦ до ШАГ КОММЕНТАРИЙ ОШИБКА КОНЦМАРКЕР ЗНАЧЕНИЕ Указатель на таблицу имен Указатель на таблицу имен Номер операции Номер отношения Указатель на таблицу имен Указатель на таблицу имен Указатель на таблицу имен Указатель на таблицу имен Указатель на таблицу имен Нет Нет Нет Нет Нет Нет Нет Нет Нет Нет Рис. 4.1. 120 X, РЗ, 10ЕЗ, 2.6, 2 + , *, /Л,- = ,>=,<> NEXT J LET X =, LET A7 - FOR I =, FOR N9 = GOTO 17 GOSUB 130 ( ) IF RETURN ENB TO STEP REM ЭТО КОММЕНТАРИЙ 10.6.3, LOT X = конец файла и синтаксическим блоками. Множество лексем, обеспечивающих это взаимодействие, изображено на рис. 4.1. Таблица содержит и класс лексемы, и соответствующее значение. Справа от каждого элемента таблицы приведены примеры цепочек символов, образую-
102 Гл. 4. Лексический блок для языка MINI-BASIC щих данную лексему. Классам лексем даны значимые имена, но внутри компилятора они будут представлены числами. Мы предполагаем, что имеется некоторая процедура СОЗДАТЬ ЛЕКСЕМУ, с помощью которой осуществляется взаимодействие с синтаксическим блоком. По отношению к лексическому блоку СОЗДАТЬ ЛЕКСЕМУ может быть процедурой записи лексемы в промежуточный файл или процедурой, которая сообщает синтаксическому блоку о готовности очередного выхода. Так как мы планируем однопроходную компиляцию языка MINI-BASIC, здесь используется только последняя интерпретация, хотя общее построение годится как для одного, так и для нескольких проходов компилятора. Мы предполагаем, что, когда вызывается процедура СОЗДАТЬ ЛЕКСЕМУ, переменной РЕГИСТР КЛАССА уже присвоено некоторое число, представляющее класс создаваемой лексемы, а значение этой лексемы находится в некоторой переменной, зависящей от класса лексемы. Чтобы задать эти переменные и объяснить выбор лексем, обсудим каждую лексему в отдельности. Лексема СТРОКА служит для указания номера строки, стоящего перед оператором. Ее значение — указатель на элемент таблицы имен для этого номера строки. Этот указатель будет засылаться в переменную РЕГИСТР УКАЗАТЕЛЯ ". Лексема ОПЕРАНД служит для указания вхождения константы или переменной в выражение. Она не используется для тех вхождений переменных, которые следуют непосредственно после слов NEXT, FOR и LET. Эти исключения рассматриваются отдельно в описании лексем КОНЕЦ ЦИКЛА, ДЛЯ, ПРИСВОИТЬ. Значением лексемы ОПЕРАНД является указатель на элемент таблицы имен, содержащий переменную или константу. Этот указатель будет храниться в РЕГИСТРЕ УКАЗАТЕЛЯ. Лексема АРИФМЕТ ОПЕРАЦИЯ 2) указывает вхождения символов + , —, *, / и |. Эти знаки операций будут пронумерованы числами от 1 до 5 соответственно. Номер операции служит значением лексемы и будет находиться в переменной РЕГИСТР ЗНАЧЕНИЯ. На самом деле этот номер будет засылаться туда непосредственно процедурой транслитерации. Лексема ОТНОШЕНИЕ указывает вхождения знаков отношений = , >, <, ">—-, <.=- и < >, которым присваиваются номера 5) Выражения «заслать в переменную Хъ, «хранить в переменной X» и т. п. надо понимать как сокращения более правильных, но более длинных выражений «заслать в ячейку, соответствующую переменной X», и т. д.— Прим. ред. 2) Словом «операция» (в оригинале operator) здесь для краткости назван собственно знак операции. Так же употребляется ниже слово «отношение» (в оригинале relational operator). В дальнейшем тот или иной смысл этих слов будет ясен из контекста,— Прим. ред.
4.1. Множество лексем 103 от 1 до 6 соответственно. Номер данного отношения будет засылаться в переменную РЕГИСТР ОТНОШЕНИЯ. Лексема КОНЕЦ ЦИКЛА представляет слово NEXT и идущую за ним переменную. Ее значение, указатель на элемент таблицы имен, соответствующий этой переменной, засылается в РЕГИСТР УКАЗАТЕЛЯ так же, как значение лексемы ОПЕРАНД. Можно было бы определить взаимодействие так, чтобы последовательность символов NEXT КЗ представлялась двумя лексемами — для NEXT и для КЗ. Однако мы решили представлять эту информацию одной лексемой. Такое решение принято для того, чтобы уменьшить количество вызовов процедуры СОЗДАТЬ ЛЕКСЕМУ, а также для того, чтобы читатель лучше усвоил тот факт, что лексемы, используемые внутри компилятора, не совпадают с лексемами, которые фигурируют в руководстве по этому языку. На самом деле в лексическом блоке мы производим мало синтаксических действий, чтобы добиться большей эффективности. Принимая решения такого рода, мы полагаемся на понимание возможностей конечных автоматов, а не на эвристическое истолкование слов «лексический» и «синтаксический». Лексема ПРИСВОИТЬ представляет слово LET, за которым следуют переменная и знак равенства. Ее значение — указатель на элемент таблицы имен, соответствующий этой переменной,— засылается в РЕГИСТР УКАЗАТЕЛЯ. Мы снова используем лексический блок для сжатия информации, содержащейся в исходном операторе. Для представления того, что в руководстве по языку выражалось бы тремя словами, здесь используется одна лексема. Лексема ДЛЯ представляет слово FOR, за которым следуют переменная и знак равенства. Ее значение — указатель на элемент таблицы имен, соответствующий этой переменной,— засылается в РЕГИСТР УКАЗАТЕЛЯ. Эта лексема —вариант лексемы ПРИСВОИТЬ. Лексема ПЕРЕХОД НА представляет слово GOTO, за которым следует номер строки. Мы вновь объединяем два понятия в одну лексему. Значение лексемы — указатель на элемент таблицы имен, содержащий этот номер строки,— помещается в РЕГИСТР УКАЗАТЕЛЯ. Лексема ПЕРЕХОД НА ПОДПР представляет слово GOSUB, за которым следует номер строки. Как и для лексем СТРОКА и ПЕРЕХОД НА, ее значением служит указатель на элемент таблицы имен, содержащий этот номер строки. Значение лексемы засылается в РЕГИСТР УКАЗАТЕЛЯ. Лексемы ЛЕВАЯ СКОБКА и ПРАВАЯ СКОБКА представляют левую и правую скобки соответственно. У них нет значений. Лексемы ЕСЛИ, ВОЗВРАТ, КОНЕЦ, ДО и ШАГ представляют соответствующие зарезервированные слова IF, RETURN, END, ТО и STEP. У этих лексем нет значений.
104 Гл. 4. Лексический блок для языка MINI-BASIC Лексема КОММЕНТАРИЙ представляет слово REM и все остальные символы данной строки 1]. У нее нет значения. С помощью этой лексемы синтаксический блок обнаруживает комментарии, вставленные в неподходящем месте, как, например, в операторе 13 IF Al REM ЭТО НЕЛЕПО Лексический блок порождает лексему КОММЕНТАРИЙ, чтобы синтаксический блок мог проверить правильность расположения комментария. Лексема ОШИБКА используется в лексическом блоке для передачи в синтаксический блок сообщения о том, что обнаружена ошибка. Это освобождает синтаксический блок от необходимости выдавать еще одно сообщение об ошибке. Эта лексема будет порождаться каждый раз, когда лексический блок окажется не в состоянии разбить исходный оператор на значимую последовательность лексем. Такая ситуация может возникнуть, когда неправильно написаны зарезервированные слова (например, RETARN), когда неверно сформированы константы (например, 3.6Е.З) или когда перемешиваются между собой переменные и константы (например, АЗ.Е2). У лексемы ОШИБКА значения нет. Лексема КОНЦ МАРКЕР выдается, когда встречается конец файла. У нее нет значения. Информация, необходимая для процедуры СОЗДАТЬ ЛЕКСЕМУ, представлена на рис. 4.2, где даны переменные, о которых шла речь при обсуждении множества лексем, и описано их использование. РЕГИСТР' КЛАССА —Для номера класса лексемы РЕГИСТР УКАЗАТЕЛЯ —Для указателя на элемент таблицы имен РЕГИСТР ЗНАЧЕНИЯ —Для номера арифметической операции РЕГИСТР ОТНОШЕНИЯ —Для номера отношения Рис. 4.2. Информация, используемая процедурой СОЗДАТЬ ЛЕКСЕМУ. 4.2. Проблемы идентификации Установив взаимодействие между лексическим и синтаксическим блоками, мы займемся теперь проблемами построения собственно лексического блока. Начнем построение с описания схемы решения следующих четырех проблем идентификации: 1) обнаружение зарезервированных слов, 2) идентификация переменных, 3) идентификация номеров строк, 4) идентификация знаков отношений. ') Далее в примерах программ комментарии пишутся по-русски, что не противоречит определению языка (см. разд. А.5), так как компилятор игнорирует текст комментария,— Прим, ред, •-...■■
4.2. Проблемы идентификации 105 Мы обсудим все эти задачи по очереди. Обнаружение зарезервированных слов — это по сути задача обнаружения префиксов, обсуждавшаяся в разд. 3.10. Конечное множество слов таково: {END, FOR, GOSUB, GOTO, IF, LET, NEXT, REM, RETURN-, STEP, TO}. Мы можем обнаружить, где А В С D Е F G Н J К L М N О Р Q R S Т и V W X Y Z Начальный © © © rtTi lil) © © © © © вектор © © © © © © © © © © © © © © © © © © © © © © © © © Буква N D О R О - Т О S и В F Е Переход file/ A2q aic/ F16 Sid S1c/ f1a S1c/ Sic/ F16 /»2r Sic/ кпьтериатЬа © T E X T E T и R N M T E P О F\a B\d S1c/ Cla Sic/ S1c/ Sic/ S1c/ ,42* G\a B\d file/ /»2f V»2u Таблица обнаружения Рис. 4.3. Обнаружение зарезервированных слов.
106 Гл. 4. Лексический блок для языка MINI-BASIC эти слова начинаются, но не можем обнаружить, где они кончаются, если не рассматривать все заданное множество. Выбранное нами решение основывается на моделировании соответствующего автомата, обнаруживающего префиксы. В основных чертах схема, изображена на рис. 4.3. Для переходов, происходящих под действием первой буквы, используется вектор переходов, а для последующих переходов используются списки переходов (подобно тому, как на рис. 3.6). Когда начинается зарезервированное слово, его первая буква служит для указания на элемент начального вектора. Это действие соответствует первому переходу автомата, обнаруживающего префиксы. Каждый элемент вектора содержит указатель на таблицу обнаружения. В таблице обнаружения содержатся списки переходов для обработки дальнейших букв. Например, по начальной букве G из начального вектора выбирается указатель 5, по которому в таблице обнаружения разыскивается информация для обнаружения слов GOTO и GOSUB. Для букв, с которых зарезервированные слова начинаться не могут (таких, как А), начальный вектор содержит нулевые элементы. Если по первой букве слова выбирается нулевой элемент вектора, происходит вызов соответствующей процедуры обработки ошибок. Каждый элемент таблицы обнаружения состоит из трех частей. Первая часть содержит представление буквы, вторая — метку процедуры перехода, а третья — указатель или нуль, означающий отсутствие указателя. Очередная входная буква сравнивается с буквой из текущего элемента таблицы. Если они совпадают, управление передается процедуре перехода, метка которой содержится в этом элементе. Если они не совпадают, то по указателю, содержащемуся в данном элементе, находится новый элемент таблицы, для которого повторяется весь процесс сравнения. Элементы таблицы, связанные друг с другом таким образом, соответствуют списку переходов автомата, обнаруживающего префиксы, который решает эту же задачу. Процедуры переходов, метки которых встречаются в таблице, описываются с помощью основных процедур лексического анализа, перечисленных ниже. Процедура Bid увеличивает указатель на текущий элемент таблицы так, что он будет указывать на следующий элемент. Так, если после G следует О, то под действием О указатель изменится с 5 на 6. 6 — это первый элемент списка переходов для состояния, соответствующего префиксу GO. Другие процедуры переходов, перечисленные в таблице, соответствуют выходам из процедуры обнаружения префиксов. Каждый из этих выходов представляется меткой процедуры перехода, соответствующей конкретному зарезервированному слову, которое удалось обнаружить Система выбора имен для этих меток то же, что и в основном лексическом анализаторе, и описывается ниже. Идентификация переменных будет осуществляться методом ин-
4.2. Проблемы идентификации 107 дексов, задающих их места в таблице имен. Предположим, что первые 286 ячеек таблицы-отведены для 286 возможных переменных. Остальная часть таблицы имен отводится для констант, меток и т. д. Как только прочитана буква, с которой начинается переменная, соответствующее число в интервале от 1 до 26 прибавляется к базовому адресу таблицы имен и результат засылается в РЕГИСТР УКАЗАТЕЛЯ. Если за буквой следует цифра й, то к РЕГИСТРУ УКАЗАТЕЛЯ прибавляется число 26*(d+l). Этот метод индексирования переменных MINI-BASIC'a обсуждался в разд. 3.6. Значение 1 2 3 4 5 6 Отношение = < > <= >= о а 1W РЕГИСТР 2 (О ОТНОШЕНИЯ 3(» РЕГИСТР ЗНАЧЕНИЯ 1 (=) 2 К) 3 (» 0 4К=) 5(>=) 0 0 0 0 6 К» 0 Рис. 4.4. <а) Значения лексемы ОТНОШЕНИЕ; (б) Таблица отношений. Идентификация номеров строк достигается построением числового значения номера строки при обработке автоматом цифр этого номера. Вспомним, что значением лексемы СТРОКА является указатель на элемент таблицы имен для данного номера строки. Значит, каждому появлению данного номера строки нужно поставить в соответствие один и тот же элемент таблицы имен. Это будет сделано путем поиска в списке элементов таблицы имен, соответствующих номерам строк. При поиске в списке мы используем метод расстановки.
108 Гл. 4. Лексический блок для языка MINI-BASIC Функцией расстановки будет остаток от деления числового значения номера строки на простое число, скажем Р. Число Р — параметр построения, его можно легко изменить в зависимости от таких факторов, как объем доступной компилятору памяти и ожидаемый объем исходной программы, поступающей на вход компилятора. Первоначально мы выбираем Р равным 101. Идентификация знаков отношения — это задача идентификации элементов множества {=, <С, >, <=, >=, < >}. Наш процесс идентификации будет основываться на методе индексов, в котором используется тот факт, что каждое слово состоит либо из одного, либо из двух символов. Когда встречается первый символ, в РЕГИСТР ОТНОШЕНИЯ загружается соответствующее число от 1 до 3. Если встречается еще и второй символ, соответствующее ему число помещается в РЕГИСТР ЗНАЧЕНИЯ. Затем номер соответствующего двухсимвольного знака отношения ищется в таблице, называемой таблицей отношений. Этот метод детализирован на рис. 4.4. На рис. 4.4, а показана выбранная нами нумерация знаков отношения; на рис. 4.4, б изображена сама таблица отношений. Нулевые элементы соответствуют ситуациям, подобным = =, когда комбинация символов ошибочна. Заметим, что для того, чтобы придать недозволенным сочетаниям =>, =< и X их естественный смысл, достаточно изменить лишь несколько элементов таблицы. В конкретных ситуациях некоторые расширения языка часто реализуются тривиальным образом. 4.3. Транслитератор При определении транслитератора мы постараемся помнить об экономии, стремясь получить небольшое множество символьных (или литерных) лексем. Спецификация транслитератора приведена на рис. 4.5. Взаимодействие транслитератора с операционной системой вычислительной машины и с остальной частью лексического блока не описывается здесь детально, но оно подразумевается, когда мы говорим «для очередного входного символа сделать переход из состояния Л». Предположим, что транслитератор будет помещать значения символьных лексем в переменную РЕГИСТР ЗНАЧЕНИЯ. Чтобы выбрать множество символьных лексем, обсудим каждую из них в отдельности. Символьная лексема БУКВА используется для представления всех букв, а ее значение указывает конкретную букву. Буквы объединены в один класс, поскольку все они, за единственным исключением, более или менее взаимозаменяемы. Единственное исключение составляет буква Е, которая может использоваться для записи порядка константы. Таким образом, встретив символы 10Е, мы ожидаем, что после Е будет следовать число, задающее
N СЛ <£ X со > СП -< Ч (Г X ш > 14} <Л X СП 1С 7s CD > S Щ 1С А Ш > со < щ 1С х ш > гч) С 43 (С х со > 1ч> ч 13 и; тс го > 1ч> О СО СП 1С т: со > (О 33 m (С 7ч Ш > 00 D m (Г т; го > ~j -а СП (С X го > СП О 03 1С х ш J> «л Z гл и* 7ч го > А 2 m и: тс го > со г- m 1С т; го > 14} 7? СП (С 7ч го > - <_ m ir 7ч ГО > О - OJ (Г 7ч го > (О X СП 1Г X го > со о m 1С X го J> >4 -п m tt. 7ч ГО > СП m гп 1С 7ч го 3> «л о m иг 7ч ГО > ■и о m (Г 7ч ГО J> СО OS m IT 7ч ГО > 14} > сп ее т; ш > - 1 Ci _ «о о> I 2 $ лек семы X кон 1 ■А- > 1' ® кон- п ч РОК 1 ©d) ПРОБ ПРОБ m m ь ^ =1 ■о О сп гп ь 1 • ч о JZ ж > 1 — ПРА го i о КОБ 1 — m Ш -СКОБ 1 V О н X о h со Л О ч X о Ь NJ И О ч О Ь - > -о X ■& 1 о =1 ел - > "0 ■ч в о Л * > "0 ■с €• О со 1 > "0 т е о 3 I4J + > -о X ■в 1 О (О Я тг -ft Т1 > СО 09 J= f т» > 00 ~J р .т в Т1 > ~J СП J= S -{* Т1 > СП «л с; з: ■В- тт j> ел ■и р i ■в- Т» >' ■и со с S е ■о > со м р; S # Т) > м - с; з: « ■о > о с X 4* и J> о >5 ^ й f^ i§%? I S № •лексемы S
по Гл. 4. Лексический блок для языка MINI-BASIC порядок, а встретив 10S, мы ожидаем, что S — первая буква зарезервированного слова (по всей вероятности, слова STEP). Одно вполне разумное решение заключается в юм, чтобы выделить букву Е в специальный класс символьных лексем, оставив ее значение фавным 5. Однако мы выбрали для буквы Е другую схему проверки, она осуществляется при тех особых переходах автомата, где это важно. Получившийся выигрыш в числе столбцов таблицы переходов не имеет реального значения для нашего довольно маленького автомата, но мы хотим продемонстрировать читателю эту технику. Значения лексем становятся внутренним представлением букв, и именно они па самом деле используются в таблице обнаружения на рис. 4.3. Символьная лексема ЦИФРА используется для представления всех цифр. Им приписываются их естественные значения, так как нам нужно будет производить арифметические действия над этими значениями при обработке констант и номеров строк. Символьная лексема АРИФ-ОПЕР представляет арифметические операции. Значение каждой арифметической операции такое же, как у соответствующей лексемы АРИФМЕТ ОПЕРАЦИЯ. Символьные лексемы ЛЕВ-СКОБ, ПРАВ-СКОБ и ТОЧКА представляют соответственно левую скобку, правую скобку и десятичную точку. Значений у них нет. Символьная лексема ПРОБЕЛ представляет символ пробела, символ вычеркивания RO и символ перевода строки LF. Такая кодировка символа вычеркивания и символа перевода строки требует некоторого объяснения. Мы предполагаем, что исходная программа должна подготавливаться на некотором устройстве типа телетайпа, где конец строки представляется некоторой комбинацией символов вычеркивания и символов возврата каретки CR и перевода строки. Наш принцип работы с такой системой заключается в том, чтобы интерпретировать символы вычеркивания и перевода строки как пробелы, а возврат каретки — как символ конца строки. Читатель, которому эти символы незнакомы, может просто забыть об их существовании, так как они нигде в построении не участвуют. Символьная лексема КОН-СТРОК представляет символ возврата каретки и свидетельствует о том, что строка окончена. Символьная лексема КОН-ФАЙЛ служит для лексического блока сигналом о том, что на входе больше нет символов. Метод получения этой лексемы зависит от конкретных особенностей операционной системы машины и не влияет на построение. Предполагается, что у нас есть средства для генерации этой лексемы в нужный момент, и к этому вопросу мы больше не будем возвращаться. Мы игнорируем возможность существования других символов, допускаемых операционной системой, но не используемых в языке MINI-BASIC. Входная цепочка могла бы, например, включать запятую. Такие символы можно было бы переводить в новую лексему,
4.4. Лексический блок 111 появление которой означает ошибку в любом месте, кроме комментария. Однако для простоты будем считать, что мы имеем дело лишь с символами, приведенными на рис. 4.5. 4.4. Лексический блок Конечный автомат, заключающий в себе основную часть лексического блока, изображен на рис. 4.6. Имена состояний, имеющих одинаковое назначение, начинаются с одной и той же буквы, а сами эти состояния сгруппированы в одном месте таблицы переходов. Рассмотрим эти группы состояний, чтобы читатель мог понять, как работает автомат. Состояния Al, А2 и A3 можно считать управляющими. Они используются для представления таких ситуаций, когда только что прочитана одна лексема и должна начаться другая. Состояние А1 — начальное и используется в начале строки, первой лексемой которой должен быть номер строки. Чтобы убедиться в необходимости двух других управляющих состояний, рассмотрим оператор MINI-BASIC'a " • 30 IF G < G1*(G+1) GOTO 10 Буква G встречается в этом операторе четыре раза. В первых трех случаях (после зарезервированного слова IF, после знака отношения и после левой скобки) буква G является началом переменной MINI-BASIC'a. Последнее вхождение G (после правой скобки) — не начало переменной, а первая буква зарезервированного слова. Состояние А2 используется в тех случаях, когда буква рассматривается как первая буква переменной, а состояние A3, когда буква должна быть первой буквой зарезервированного слова. Какое состояние должно быть использовано, можно определить по предыдущей лексеме. Не будь такого различия между состояниями, лексический блок мог бы пытаться переводить GOTO в последовательность ОПЕРАНД G ОПЕРАНД О ОПЕРАНД Т ОПЕРАНД О. Приводимое нами в конце этого раздела описание процедур перехода организовано так, чтобы процедуры, соответствующие переходам в одно и то же состояние, были расположены рядом. В случае управляющих состояний многие переходы, которые ведут в эти состояния, сопровождаются действиями, необходимыми для завершения предыдущей лексемы. Чтобы понять назначение этих особых процедур, следует обращать внимание на то состояние, из которого происходит тот или иной переход. Состояние В1 предназначено для обнаружения зарезервированных слов. Оно использует переменную РЕГИСТР ОБНАРУЖЕНИЯ для хранения указателя на таблицу обнаружения рис. 4.3
112 Гл. 4. Лексический блок для языка MINI-BASIC 1ГИУФ-Н0Я MOdiO- НОИ U390dli VMhOl домо-flwu йОЮ-8Эи~ тоню йШ-ФШ Vd^Htl vgxfig Начало строки Искать пер., конст., оп., отн., CR,),( Искать зарезерВ. слабо, оп„ отн., cr,). EXITl EXIT1 EXITl 5 5 5 «- гм о ч ч ч со со Ч Ч ■с -с гм гм ч ч 1 1 (0 fQ СМ СМ Ф Ф (В гм »- ■- Ш Q Q (0 £13 ГМ *- О QQ <- ем го Ч Ч Ч Обнаружить зарезербиробаиное слово 00 i QQ Искать переменную Завершить обработку переменной гм н X LU со 5 •- гм оо •4: гм Ч 1 СП ГМ ч со Ч ■о -о ем •- (J QQ — ем Завершить обработку целой части Забершить офаботку десятичной часп После букбы Е После буквы Е и знака Забершить обработку порядка После первой десятичной точки EXIT3 EXIT4 EXIT5 5 5 5 <- rj п ^ ю о Q Q Q Q Q Q гм Q го Л го ч ч ч гм гм гм ч ч ч 11 1 О "S3 (0 Q» гм гм <т гм Ч Ч Q Ч •О пз <5 -О о -О *- гм in in m гм Q Q Q Q Q Q ем ool £ 5 5 оо *- гм со *r in to Q Q Q Q Q Q Искать номер строки Оставшаяся часть номера строки со н X LU 5 *- гм Uj 1JJ Ч гм Ч ч* 1 ч. ГМ Ч гм см 00 г- CM Uj Uj Искать переменную и = Оставшаяся часть переменной Искать = >- гм со п. п. п. 0 0 см ем Ч Ч со п. го СМ п. — ем оо U. U. П. Найти cr г- X ш 5 О 5 о о 5 5 5 5 5 Завершить обработку отношение гм н X LU го 5 5 Q го Ч см Ч «N1 ч см1 Ч о Q О *
4.4. Лексический блок 113 (описанную выше). Когда входом служит буква, следующее состояние определяется переходом ЛИ, при котором из таблицы обнару^ жения выбирается новое состояние. Описание Ml приведено после других процедур переходов. Состояния С1 и С2 предназначены для обнаружения переменных^. Они используются для вычисления значений лексем ОПЕРАНД и КОНЕЦ ЦИКЛА. Состояние С1 обнаруживает букву, а состояние С2 — возможную цифру, которая может следовать за буквой. Во многих случаях определить, что началась переменная, можно только после того, как прочитана ее первая буква. В таких случаях сразу наступает состояние С2 и процедура перехода должна включать все те действия, которые обычно выполняются в состоянии С1. К ним относится загрузка в РЕГИСТР КЛАССА имени обрабатываемой лексемы, так как иначе эта информация будет быстро потеряна. Состояние С1 используется только после появления слова NEXT. Состояния DI, ..., D6 используются для вычисления значений констант MINI-BASIC'a. Эти состояния соответствуют состояниям 1, 23, 4, 5, 6 и 7 процессора, рассмотренного в разд. 2.14. Как и в том разделе, для промежуточных вычислений мы пользуемся переменными РЕГИСТР ЧИСЛА, РЕГИСТР ПОРЯДКА, РЕГИСТР СЧЕТЧИКА и РЕГИСТР ЗНАКА. Предполагается, что существует процедура ВЫЧИСЛИТЬ КОНСТАНТУ, которая получает новый элемент таблицы имен, строит внутреннее представление константы с помощью РЕГИСТРА ЧИСЛА и РЕГИСТРА ПОРЯДКА, запоминает это представление в новом элементе таблицы и засылает указатель на этот элемент в РЕГИСТР УКАЗАТЕЛЯ. Процедуры переходов по существу те же, что и в разд. 2.14. Три процедуры выхода ДА1, ДА2 и ДАЗ из разд. 2.14 используются и здесь, но с той разницей, что они не служат больше процедурами выхода. Они переименованы в.ДАШ, ДА20 и ДАЗО, что указывает на их связь с состояниями DI, ..., £)6, обрабатывающими константы. Они вызываются в тех ситуациях, когда ясно, что константа кончилась и начинается следующая лексема. Например, в операторе MINI-BASIC'a 512 IF 3>(X + 10.3)/2E3 GOTO 10 имеются три константы, и лексический блок не может определить последний символ константы, пока не будет прочитан следующий символ, а именно—>,) или G. Другими словами, 3 может оказаться началом 37; 10.3 может оказаться префиксом 10.31, а 2ЕЗ можно продолжить до 2Е30. В каждом из трех случаев процедуры переходов должны заканчивать обработку константы и начинать обработку следующей лексемы. Таким образом, соответствующие процедуры переходов (т. е. Hlc, АЗе и Blc) сначала вызывают подхо-
114 Гл. 4. Лексический блок для языка MINI-BASIC дящую ДА-процедуру для завершения константы, а затем процедуры переходов начинают действия по обработке следующей лексемы. Состояния £1 и £2 служат для обработки номеров строк. Они используются для вычисления значений лексем СТРОКА, ПЕРЕХОД НА и ПЕРЕХОД НЛ ПОДПР. Состояние £1 обнаруживает первую цифру номера строки, а Е2 обрабатывает остальную часть номера строки. Пока идут цифры, в переменной РЕГИСТР СТРОКИ накапливается соответствующее целое число. Когда номер строки завершен, вызывается процедура ДА1Е, которая находит соответствующий элемент в таблице имен и помещает указатель на него в РЕГИСТР УКАЗАТЕЛЯ. Эта процедура вызывается после того, как встретился символ, следующий за номером строки, т. е. аналогично другим ДА-процедурам. Состояния Fl, F2 и F3 служат для обнаружения переменной,, за которой следует знак равенства, и используются для вычисления значений лексем ПРИСВОИТЬ и ДЛЯ. Состояние F\ находит букву переменной, F2 ищет возможную цифру, a F3 — знак равенства после цифры. Они используют РЕГИСТР УКАЗАТЕЛЯ так же, как состояния С\ и С2. Состояние G\ служит для того, чтобы найти КОН-СТРОК после того, как обнаружено слово REM. В результате выбрасывается комментарий. Переход в G\ может произойти также после обнаружения лексической ошибки, чтобы возобновить нормальную лексическую обработку на следующей строке. Состояние Н\ используется, когда встречается символьная лексема ОТНОШ, и служит для того, чтобы узнать, следует ли далее еще одна лексема ОТНОШ. Идентификация знаков отношения уже обсуждалась выше. В таблице переходов лексического анализатора есть несколько дополнительных элементов, где могут быть использованы процедуры, обрабатывающие ошибки. Примером может служить элемент таблицы, соответствующий ПРАВ-СКОБ и состоянию Л2. Так как состояние Л2 наступает только после обработки знака арифметической операции, отношения, левой скобки или слов END, IF, RETURN, STEP и ТО, мы знаем, что правая скобка не может следовать далее ни в одном правильном операторе. Однако мы решили, что лексический блок сформирует лексему ПРАВ- СКОБ и будет продолжать работу, как будто ничего не случилось. Синтаксический блок, разумеется, обнаружит ошибку и выдаст сообщение. Как правило, лексема создается всякий раз; когда ее границы четко определены, и обнаружение ошибок предоставляется синтаксическому блоку. Мы поступаем так, считая, что синтаксический блок располагает большей информацией о том, что делает пользователь, и может выдавать более осмысленные сообщения об ошибках.
4.4. Лексический блок 115 Есть три процедуры переходов, в которых следующее состояние определяется иначе, чем по информации о том, что автомат находится в данном состоянии. Это процедуры Ml, M2 и МЗ. Следующее состояние для перехода Ml определяется по таблице обнаружения с тем, чтобы после завершения зарезервированного слова можно было выполнять разные действия. Переходы М2 и МЗ определяют следующее состояние в зависимости от того, является ли Е следующей буквой. Оба эти действия обсуждались раньше. Способ определения процедур переходов ориентирован на кодировку переходов с неявным представлением состояний. Переход А\Ь, например, задается тремя операторами, последний из которых «Для очередного входного символа сделать переход из состояния Ah. Второй оператор помечен как А]а, потому что последние операторы, оказывается, задают переход А\а, и две процедуры переходов могут иметь в этом месте общий код. Процедуры, ориентированные на явное представление состояний, могут начинаться с такого оператора, как «Установить состояние /41», а затем передавать управление операторам, общим для переходов в другие состояния. Последним оператором может быть оператор «Для очередного входного символа сделать переход». Спецификация элементов таблицы переходов приводится ниже. Пустые элементы соответствуют процедурам, обрабатывающим ошибки. Эти процедуры детально не описаны. Предполагается лишь, что они вызывают создание лексемы ОШИБКА и переводят автомат в состояние G\ для продолжения лексического анализа Если на входе находится КОН-ФАЙЛ, будем считать, что выходом является процедура обработки ошибки. Alb: Ala: Al: Ale: Aid: Ale: Л 2c: A2g: A2a: A2b: Выполнить процедуру ДА1 D СОЗДАТЬ ЛЕКСЕМУ Для очередного входного символа сделать переход из состояния Л1 Выполнить процедуру ДА20 Перейти на Ala Выполнить процедуру ДАЗЭ Перейти на А 1а Выполнить процедуру ДА IE Перейти на Ala Выполнить процедуру ДАШ СОЗДАТЬ ЛЕКСЕМУ Загрузить АРИФМЕТ ОПЕРАЦИЮ в РЕГИСТР КЛАССА СОЗДАТЬ ЛЕКСЕМУ
116 Гл. 4. Лексический блок для языка MINI-BASIC Л2: Для очередного входного символа сделать переход из состояния Л2 Л 2d: Выполнить процедуру ДА20 Перейти на A2g А2е: Выполнить процедуру ДАЗО Перейти на A2g Л2/: Выполнить процедуру ДАШ Перейти на A2g /42/: Выполнить процедуру ДА1Е A2k: СОЗДАТЬ ЛЕКСЕМУ A2h: Загрузить ЛЕВУЮ СКОБКУ в РЕГИСТР КЛАССА СОЗДАТЬ ЛЕКСЕМУ Для очередного входного символа сделать переход из состояния Л2 Л2/: Выполнить процедуру ДАШ Перейти на A2k /42m: Выполнить процедуру ДА20 Перейти на A2k А2п: Выполнить процедуру ДАЗО Перейти на A2k А2о: Если в РЕГИСТРЕ ЗНАЧЕНИЯ не 1 (т. е. знак операции отличен от =), то перейти на процедуру обработки ошибки Если РЕГИСТР ЗНАЧЕНИЯ содержит 1, то перейти на А2Ь А2р: Используя РЕГИСТР ОТНОШЕНИЯ и РЕГИСТР ЗНАЧЕНИЯ, получить число из таблицы отношений (рис. 4.4, 6) Если это число равно нулю, то перейти на процедуру, обрабатывающую ошибку Заслать полученное число в РЕГИСТР ОТНОШЕНИЯ Перейти на А2Ь A2q: Загрузить КОНЕЦ в РЕГИСТР КЛАССА Перейти на А2Ь А2г. Загрузить ЕСЛИ в РЕГИСТР КЛАССА Перейти на А2Ь A2s: Загрузить ВОЗВРАТ в РЕГИСТР КЛАССА Перейти на А2Ь
4.4. Лексический блок 117 Alt: Загрузить ШАГ в РЕГИСТР КЛАССА Перейти на Alb А2и: Загрузить ДО в РЕГИСТР КЛАССА Перейти на Alb АЪа: Содержимое РЕГИСТРА ЗНАЧЕНИЯ увеличить на 1 и умножить на 26 Сложить результат с содержимым РЕГИСТРА УКАЗАТЕЛЯ СОЗДАТЬ ЛЕКСЕМУ A3: Для очередного входного символа сделать переход из состояния ЛЗ AM: Выполнить процедуру Да ID АЗс: СОЗДАТЬ ЛЕКСЕМУ АЗЬ: Загрузить ПРАВУЮ СКОБКУ в РЕГИСТР КЛАССА СОЗДАТЬ ЛЕКСЕМУ Для очередного входного символа сделать переход из состояния ЛЗ АЗе: Выполнить процедуру ДА2Э Перейти на АЗс ЛЗ/: Выполнить процедуру ДАЗЭ Перейти на АЗс A3g: Выполнить процедуру ДАШ Перейти на АЗс В\с: Выполнить процедуру ДАЗО Bib: СОЗДАТЬ ЛЕКСЕМУ Qla: Пользуясь РЕГИСТРОМ ЗНАЧЕНИЯ как индексом, получить из начального вектора указатель и загрузить его в РЕГИСТР ОБНАРУЖЕНИЯ Если в РЕГИСТРЕ ОБНАРУЖЕНИЯ нуль, перейти на процедуру, обрабатывающую ошибку fll: Для очередного входного символа сделать переход из состояния В\ ВЫ: Увеличить РЕГИСТР ОБНАРУЖЕНИЯ на 1 Для очередного входного символа сделать переход из состояния В\ В\е: Выполнить процедуру ДАШ Перейти на Bib С\а- Загрузить КОНЕЦ ЦИКЛА в РЕГИСТР КЛАССА
118 Гл. 4. Лексический блок для языка MINI-BASIC CI: Для очередного входного символа сделать переход из состояния С1 С2Ь: СОЗДАТЬ ЛЕКСЕМУ £2а: Загрузить ОПЕРАНД в РЕГИСТР КЛАССА С2± Загрузить в РЕГИСТР УКАЗАТЕЛЯ базовый адрес таблицы имен плюс содержимое РЕГИСТРА ЗНАЧЕНИЯ С2: Для очередного входного символа сделать переход из состояния С2 Die: СОЗДАТЬ ЛЕКСЕМУ D\a: Заслать ОПЕРАНД в РЕГИСТР КЛАССА Переслать число из РЕГИСТРА ЗНАЧЕНИЯ в РЕГИСТР ЧИСЛА Для очередного входного символа сделать переход из состояния D\ D\b: Умножить содержимое РЕГИСТРА ЧИСЛА на 10 Прибавить к РЕГИСТРУ ЧИСЛА число из РЕГИСТРА ЗНАЧЕНИЯ D\: Для очередного входного символа сделать переход из состояния D\ D2a: Увеличить РЕГИСТР СЧЕТЧИКА на 1 Умножить на 10 содержимое РЕГИСТРА ЧИСЛА Прибавить к РЕГИСТРУ ЧИСЛА число из РЕГИСТРА ЗНАЧЕНИЯ D2: Для очередного входного символа сделать переход из состояния D2 D2b: Инициализировать РЕГИСТР СЧЕТЧИКА единицей Переслать число из РЕГИСТРА ЗНАЧЕНИЯ в РЕГИСТР ЧИСЛА Для очередного входного символа сделать переход из состояния D2 D2c: Инициализировать РЕГИСТР СЧЕТЧИКА нулем Для очередного входного символа сделать переход из состояния D2 D3a: Инициализировать РЕГИСТР СЧЕТЧИКА нулем D3: Для очередного входного символа сделать переход из состояния D3 D4a: Если РЕГИСТР ЗНАЧЕНИЯ содержит 1 (т. е. знаком операции оказался +), загрузить +1 в РЕГИСТР ЗНАКА
4.4. Лексический блок \\9 Если РЕГИСТР ЗНАЧЕНИЯ содержит 2 (т. е. знаком операции оказался —), загрузить —1 в РЕГИСТР ЗНАКА Если РЕГИСТР ЗНАЧЕНИЯ содержит число больше 2, перейти на процедуру, обрабатывающую ошибку D4: Для очередного входного символа сделать переход из состояния D4 D5a: Загрузить +1 в РЕГИСТР ЗНАКА Dbb: Переслать число из РЕГИСТРА ЗНАЧЕНИЯ в РЕГИСТР ПОРЯДКА D5: Для очередного входного символа сделать переход из состояния D5 Dbc: Умножить содержимое РЕГИСТРА ПОРЯДКА на 10 Прибавить к РЕГИСТРУ ПОРЯДКА число из РЕГИСТРА ЗНАЧЕНИЯ Для очередного входного символа сделать переход, из состояния D5 D6a: СОЗДАТЬ ЛЕКСЕМУ D6: Загрузить ОПЕРАНД в РЕГИСТР КЛАССА Для очередного входного символа сделать переход из состояния D6 Е\а: Загрузить ПЕРЕХОД НА в РЕГИСТР КЛАССА El: Для очередного входного символа сделать переход из состояния Е\ Elb: Загрузить ПЕРЕХОД НА ПОДПР в РЕГИСТР КЛАССА Для очередного входного символа сделать переход из состояния Е\ ЕЧа: Загрузить СТРОКУ в РЕГИСТР КЛАССА Е2Ь: Загрузить в РЕГИСТР СТРОКИ содержимое РЕГИСТРА ЗНАЧЕНИЯ £2: Для очередного входного символа сделать переход из состояния Е2 Е2с: Умножить на 10 содержимое РЕГИСТРА СТРОКИ Прибавить к РЕГИСТРУ СТРОКИ содержимое РЕГИСТРА ЗНАЧЕНИЯ Для очередного входного символа сделать переход из состояния £2
120 Гл. 4. Лексический блок для языка MINI-BASIC Fla: Загрузить ПРИСВОИТЬ в РЕГИСТР КЛАССА F\: Для очередного входного символа сделать переход из состояния F\ Fib: Загрузить ДЛЯ в РЕГИСТР КЛАССА Для очередного входного символа сделать переход из состояния F\ F2a: Загрузить в РЕГИСТР УКАЗАТЕЛЯ базовый адрес таблицы имен плюс содержимое РЕГИСТРА ЗНАЧЕНИЯ F2: Для очередного входного символа сделать переход из состояния F2 F3a: К содержимому РЕГИСТРА ЗНАЧЕНИЯ прибавить 1 и умножить на 26 Прибавить результат к содержимому РЕГИСТРА УКАЗАТЕЛЯ F3: Для очередного входного символа сделать переход из состояния F2 Gla: Загрузить КОММЕНТАРИЙ в РЕГИСТР КЛАССА СОЗДАТЬ ЛЕКСЕМУ 01: Для очередного входного символа сделать переход из состояния G1 Н\с: Выполнить процедуру ДАЮ Hlb: СОЗДАТЬ ЛЕКСЕМУ Hla: Загрузить содержимое РЕГИСТРА ЗНАЧЕНИЯ в РЕГИСТР ОТНОШЕНИЯ Загрузить ОТНОШЕНИЕ в РЕГИСТР КЛАССА HI: Для очередного входного символа сделать переход из состояния Н\ Hid: Выполнить процедуру ДА20 Перейти на Hlb Hie: Выполнить процедуру ДАЗО Перейти на Hlb Hlf: Выполнить процедуру ДАШ Перейти на Hlb Ml: Сравнить значение символа, на которое указывает РЕГИСТР ОБНАРУЖЕНИЯ, с РЕГИСТРОМ ЗНАЧЕНИЯ Если значения равны, перейти к процедуре
4.4. Лексический блок 121 перехода, на которую указывает РЕГИСТР ОБ' НАРУЖЕНИЯ В противном случае заслать несовпадающее значение, на которое указывает РЕГИСТР ОБНАРУЖЕНИЯ, в РЕГИСТР ОБНАРУЖЕНИЯ Если РЕГИСТР ОБНАРУЖЕНИЯ содержит О, перейти на процедуру, обрабатывающую ошибку Перейти на All М2: Если РЕГИСТР ЗНАЧЕНИЯ содержит число, не равное 5 (т. е. буква оказалась не Е), то выполнить процедуру ДАШ и перейти на Bib Если число равно 5, перейти на D3a МЗ: Если РЕГИСТР ЗНАЧЕНИЯ содержит число, не равное 5 (т. е. оказалось, что это не буква Е), то выполнить процедуру ДА20 и перейти на Bib Если число равно 5, перейти на D3 ВЫХОДЗ: Выполнить процедуру ДАШ ВЫХОД2: СОЗДАТЬ ЛЕКСЕМУ ВЫ ХОД 1: Загрузить КОНЦ МАРКЕР в РЕГИСТР КЛАССА СОЗДАТЬ ЛЕКСЕМУ Выйти из лексического блока ВЫХОД4: Выполнить процедуру ДА20 Перейти на ВЫХОД2 ВЫХОД5: Выполнить процедуру ДАЗО Перейти на ВЫХОД2 ВЫХОД6: Выполнить процедуру ДАШ Перейти на ВЫХОД2 Процедуры, связанные с обработкой констант ДАШ: Загрузить 0 в РЕГИСТР ПОРЯДКА ВЫЧИСЛИТЬ КОНСТАНТУ Возврат DA2D: Присвоить РЕГИСТРУ ПОРЯДКА значение РЕ- ГИСТРА СЧЕТЧИКА с минусом ВЫЧИСЛИТЬ КОНСТАНТУ Возврат ДАЗЭ: Если РЕГИСТР ЗНАКА содержит —1, сделать РЕГИСТР ПОРЯДКА отрицательным Вычесть РЕГИСТР СЧЕТЧИКА из РЕГИСТРА ПОРЯДКА ВЫЧИСЛИТЬ КОНСТАНТУ Возврат
122 Гл. 4. Лексический блок для языка MINI-BASIC Процедура для поиска номеров строк ДАШ: (Используется метод расстановки. Индекс расстановки вычисляется путем деления номера строки на константу Р. На начальных стадиях построения мы полагаем Р равным 101. По индексу из таблицы указателей списков (см. разд. 3.9) выбирается указатель на таблицу номеров строк, имеющих этот индекс, организованную в виде связного списка элементов. В этих элементах нас интересуют только две их части: НОМЕР СТРОКИ и СЛЕД ЭЛЕМЕНТ, содержащий указатель на следующий элемент списка. Если в списке, соответствующем индексу номера строки, нет элемента, содержащего номер, который мы ищем (возможно, из-за того, что список пуст), создается новый элемент, он заполняется и помещается в начало этого списка.) Вычислить остаток от деления РЕГИСТРА СТРОКИ на Р Загрузить в РЕГИСТР УКАЗАТЕЛЯ содержимое элемента таблицы указателей списков, выбранного по индексу, равному только что вычисленному остатку. ДА1ЕЦИКЛ: Если в РЕГИСТРЕ УКАЗАТЕЛЯ нуль, то Начало (в списке больше нет элементов) Создать новый табличный элемент Загрузить в РЕГИСТР УКАЗАТЕЛЯ указатель на новый элемент Заслать в НОМЕР СТРОКИ нового элемента содержимое РЕГИСТРА СТРОКИ Заслать в СЛЕД ЭЛЕМЕНТ нового элемента содержимое выбранного элемента таблицы указателей списков Заслать в выбранный элемент таблицы указателей списков указатель на новый элемент таблицы имен Возврат Конец
4.4. Лексический блок 123 Если НОМЕР СТРОКИ элемента списка, на который указывает РЕГИСТР УКАЗАТЕЛЯ, равен содержимому РЕГИСТРА СТРОКИ, то (номер строки найден) выполнить возврат В противном случае (номер строки не найден, и нужно перейти к следующему элементу списка) загрузить в РЕГИСТР УКАЗАТЕЛЯ то, что содержится в СЛЕД ЭЛЕМЕНТЕ элемента списка, на который указывает РЕГИСТР УКАЗАТЕЛЯ, и перейти на ДА1ЕЦИКЛ Построение любого значительного блока программного обеспечения должно включать средства для его проверки и отладки. В связи с этим мы включили в текст следующую «программу», которая вызывает каждый переход, не являющийся переходом в состояние ошибки, и приводит в действие все части лексического блока. Заметим, что она не является правильной программой MINI-BASIC'a, но лексический блок не обнаруживает в ней ошибки. Правильную последовательность лексем, которая должна появиться на выходе при обработке этой программы, можно получить путем моделирования лексического блока вручную (см. упр. 4.9). Если эта последовательность лексем действительно является выходом при некоторой реализации построенного лексического блока, то разработчик может быть в значительной степени уверен в том, что при программировании не сделано ошибок. Если эта последовательность не получается, то обнаружена ошибка. Для отладки разработчик может заготовить таблицу правильных значений различных внутренних переменных лексического блока. В разд. 2.14 мы сделали это для процессора, обрабатывающего константы, но здесь мы этого делать не будем. 1 REM ПРОГРАММА ОТЛАДКИ ЛЕКСИЧЕСКОГО БЛОКА 5 REM А1 + ='( ). 13 LET F 1 = ■? / - (ЬЗ < > Ч)) = * < ( W LET G = - 3 TO .fe.5 + .01 = .1) + .fl( .fe. 50 F OR I = .} E Ь fl STEP . ЭЕ + t> = 1.3E - 1) (1.ЭЕЧ 53 IF 10 Eb +'3E1 (1ELGQTO 1 5 = H I > <0 NEXT ЕЭ <5 + A T В ( С ) 5 ( D <5 GOSUB 1)G0T0 fe. so ( q • 55 ) . Ь RETURN )END >=<=<) Ь0 7 54 =J0+Kl+L5+M3+»K+O5+Pb+Q7+Ra+Sq+T+U+V+H+X+Y+Z
124 Гл. 4. Лексический блок для языка MINI-BASIC Упражнения 1. Определите содержимое каждой переменной лексического блока при обработке каждого символа данного оператора. 010 IF X>Y1+2E3 GOTO 20 2. Составьте сообщения об ошибках для каждого пустого элемента лексического блока. 3. Укажите, какие изменения нужно внести в лексический блок, если таблицу транслитерации на рис. 4.5 изменить так, как показано на рисунке Е Е 5 + + 1 - - 2 = = 1 Укажите изменения, которые нужно внести в лексический блок, если множество выходных лексем изменить так, чтобы различались унарное и бинарное употребление знаков + и —. На рис. 4.1 лексема АРИФМЕТ ОПЕРАЦИЯ будет по-прежнему указывать на бинарные операции +, —, *, /, f (например, 1+1), а новая лексема УНАРНАЯ ОПЕРАЦИЯ будет использована для унарных операций + и — (например, —1). Значения лексем + и — останутся прежними. Напишите на MINI-BASIC'e программу, вызывающую все переходы на рис. 4.6, которые может вызывать правильная программа, написанная на этом языке. Лексический блок допускает все правильные, а также многие неправильные программы на MINI-BASIC'e. Найдите на рис. 4.6 веете переходы, прн замене которых переходами в состояние ошибки новый блок будет тем не менее допускать все правильные программы на MINI-BASIC'e. Лексический блок допускает такой неправильный оператор IF X1=X2 GOSUB 10 Однако этот оператор «не лишен смысла» и мог бы быть включен в потенциальное расширение языка MINI-BASIC. Найдите еще три неправильных, но имеющих смысл оператора. 8. Укажите, как бы изменились процедуры из разд. 4.4, если бы для запоминания состояний использовался явный метод. Какое множество лексем порождает лексический блок для отладочной цепочки, приведенной в конце разд. 4.4? Постройте лексический блок для языка MINI-BASIC при условии, что используются пробелы, т. е. зарезервированные слова, переменные и константы должны разделяться пробелами. 11. Запрограммируйте лексический блок для языка MINI-BASIC на любом языке. 9. 10.
Упражнения 125 12. Постройте лексический блок для автомата, решающего задачу 2.22. Входом для лексического блока служат символы а, б, в, . . ., я. Выходами являются лексемы ОДИН, ДВА ДЕСЯТЬ, ОДИННАДЦАТЬ ДВАДЦАТЬ ДЕВЯНОСТО, СТО, ДВЕСТИ, СТА, СОТ. 13. Постройте лексический блок для языка Снобол. 14. Постройте лексический блок для языка ассемблера, которым вы пользуетесь. 15. Постройте лексический блок для кода Морзе. Входами служат «точка», «тире» и «пауза». Выходами являются лексемы, соответствующие всем буквам, цифрам, а также знакам пунктуации, которые представляются этим кодом.
5 Автоматы с магазинной памятью 5.1. Определение автомата с магазинной памятью Конечный автомат может решать лишь такие вычислительные задачи, которые требуют фиксированного и конечного объема памяти. В компиляторе, однако, возникает много задач, которые не могут быть решены при таком ограничении, и поэтому нам нужна модель более сложного автомата. Рассмотрим, например, задачу обработки скобок в арифметических выражениях. Арифметическое выражение может начинаться с любого количества левых скобок, и компилятор должен проверять, имеется ли в выражении точно такое же число соответствующих правых скобок. Каждый раз самая левая из скобок в выражении будет иметь особую «роль», так как каждая из таких ролей требует разного числа соответствующих правых скобок для завершения выражения. Другими словами, компилятор должен эффективно подсчитывать левые скобки, чтобы сбалансировать их. Разумеется, конечное множество состояний не годится для запоминания числа необходимых правых скобок, так как множество этих чисел бесконечно. Для систематического решения этой проблемы, связанной с выражениями, а также для решения многих других проблем компиляции необходимо использовать в компиляторе модели более мощных автоматов. Чтобы получить более мощный автомат, память конечного автомата расширяется за счет дополнительного механизма хранения информации. Один из методов хранения информации, который оказался весьма полезным в компиляции и просто реализуется,— это использование магазина или стека1). Основная особенность магазинной памяти с точки зрения работы с нею состоит в том, что символы можно помещать в магазин и удалять из него по одному, причем удаляемый символ — это всегда тот, который был помещен в магазин последним. Последовательность символов в магазинной памяти можно сравнить со стопкой тарелок в кафетерии. Служащие кафетерия ставят чистые тарелки на верх стопки, и посетители затем берут их тоже сверху. Таким образом, посетитель всегда берет *) В данной книге и очень часто вообще в программировании эти термины считаются синонимами, хотя в литературе по теории автоматов они различаются, причем стек имеет более широкий смысл. Мы пошли на компромисс, в более «автоматных» разделах употребляя термин магазин, а в более «программистских» переходя на стек.— Прим. ред.
5.1. Определение автомата с магазинной памятью 127 из стопки тарелку, поставленную туда последней. В подобных же терминах можно описать и функционирование магазинной памяти. Когда информация помещается в магазин, мы говорим, что она «вталкивается» в магазин. Когда информация удаляется из магазина, мы говорим, что она «выталкивается» из него 1>. Говорят, что С А В А V а информация, только что поступившая в магазин, находится в его верхушке или наверху. Хотя такое представление о работе с магазином полезно, оно мало что говорит 6 том, как реализовать магазин в компиляторе. Мы изобразили магазин на рис. 5.1, а. На дне магазина находится символ V, а на верху — символ С. Символы расположены в том порядке, в каком они поступали в магазин. Сначала поступил символ V, затем нижнее А, затем В, затем верхнее А и наконец символ С. Если втолкнуть в магазин символ D, то магазин будет выглядеть так, как показано на рис. 5.1, б, где D — верхний символ магазина. Если же, наоборот, вытолкнуть из магазина верхний символ С, то верхним символом окажется А, и магазин будет выглядеть, как показано на рис. 5.1, Ь. В обоих случаях изменениям подвергается только верх магазина, а остальные символы остаются неизменными. Символ у — это специальный символ, который помечает начало или «дно» магазина и называется маркером дна. Он используется только как метка дна и никогда не выталкивается из магазина. Так, если X — верхний символ магазина, как на рис. 5.1, г, то мы знаем, что других символов в магазине нет. В этом ') Эти термины, как, собственно, и магазин, навеяны аналогией с магазином ■автоматического оружия и патронами, которые вталкиваются в него или выталкиваются. Стек надо бы по-русски назвать стопкой, да, наверное, поздно,— Прим. В с А В А 7 6 А В А V в И Рис. 5.1.
128 Гл. 5. Автоматы с магазинной памятью случае говорят, что магазин пуст. Магазин на рис. 5.1, а можно также изобразить в виде цепочки одним из следующих способов: 1. С А В А V 2. К А В А С Представление магазина в первой строке соответствует соглашению о том, что его «верхний символ находится слева», а во второй строке — что «верхний символ справа». Которое из двух соглашений использовано, можно определить по маркеру дна. И) Состояние 1 Входная цепочка Рис. 5.2. С А В А V > Магазин 1 0 0 1 1 1 0 н 1 1 Одной из моделей автомата, в которых используется магазинный принцип организации памяти, является автомат с магазинной памятью (или сокращенно МП-автомат). В нем очень просто комбинируется память конечного автомата и магазинная память. МП-автомат может находиться в одном из конечного числа состояний и имеет магазин, куда он может помещать и откуда может извлекать информацию. Как и в случае конечного автомата, обработка входной цепочки осуществляется за ряд мелких шагов. На каждом шаге действия автомата конфигурация его памяти может измениться за счет перехода в новое состояние, а также вталкивания символа в магазин или выталкивания из него. Однако в отличие от конечного автомата. МП-автомат может обрабатывать один входной символ в течение нескольких шагов. На каждом шаге управляющее устройство автомата решает, пора ли закончить обработку текущего входного символа и получить, если это так, новый входной символ или продолжить обработку текущего символа на следующем шаге. На рис. 5.2 изображена одна из конфигураций, которая может возник-
5.1. Определение автомата с магазинной памятью 129 нуть при обработке некоторым гипотетическим МП-автоматом входной цепочки 100110. Для большей наглядности входная цепочка изображена записанной в ячейках файла или ленты с указателем на входной символ, подвергающийся в данный момент обработке. Каждый шаг процесса обработки задается множеством правил, использующих информацию трех видов: 1) состояние, 2) верхний символ магазина, 3) текущий входной символ. Это множество правил называется управляющим устройством или механизмом управления. На рис. 5.2 информация, поступающая в управляющее устройство, такова: состояние 6, верхний симеол магазина С, текущий входной символ 0. В зависимости от получаемой информации, управляющее устройство выбирает либо выход из процесса (т. е. прекращает обработку), либо переход в новое состояние. Переход состоит из трех операций: над магазином, над состоянием и над входом. Возможные операции таковы: Операции над магазином 1. Втолкнуть в магазин определенный магазинный символ. 2. Вытолкнуть верхний символ магазина. 3. Оставить магазин без изменений. Операция над состоянием 1. Перейти в заданное новое состояние. Операции над входом 1. Перейти к следующему входному символу и сделать его текущим входным символом. 2. Оставить данный входной символ текущим, иначе говоря, держать его до следующего шага. Обработку входной цепочки МП-автомат начинает в некотором выделенном состоянии при определенном содержимом магазина, а текущим входным символом является первый символ входной цепочки. Затем автомат выполняет операции, задаваемые его управляющим устройством. Если происходит выход из процесса, обработка прекращается. Если происходит переход, то он дает новый верхний магазинный символ, новый текущий символ, автомат переходит в новое состояние и управляющее устройство определяет новое действие, которое нужно произвести. ' Ф. Льюнс и др.
130 Гл. 5. Автоматы с магазинной памятью Чтобы управляющие правила имели смысл, автомат не должен требовать следующего входного символа, если текущим символом является концевой маркер, и не должен выталкивать символ из магазина, если это маркер дна. Поскольку маркер дна может находиться исключительно на дне магазина, автомат не должен также вталкивать его в магазин. Теперь подытожим, как задается МП-автомат1). Он определяется следующими пятью объектами: 1) конечным множеством входных символов, в которое входит и концевой маркер; 2) конечным множеством магазинных символов, включающим маркер дна; 3) конечным множеством состояний, включающим начальное состояние; 4) управляющим устройством, которое каждой комбинации входного символа, магазинного символа и состояния ставит в соответствие выход или переход. Переход в отличие от выхода заключается в выполнении операций над магазином, состоянием и входом, как было описано выше. Операции, которые запрашивали бы входной символ после концевогЪ маркера или выталкивали из магазина, а также вталкивали в него маркер дна, исключаются; 5) начальным содержимым магазина, которое представляет собой (при условии, что верхний символ считается расположенным справа) маркер дна, за которым следует (возможно, пустая) цепочка других магазинных символов. МП-автомат называется МП-распознавателем, если у него два выхода — ДОПУСТИТЬ и ОТВЕРГНУТЬ. Говорят, что цепочка символов входного алфавита (исключая концевой маркер) допускается распознавателем, если под действием этой цепочки с концевым маркером автомат, начавший работу в своем начальном состоянии и с начальным содержимым магазина, делает ряд переходов, приводящих к выходу ДОПУСТИТЬ. В противном случае цепочка отвергается. При описании переходов МП-автомата будем обозначать действия автомата словами ВЫТОЛКНУТЬ (или для краткости ВЫ- ТОЛК), ВТОЛКНУТЬ (или ВТОЛК), СОСТОЯНИЕ, СДВИГ и ДЕРЖАТЬ, причем: ВЫТОЛКНУТЬ означает вытолкнуть верхний символ магазина, ВТОЛКНУТЬ (Л), где А —магазинный символ, означает втолкнуть символ А в магазин, г) Заметим, что описываемая здесь и используемая до конца этой книги модель в теории автоматов называется детерминированным МП-автоматом, тогда как МП-автомат в общем случае может быть недетерминированным (ср. с недетерминированным конечным автоматом в разд. 2.12).— Прим. ред.
5.1. Определение автомата с магазинной памятью 131 СОСТОЯНИЕ (s), где s — состояние, означает, что следующим состоянием становится s, СДВИГ означает, что текущим входным символом становится следующий входной символ. В некоторых реализациях это может означать сдвиг указателя на входе, ДЕРЖАТЬ означает, что текущий входной символ надо держать . до следующего шага, т. е. оставить его текущим (в некоторых реализациях — оставить указатель на прежнем месте). Когда нам нужно определить переход, который оставляет содержимое магазина неизменным, это выражается в том, что мы опускаем слова ВЫТОЛКНУТЬ и ВТОЛКНУТЬ. Хотя ДЕРЖАТЬ по существу означает, что СДВИГ отсутствует, мы всегда будем записывать операции над входом в явном виде, чтобы читателю было понятнее, что происходит. Если автомат содержит в точности одно состояние, мы будем опускать слово СОСТОЯНИЕ. Сейчас мы опишем, как применить МП-распознаватель к проблеме скобок. Каждый раз, когда встречается левая скобка, в магазине будет вталкиваться символ А. Когда будет обнаружена соответствующая правая скобка, символ А будет выталкиваться из магазина. Цепочка отвергается, если на входе остаются правые скобки, а магазин пуст (т. е. во входной цепочке есть лишние правые скобки) или если цепочка прочитана до конца, а в магазине остаются символы А (т. е. входная цепочка содержит лишние левые скобки). Цепочка допускается, если к моменту прочтения входной цепочки до конца магазин опустошается. Полное определение таково: 1. Входное множество {(,), —|}. 2. Множество магазинных символов {А, у}. 3. Множество состояний {s}, где s — начальное состояние. 4. Переходы: (, A, s= ВТОЛКНУТЬ (А), СОСТОЯНИЕ (s), СДВИГ (, V, 8 = ВТОЛКНУТЬ (А), СОСТОЯНИЕ (s), СДВИГ ), A, s = ВЫТОЛКНУТЬ, СОСТОЯНИЕ (s), СДВИГ ), V, sОДЕРЖАТЬ Н, A, s = ДЕРЖАТЬ •Ч, V, s = ДОПУСТИТЬ Здесь комбинации входного символа, магазинного символа и состояния расположены слева от знака равенства, а переходы — справа от него. 5. Начальное содержимое магазина V. •Чтобы продемонстрировать работу автомата, мы изобразили на рис. 5.3, как он обрабатывает цепочку (()()) 5*
132 Гл. 5. Автоматы с магазинной памятью ¥ V ( ( ( ( ( ( ) f i ( ) ) а н А А V ' ) \ ( ) ) б \ ) S ( ) ) д н А А V н V / S » 4 V 1 ( ) ( ) ) н d ( 1 ) ( ) j ~> г ( ( ) ( ) ) н пи* 1/7V ЛОПУСТИТЬ Рис. 5.3.
5.1. Определение автомата с магазинной памятью 133 На рисунке показан каждый шаг процесса обработки, начиная с начальной конфигурации на рис. 5.3, а и кончая допускающей конфигурацией на рис. 5.3, з. Такое изображение последовательности конфигураций МП-автомата требует много места, поэтому представим ее в таком более компактном виде: а: б\ 6: г '. д: е : ж: з : V VA VAA VA VAA VA V ДОПУСТИТЬ м [s] W M w И Is] (OO)H ()())H )())4 0)4 ))H )4 4 В этом линейном представлении конфигураций МП-автомата магазин изображен слева, состояние — в середине, а необработанная часть входной цепочки — справа. Эта часть входной цепочки включает текущий входной символ и символы, которые следуют после ( ) н втолкнуть (л) сдвиг ВТОЛКНУТЬ^) сдвиг ВЫТОЛКНУТЬ сдвиг ОТВЕРГНУТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ Рис 5.4. него. Чтобы восстановить всю входную цепочку, нужно вернуться назад, к исходной конфигурации. Информацию, поступающую в управляющее устройство, выделить очень легко, так как символ, расположенный на верху магазина, находится непосредственно ■слева от состояния, а текущий входной символ — справа от него. Многие из МП-автоматов, применяемых на практике, имеют лишь одно состояние (как в этом примере), и в подобных случаях мы, как правило, опускаем информацию, касающуюся состояний. Управляющее устройство этого автомата с одним состоянием можно представить в виде управляющей таблицы, как на рис. 5.4, где показаны действия автомата для каждого сочетания входного символа и верхнего символа магазина. Столбцы таблицы обозначены входными символами, а на пересечении строк и столбцов
134 Гл. 5. Автоматы с магазинной памятью обозначены соответствующие им действия. Так как этот конкретный автомат имеет лишь одно состояние, информация о состоянии опущена. Мы будем пользоваться таблицами такого вида (т. е. со столбцами для входных символов и строками для символов магазина) как стандартным представлением МП-автоматов с одним состоянием. 5.2. Некоторые обозначения для множеств цепочек Чтобы привести несколько примеров обработки множеств цепочек МП-автоматом, мы введем некоторые обозначения. Начнем с определения трех операций над множествами цепочек. Применяя эти операции к множествам цепочек, мы получаем другие множества цепочек. Эти три операции — объединение, конкатенация и итерация Клини (или просто итерация). Объединение. Если Р и Q — множества цепочек, то объединение Р и Q — это множество цепочек, которые принадлежат Р или Q или обоим множествам одновременно. Хотя обычное теоретико-множественное обозначение для объединения — это Р U Q, в теории автоматов объединение множеств часто обозначается как P+Q. Некоторые примеры объединения: {FOR, IF, THEN} + {DO, IF} = {FOR, IF, THEN, DO}; {AB, X3} + {Y, e} = {AB, X3, Y, e}; {все цепочки из нулей и единиц, начинающиеся с 0 и заканчивающиеся 1} + {все цепочки из нулей и единиц, начинающиеся с 0 и заканчивающиеся 0} = {все цепочки из нулей и единиц, начинающиеся с 0}; {простые переменные Алгола} + {переменные с индексами Алгола} = {переменные Алгола}. Конкатенация. Конкатенация, или сцепление двух цепочек, определяется как цепочка, получаемая их соединением, или «приписыванием», друг к другу. Конкатенацией цепочек ОКРЕСТ и НОСТЬ,. например, является цепочка ОКРЕСТНОСТЬ. Длина получившейся цепочки равна сумме длин цепочек, участвующих в конкатенации. Так как длина пустой цепочки равна нулю, то в результате ее конкатенации с любой цепочкой последняя не изменится. Например,, конкатенацией цепочек ОКРЕСТ и е остается цепочка ОКРЕСТ длины шесть.
5.2. Некоторые обозначения для множеств цепочек 135 Конкатенацию как операцию над цепочками можно расширить до операции над множествами цепочек. Если Р и Q — множества .цепочек, то конкатенацией Р и Q называется множество, состоящее яз всевозможных конкатенации цепочек из f с цепочками из Q. Конкатенация множеств Р и Q обозначается P-Q. Точку можно, однако, опускать и писать PQ. Некоторые примеры конкатенации: {10} {1, 00}={101, 1000}; \аВ, X, ABY}{e, Y}={AB, X, ABY, XY, ABYY}; {все буквы} {все цепочки из букв и цифр}— = {все цепочки, начинающиеся с буквы, за которой следует цепочка из букв и цифр}. Конкатенация множества R с самим собой, т. е. RR или R-R, обозначается также R*. Например {0, 11}*= {00, 011, ПО, 1111}. Аналогично R1, где i — целое положительное число, обозначает множество R-R'...-R. Удобно считать, что Я°={е}. Это определение согласуется с правилом умножения степеней, -а именно так что нетрудно заметить, что обозначения, связанные с конкатенацией, во многом те же, что и для умножения. Итерация Клини. Полезно иметь обозначение для множества всех щепочек, состоящих из символов данного алфавита. Если А — множество символов алфавита, то будем говорить, что А*— множество всех цепочек, составленных из символов множества А. Предполагается, в частности, что А* всегда содержит пустую цепочку е. Так, {0, 1}* обозначает множество всех цепочек в алфавите 40, П. Описанная операция называется итерацией Клини или просто итерацией. Итерацию Клини как операцию над алфавитами можно расширить до операции над другими множествами цепочек. Например, {IF, THEN} * обозначает бесконечное множество цепочек, которое «ключает е, IF, THEN, IFIF, THENIFTHEN, IF THENIFIF. Если R — множество цепочек, то мы определим множество R* «бесконечным рядом В этом выражении + обозначает, разумеется, рассмотренную *ыше операцию объединения. Часто используется вариант итерации Клини, обозначаемый А+ я называемый позитивной итерацией. Если А — некоторое мно-
136 Гл. 5. Автоматы с магазинной памятью жество цепочек, то множество Л+ определяется равенством А+=АА*. Такии образом, А+ можно выразить как Л+=Л1+Л2+Л3+... . Множество Л+ в точности совпадает с множеством Л*, за исключением того, что Л+ содержит пустую цепочку е лишь тогда, когда ее содержит А. Эти три операции обладают тем свойством, что при их применении к регулярным множествам получается регулярное множество. (Регулярным называется множество, которое можно распознать конечным автоматом.) На самом деле любое регулярное множества цепочек в данном алфавите можно получить путем применения этих трех операций к символам алфавита. В оставшейся части данного' раздела мы дополним эти операции некоторыми способами задания множеств, позволяющими получать нерегулярные множества. Первый способ заключается в использовании переменных как показателей при обозначении множеств в виде степеней. Простым примером может служить {1"0п|я>0}. Так обозначается множество цепочек, состоящих из некоторого количества единиц, за которыми следует такое же количество нулей. Вот некоторые цепочки принадлежащие множеству: 10 111000 11111110000000 Вообще говоря, этот способ обозначения заключается в том, что в качестве показателей степени используются буквы и указываются отношения между ними, а также границы их значений. Еще один пример: {1п0т\п^т>0}. Эта формула обозначает множество цепочек, состоящих из некоторого числа единиц, за которыми следует такое же или меньшее число нулей; сюда входят, например, цепочки 111000 1110 Аналогичным образом множество {a"bmcmd" |л>0, т>0} состоит из цепочек, в которых после некоторого числа символов а следует некоторое число символов Ь, затем — некоторое число сим-
5.3. Пример распознавания множества МП-автоматом 137 волов с и некоторое число символов d, причем число символов а равно числу символов d, а число Ь совпадает с числом с. Этому множеству принадлежат такие, например, цепочки: abbccd aaabbbbccccddd Второй способ — это обозначение операции обращения цепочки •с помощью верхнего индекса г. Так, например (abc)r—cba. Используя операцию обращения, мы можем писать формулы вроде {до, дог|до принадлежит множеству (0+1)*}, которая обозначает множество цепочек, состоящих из произвольных цепочек из нулей и единиц, за которыми следуют их обращения. Это в точности множество цепочек четной длины, которые читаются слева направо так же, как и справа налево. Вот образцы таких цепочек: 1110110111 0000 Хотя эти новые способы обозначения позволяют задавать нерегулярные множества и описывать работу некоторых МП-автоматов, они не достаточно общие, чтобы описывать реальные языки программирования. Поэтому примерам и упражнениям этой главы не достает непосредственной практической мотивировки с точки зрения языков программирования. Тем не менее их достаточно для выработки правильного интуитивного понимания процессов обработки цепочек с помощью МП-автоматов. 5.3. Пример распознавания множества МП-автоматом Чтобы привести еще один пример распознавания МП-автоматом, использующим более чем одно состояние, рассмотрим задачу распознавания множества {0п1"|м>0}. В качестве первого шага построения МП-распознавателя опишем словами схему распознавания: Начальный отрезок цепочки, состоящий из нулей, вталкивается в магазин. Затем каждый раз, когда встречается единица, один нуль выталкивается из магазина. Цепочка допускается тогда и только тогда, когда в момент завершения считывания цепочки магазин пуст. Если после первого вхождения единицы встречается нуль, цепочка сразу отвергается.
1 2 3 4 5 6 7 8 1 2 3 4: 5: V [*,] 0 0 0 1 1 1 н V Z [s, ] 001 IH V Z Z [s, ] 01 IH V Z Z Z [s, ] 1 1 1 H VZZ [*j] 1 1 -, VZ [*2] H V Is21 -4 ДОПУСТИТЬ tx V [s, ] 0 0 1 0 1 1 4 V Z [s, ] 0 1 0 1 H v2г [s,] ioi и VZ [s2] 01H ОТВЕРГНУТЬ 6 Рис 5.5. Z Состояние 1 \sv\tHIV/iiM4& 1 V z Состояние 2 V 0 1 H СОСТОЯ HME(S,) втолкнуть (z) СДВИГ СОСТОЯНИЕ ($j) втолкнутьсг, СДВИГ СОСТОЯНИЕ^) ВЫТОЛКНУТЬ СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ 0 1 Н ОТВЕРГНУТЬ ОТВЕРГНУТЬ СОСТОЯНИЕ (S2) вытолкнуть СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ Начальное содержимое магазина; V Рис. 5.6.
5.4. Расширенные операции над магазином 139 Чтобы реализовать эту схему, нужно завести магазинный символ, представляющий входной символ 0. Хотя в качестве магазинного символа можно использовать сам нуль, во избежание путаницы мы предпочитаем пользоваться символом Z. Процесс обработки распадается на две фазы. Первая из них — фаза «вталкивания»; в этой .фазе нули, начинающие цепочку, помещаются в магазин. Эти нули представляются магазинными символами Z. Вторая фаза — фаза «выталкивания»; здесь при появлении единицы из магазина удаляется Z, а при появлении нуля, цепочка немедленно отвергается. Чтобы автомат «помнил», в какой фазе он находится, мы введем два состояния S* и s2, соответствующие этим фазам. Прежде чем детально определить управление МП-автоматом, посмотрим, как он работает, на примере последовательности конфигураций, возникающей при обработке цепочки 0 00 1 1 1, которую юн допускает. Эта последовательность конфигураций показана на рис. 5.5, а. Мы изобразили также на рис. 5.5, б последовательность конфигураций автомата, отвергающую цепочку 0 0 10 1 1. После конфигурации 3 на рис. 5.5, б автомат переходит в состояние s2, запоминая, что началась фаза выталкивания. Затем, встретив на входе нуль, он отвергает цепочку. Один из удобных способов описания механизма управления заключается в том, чтобы задавать множество управляющих таблиц, по одной для каждого состояния, причем каждая таблица имеет стандартный вид с одним состоянием. Две управляющие таблицы для нашего примера изображены на рис. 5.6. Чтобы уста- «овить, какое действие должно выполняться для данной комбинации состояния, входа и магазинного символа, нужно сначала найти управляющую таблицу для данного состояния, а затем по •входному и магазинному символам определить нужное действие в выбранной управляющей таблице. На рис. 5.6 комбинация s2, 1, Z вызывает выполнение операций СОСТОЯНИЕ (s2), ВЫТОЛКНУТЬ, СДВИГ. В итоге полное определение МП-автомата, распознающего множество {0"l"|n>0}, таково: 1) входное множество {0, 1, —|}, 2) множество магазинных символов {Z, V). 3) множество состояний {sj, s2}, где St — начальное состояние, 4) переходы, изображенные на рис. 5.6, 5) начальное содержимое магазина V- 5.4. Расширенные операции над магазином На самом деле имеется много разных способов определения класса моделей автоматов, переходы которых выбираются в зависимости от входа, состояния и верхних магазинных символов, а операции
140 Гл.5. Автоматы с магазинной памятью затрагивают только верхнюю часть магазина. Каждую из этих моделей принято называть автоматом с магазинной памятью. Таким образом, модель автомата, описанная в разд. 5.1,— это как раз пример модели МП-автомата. В тех случаях, когда может возникнуть недоразумение, мы будем называть МП-автоматы такого типа примитивными МП-автоматами. Есть несколько причин для того, чтобы начать изучение МП- автоматов с примитивной модели. Одна из них в том, что принципы МП-обработки проявляются в этой модели в простейшей форме. Вторая причина заключается в том.что эта модель — математически определенное понятие, которое можно применять при доказательстве теорем или при решении задач, приведенных в конце этой главы. Она служит стандартной моделью в том смысле, что термин «МП- автомат» сохраняется для всех автоматов, которые используют магазин аналогичным образом и обладают той же способностью распознавания (например, могут распознавать те же множества), что и примитивные МП-автоматы. Мы называем примитивные автоматы «примитивными» по той причине, что их переходы включают не более одной операции втал-i кивания или выталкивания. Эти операции можно было бы использовать при аппаратной реализации МП-автомата в некоторой реальной машине. Однако, если МП-автомат взять за основу при разработке программной реализации алгоритмов, эти операции оказываются неестественно ограниченными. В данном разделе мы будем рассматривать операции над магазином с точки зрения их программирования. Как только по состоянию, магазинному символу и входу выбран переход, с точки зрения программирования разумно сделать как можно больше, прежде чем вновь обращаться к управляющей информации и выбирать новый переход. Другими словами, разработчик может захотеть связать с переходами более общие процедуры и не ограничивать работу с магазином операциями вталкивания и выталкивания одного символа. Предположим, например, что надо поместить в магазин два символа. При желании можно втолкнуть в магазин первый символ и перейти в состояние, единственное назначение которого — поместить в магазин второй символ при следующем переходе. Однако этот метод с двумя переходами является неэффективным и неестественным по сравнению с другим очевидным методом, когда оба вталкивания выполняются одной процедурой перехода. Так как переход с двумя вталкиваниями можно промоделировать с помощью двух переходов примитивного МП-автомата, мы можем назвать такое вталкивание двух символов «расширенной операцией над магазином», а термин МП-автомат распространим на устройства, которые могут вталкивать в магазин по два символа сразу.
5.4 Расширенные операции над магазином 141 Методы, описанные в этой книге, включают процедуры переходов многих типов. В принципе результат каждой из этих процедур можно получить, комбинируя примитивные переходы; поэтому мы по-прежнему называем автоматы, в которых они используются, МП-автоматами. В этом разделе мы введем расширенную операцию над магазином, назовем ее ЗАМЕНИТЬ и проиллюстрируем ее использование. Другие расширенные операции будут вводиться в последующих главах по мере надобности. Операция ЗАМЕНИТЬ состоит в выталкивании верхнего символа магазина и последующем выполнении нескольких вталкиваний. Последовательность символов, которые операция ЗАМЕНИТЬ должна помещать в магазин, указывается в качестве ее аргумента. Так, мы пишем ЗАМЕНИТЬ (ABC) если в магазин нужно поместить ABC. Это эквивалентно последовательности операций ВЫТОЛКНУТЬ ВТОЛКНУТЬ(Л) ВТОЛКНУТЬ(В) ВТОЛКНУТЬ(С) Таким образом, левый символ последовательности помещается в магазин первым и оказывается ниже остальных символов этой последовательности. Если операция ЗАМЕНИТЬ (ABC) применяется к магазину V X У Z то новый магазин выглядит так: V X У А В С Операция ЗАМЕНИТЬ широко и систематически используется в последующих главах. В данный момент мы рассматриваем ее просто как сокращение для последовательности примитивных операций над магазином, которую программист может включить как часть одной процедуры перехода. Чтобы проиллюстрировать использование операции ЗАМЕНИТЬ, вернемся к задаче распознавания множества {0"1"|«>0}. В разд. 5.3 мы видели, что можно построить распознаватель, который работает в двух фазах — «вталкивания» и «выталкивания». Мы строили такой автомат, пользуясь для запоминания фазы управляющим состоянием. Теперь для этой же задачи построим другой МП-автомат. Новый МП-автомат использует тот же метод счета, что и предыдущий автомат. Z вталкивается в магазин при каждом появлении
142 Гл. 5. Автоматы с магазинной памятью на входе символа 0 и выталкивается из него при каждом появлении на входе символа 1. Однако для различения фаз вталкивания и выталкивания используется иная стратегия. Во время фазы втал- 0 1 ч X ЗАМЕНИТЬ (ZX) СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ вытолкнуть дер; (ать вытолкнуть сдвиг ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ Начальное содержимое магазина ■ V X Рис 5.7. кивания в верхней ячейке магазина хранится новый магазинный символ X. Единственное его назначение—напоминать управляющему устройству, что автомат находится в фазе вталкивания. Когда впервые встречается единица^ X выталкивается из магазина и автомат начинает сопоставлять символы Z и единицы. Наличие процедуры ЗАМЕНИТЬ позволяет нам реализовать этот алгоритм с помощью единственного состояния, как показано на рис. 5.7. Последовательность конфигураций при распознавании* цепочки 0 0 0 1 1 1 показана на рис. 5.8. Эту последовательность конфигураций можно сравнить с рис. 5.5, а, где изображена обработка этой же цепочки предыдущим автоматом. Операция ЗАМЕНИТЬ используется, когда на верху магазина X, а на входе 0. За один шаг эта операция выталкивает из магазина ненужный верхний символ X, помещает на его место символ для запоминания вхождения 0, а затем помещает на верх магазина другой X, чтобы указать, что автомат по-прежнему в «фазе вталкивания». 1 2 3 4 5: 6: 7: 8: 9: V X V Z X V Z Z X V Z Z Z X V Z Z Z V Z Z V Z V ДОПУСТИТЬ Рис 5.8. 00011 t н 0 0 0 1 1 1 н 11 И 111-1 1 1 И 1 1 н 1 н н
5.5. Перевод с помощью МП-автоматов 143 В этом примере впервые используется операция ДЕРЖАТЬ. Она появляется при переходе, на котором X выталкивается из магазина и начинается «фаза выталкивания». Входной символ 1 удерживается, т. е. сдвига на входе не происходит, и эту единицу можно сопоставить с соответствующим магазинным символом. Сравнивая рис. 5.7 с предыдущим автоматом, изображенным на рис. 5.6, мы видим, что новый автомат использует только два вида информации: входной символ и магазинный символ, тогда как предыдущему автомату был необходим обычный набор из трех видов информации. С другой стороны, в новом автомате операции с магазином сложнее. В отличие от конечных автоматов здесь нет понятия единственного «приведенного МП-автомата» и соответственно труднее сделать выбор между конкурирующими МП-автоматами. Во время написания этой книги не было даже известно, существует ли алгоритм, который решает проблему, допускают ли два МП-распознавателя одно и то же множество1). Во всяком случае на этом примере видно, что для одной и той же задачи можно построить несколько хороших МП-автоматов и даже есть возможность выбирать, какую информацию запоминать с помощью состояния, а какую хранить в магазине. 5.5. Перевод с помощью МП-автоматов МП-автомат называется МП-транслятором, если при распознавании он порождает выходную цепочку. Чтобы автомат выдавал выходную цепочку, управляющее устройство может наряду с обычными операциями над состоянием, входом и магазином производить операцию на выходе. При отсутствии выходной операции предполагается, что на выход ничего не выдается. Если надо выдать цепочку АВ, то в определении соответствующего МП-перехода мы пишем ВЫДАТЬ (А В) Чтобы посмотреть, как можно пользоваться операцией ВЫДАТЬ, рассмотрим задачу перевода произвольной цепочки из нулей и единиц в цепочку вида \nQm, где пит соответственно число единиц и нулей в данной цепочке. Например, цепочка 0 110 11 будет переведена в 1 1 1 1 0 0, так как в цепочке четыре единицы и два нуля. Один из способов такого перевода заключается в том, чтобы выдавать единицы сразу при их появлении на входе, а при появлении на ^входе нулей помещать их в магазин. Когда встречается концевой маркер, автомат выталкивает из магазина нули и выдает ) К моменту перевода это по-прежнему неизвестно.— Прим. рео\
144 Гл. 5. Автоматы с магазинной памятью их на выход. Управляющая таблица для автомата с одним состоянием, реализующего этот способ, изображена на рис. 5.9. О * , н ВТОЛКНУТЬ(А) СДВИГ L ВТОЛКНУТЬ (А) СДВИГ выхоШЮтЬ I выттктуъ ,\ ,* сдвиг у ■уК.'.'.Г держа- (tb^ ДОПУСТИТЬ Начальное содержите магазина: V Рис. 5.9. Последовательность конфигураций этого автомата при обработке цепочки 0 10 11 показана на рис. 5.10. Если переход вызывает операцию с выходом, мы помещаем эту операцию между конфигурацией, вызывающей переход, и конфигурацией, наступающей после перехода. Множество, распознаваемое в нашем примере,— 1: 2: 3: 4: 8: 9: V V О ВЫДАТЬ(1) V О V 0 0 ВЫДАТЬ (1/ V О О ВЫДАТЬ (1) V О О ВЫДАТЬ (0) V О ВЫДАТЬ (0) V ДОПУСТИТЬ Рис. 5.10 1 О I 0 1 1 ч 1 1 ч 1 1 ч 1 1 ч ! Ч Ч это просто (0+1)*, т. е. множество всех цепочек. В данном случае магазин служит не для распознавания, а только для перевода. Нули вталкиваются в магазин только для того, чтобы позже автомат выдал их на выходе. В следующем примере магазин будет использован как для распознавания, так и для перевода. Рассмотрим проблему распознавания множества {w2wr} до принадлежит (0+1)* и перевода каждой цепочки до2дог в цепочку l"0m, где п и m соответственно число единиц и нулей в цепочке до. Так, цепочка 0 10 112 110 10
5.5. Перевод с помощью МП-автоматов 145 должна быть переведена в 1110 0 Чтобы выполнить этот перевод, построим МП-автомат, работающий в двух фазах. Первая из них — фаза вталкивания — длится О 1 2 ч СОСТОЯНИЕ (ФАЗА 1) ВТОЛКНУТЬ (0) СДВИГ С0СТ0ЯНИЕ(ФАЗА1) ВТОЛКНУТЬ (0) СДВИГ СОСТОЯНИЕ (ФАЗА 1) ВТОЛКНУТЬ (0) СД6ИГ — СОСТОЯ НИ Е (ФАЗ А 1) ВТОЛКНУТЬ (1) ВЫДАТЬ (1) СДВИГ СОСТОЯНИЕ (ФАЗА 1) ВТОЛКНУТЬ (1) ВЫДАТЬ (1) СДВИГ СОСТОЯНИЕ(ФАЗА1) ВТОЛКНУТЬ С V) ВЫДАТЬ (1) СДВИГ ■ СОСТОЯНИЕ (ФАЗАф СДВИГ СОСТОЯНИЕ (ФАЗА 2) СДВИГ СОСТОЯН.ИЕ(ФАЗА2) СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ а 0 1 2 ч СОСТОЯНИЕ (ФАЗА2) вытолкнуть ВЬ|ДАТЬ ( 0) СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ СОСТОЯНИЕ (ФАЗА2) вытолкнуть СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ 6 Начальное содержимое магазина : v Рис 5.11. (а) Таблица для начального состояния ФАЗА 1; (б) Таблица для состояния ФАЗА 2.
146 Гл. 5. Автоматы с магазинной памятью до тех пор, пока на входе не встретится 2. Во время этой фазы при появлении на входе символов 0 и 1 они помещаются в магазин. Кроме того, при появлении на входе единицы она выдается на выход. Вторая фаза — фаза выталкивания — наступает после того, как встретился входной символ 2. Во время этой фазы входные символьТ сравниваются в магазинными символами, чтобы проверить, совпадают ли они. В результате мы убедимся, что цепочка 1: V ФАЗА1 0 0 12 10 1Н 2: V 0 ФАЗА1 0 1 2 1 0 1 Н 3: V 0 0 ФАЗА1 1 2 1 0 1 Н ВЫДАТЬ (1) 4: V 0 0 1 ФАЗА1 2101 Н 5: V 0 0 1 ФАЗА2 1 0 1 -» 6: V 0 0 ФАЗА2 0 1 -I ВЫДАТЬ (0) 7: V 0 ФАЗА2 1 н 8: ОТВЕРГНУТЬ Рис. 5.12. после 2 действительно является обращением цепочки, предшествующей символу 2. При совпадении символов каждый встреченный нуль выдается на выход. Фаза МП-автомата запоминается с помощью состояния. Две управляющие таблицы, реализующие эту схему, изображены на рис. 5.11, где используются состояния ФАЗА1 и ФАЗА2. На рис. 5.12 показана работа автомата, отвергающего входную цепочку 0 0 12 10 1. Так как эта цепочка отвергается, мы говорим, что у нее нет перевода, даже если МП-автомат выдает что-то на выход до того, как обнаруживает входной символ 1, который не совпадает с соответствующим символом цепочки, предшествующей символу 2. 5.6. Зацикливание Одна из опасностей, связанных с использованием МП-автомата, заключается в том, что он может работать бесконечно, никогда не выходя из процесса обработки. Рассмотрим, например, автомат с одним состоянием, изображенный на рис. 5.13. Последовательность конфигураций этого автомата при обработке цепочки 0 0 1 показана на рис. 5.14. Когда на верху магазина находится символ А, а на входе — символ 0, автомат вталкивает в магазин символ В,
5.6. Зацикливание 147 причем входной символ остается прежним. Однако В тотчас же выталкивается, причем автомат удерживает все тот же входной символ. Поэтому автомат зацикливается, т. е. бесконечно повторяет цикл, никогда не выходя из обработки и не сдвигаясь по входу. Аналогичная ситуация возникает при обработке входной цепочки 1 1 0, как показано на рис. 5.15. Когда на верху магазина символ В, а на входе 1, автомат вталкивает в магазин еще один В, удерживая тот же входной символ. Таким образом, автомат зацикливается, продолжая помещать в магазин символы В, не выходя из обработки и не сдвигая входной указатель. При построении МП-автомата, предназначенного для использования в компиляторе, нужно быть уверенным, что автомат никогда не зациклится — даже при обработке недопустимой цепочки. Если он зацикливается, компилятор тоже зациклится и не выполнит своей задачи. Тем не менее проблема зацикливания не является одной из основных при построении компиляторов. Все обычные процедуры 1: 2: 3: 4: 5: б: 7: V VA VAB VA VAB VA VAB ООН он он он он он он 1: 2: 3: 4: 5: V VB VBB VBBB VBBBB Рис. 5.14. Рис. 5.15. построения, включая те, которые приводятся в данной книге, фактически дают автоматы, которые никогда не зацикливаются. Более того, даже если автомат строится некоторым специальным образом, О 1 втолкнуться ДЕРЖАТЬ ВЫТОЛКНУТЬ ДЕРЖАТЬ ВЫТОЛКНУТЬ сдвиг втолкнуть^ ДЕРЖАТЬ ВТОЛКНУТЬА ВТОЛКНУТЬ(А) СДВИГ СДВИГ вытолкнуть ДЕРЖАТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ Начальное содержимое магазию-я Рис. 5.13. 1 юн i.o Н юн юч
148 Гл. 5, Автоматы с магазинной памятью эвристических соображений обычно достаточно, чтобы убедиться в том, что автомат не может зацикливаться. Хотя, как правило, они не требуются, существуют методы, позволяющие по каждой комбинации состояния, входного и магазинного символов определять, не начинается ли с этой комбинации- цикл. Полное исследование этого вопроса дано в книге Хопкрофтэ и Ульмана [1969] 1). 5.7. Замечания по литературе Организация памяти в виде магазина (или, как часто говорят, стека) используется в программировании давно и принадлежит «фольклору» этой области. Использование автоматов с магазинной памятью в компиляторах описано у Ершова [1959], у Бауэра и За- мельзона [1959, I960]. Детерминированные МП-автоматы, рассматриваемые в этой главе, изучались в работах Шютценберже [1963], Фишера [1963], Гинзбурга и Грейбах [1966]. Они обсуждаются также в книге Хопкрофтэ и Ульмана [1969]. Упражнения 1. а) Напишите три цепочки, принадлежащие множеству, распознаваемому МП-автоматом с одним состоянием, изображенным на рисунке, б) Для каждой из этих цепочек укажите соответствующие последовательности конфигураций, допускающие эти цепочки. a b с -i втолкнуть(а) сдвиг ВТОЛКНУТЬ (С) сдвиг ВТОЛКНУТЬ (15) ДЕРЖАТЬ ВТОЛКНУТЬ (А) СДВИГ ОТВЕРГНУТЬ ВЫТОЛКНУТЬ ДЕРЖАТЬ ВТОЛКНУТЬ (С) сдвиг ВТОЛКНУТЬСЯ) сдвиг ВЫТОЛКНУТЬ сдвиг ВТОЛКНУТЬ(А) сдвиг вытолкнуть СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ Начальное содержимое магазина : V 2. Опишите словами множество цепочек, распознаваемое МП-автоматом с одним состоянием, изображенным на рисунке. (Сравните с рис. 5.4.) *) См. также книгу Ахо и Ульмана [1972а, стр. 213—215].— Прим, ред.
Упражнения 149 ВТОЛКНУТЬ(О) сдвиг ВТОЛКНУТЬ(О) сдвиг ВЫДАТЬ (1) СДВИГ ВЫДАТЬ (1) сдвиг ВЫДАТЬ (0) ВЫТОЛКНУТЬ ДЕРЖАТЬ ДОПУСТИТЬ Начальное содержание магазина: V 3. Постройте (примитивный) МП-распознаватель для каждого из следующих множеств цепочек:. а) {l"Om|«>m>0}; б) {l"Om|n>m>0}; в) \\»От\т>п>0}; г) {1"0п|я>0}+{0'я!2'я|л, т>0); д) \\пОп\тОт\п, т>0}; е) {1"0'в1'я0л|л> m>\); ж) множество цепочек из нулей и единиц, где число единиц равно числу нулей; з) {bi2(bi+l)r}, где Ь; — цепочка из нулей и единиц, являющаяся двоичным представлением числа i (например, для (=48 цепочка, принадлежащая данному языку такова: 1100002100011). 4. а) Для каждого из множеств задачи 3 укажите цепочку длины, большей 3. б) Покажите последовательность конфигураций соответствующих автоматов, построенных в упр. 3, при распознавании каждой из этих цепочек. 6. Постройте (примитивный) МП-автомат, распознающий дополнение множества, распознаваемого автоматом на рис. 5.6. в. На каждом из следующих языков напишите программу для вычислительной машины, реализующую МП-автомат на рис. 5.7: а) Фортран. Напишите подпрограммы, выполняющие операции ВТОЛКНУТЬ и ВЫТОЛКНУТЬ. б) Лисп. в) Язык ассемблера некоторой вычислительной машины. Один регистр используйте как указатель верхнего символа магазина. Составьте макрокоманды, выполняющие операции ВТОЛКНУТЬ и ВЫТОЛКНУТЬ. 7. Решите задачи 3 и 4, пользуясь операцией ЗАМЕНИТЬ. Постарайтесь найти автомат с одним состоянием. 8. Составьте три допускаемые цепочки и соответствующие последовательности конфигураций для следующего МП-автомата с одним состоянием, изображенного на рисунке. 0 1 н "ЗАМЕНИТЬ (АА) СДВИГ ОТВЕРГНУТЬ ВЫТОЛКНУТЬ СДВИГ ОТВЕРГНУТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ Начальное содержимое магазина: VA
150 Гл. 5. Автоматы с магазинной памятью 9. Рассмотрим новую расширенную операцию над магазином: ВЫТОЛКНУТЬ («), где п — произвольное целое положительное число. Эта операция выталкивает из магазина л верхних символов. Если в магазине меньше п символов, включая концевой маркер, то автомат отвергает входную цепочку. а) Докажите, что ВЫТОЛКНУТЬ {п) можно промоделировать на примитивном МП-автомяте. б) Пользуясь этой новой операцией, постройте автоматы, распознающие каждое из следующих множеств: i) {l**0«|m>0} ii) {12<»0'в}и{1л2л} m>0, «>0. 10. Покажите последовательности конфигураций каждого из МП-автоматов на рис. 5.6, 5.7, 5.9 и 5.11 при обработке пустой цепочки. 11. Найдите множество входных цепочек, под действием которых происходят все переходы автоматов на рис. 5.6 и 5.7. 12. Используя операцию ЗАМЕНИТЬ, постройте транслятор с одним состоянием, который выполняет тот же перевод, что и автомат на рис. 5.11. 13. Постройте не примитивные МП-автоматы, которые будут выполнять следующие переводы: а) l"*)»1 в 1л22л, где «>0, /и>0; б) l«o»l*On в l'«0',+m, где тХ), л>0; в) Ь{ в (bi+i)r, где Ь{ — цепочка из нулей и единиц, являющаяся бинарным представлением числа i; г) \т0п в \т~п, если т>п, 0 "-«, если т<п, е, если т=п (задача (г) аналогична выдаче сообщений об ошибке для неправильных скобочных выражений). 14. Покажите, что любой конечный распознаватель можно промоделировать МП- распознавателем с одним состоянием. 15. Покажите, что каждый из МП-автоматов, изображенных на рисунке, никогда не зацикливается при обработке любой входной цепочки. 0 1 2 3 Н ВЫТОЛКНУТЬ сдвиг ВТОЛКНУТЬ(В) сдвиг ВТОЛКНУТЬ (4) сдвиг ВТОЛКНУТЬ(4.) сдвиг ДОПУСТИТЬ втолкнуть(а) сдвиг ОТВЕРГНУТЬ ВЫТОЛКНУТЬ СДВИГ ОТВЕРГНУТЬ ВЫТОЛКНУТЬ СДВИГ допустить ВТ0ЛКНУТЬ(4) сдвиг ДОПУСТИТЬ ОТВЕРГНУТЬ ДОПУСТИТЬ Начальное содержимое магазина: v
Упражнения 151 н ВТОЛКНУТЬСЯ сдвиг ВЫТОЛКНУТЬ сдвиг ВТОЛКНУТЬСС) ДЕРЖАТЬ ДОПУСТИТЬ ВТОЛКНУТЬСС) ДЕРЖАТЬ ОТВЕРГНУТЬ ВТОЛКНУТЬСЯ) сдвиг ВЫТОЛКНУТЬ ДЕРЖАТЬ ВТОЛКНУТЬСЯ) СДВИГ ДОПУСТИТЬ вытолкнуть ДЕРЖАТЬ ДОПУСТИТЬ Начальное содержимое магазинам 16. Для каждого из следующих случаев покажите, что МП-автомат с заданным свойством никогда не "зацикливается ни на какой входной цепочке. а) Каждый переход, не являющийся выходом из процесса обработки, содержит операцию СДВИГ. б) Операция ДЕРЖАТЬ встречается только в переходе ВЫТОЛКНУТЬ. 17. Найдите две входные цепочки, одна из которых начинается нулем, а другая — единицей, при обработке которых следующий автомат зацикливается. О 1 2 3 ■ н ДОПУСТИТЬ ВЫТОЛКНУТЬ сдвиг ВТОЛКНУТЬ(Е) ДЕРЖАТЬ ДОПУСТИТЬ ВТОЛКНУТЬ (С) ДЕРЖАТЬ ВТОЛКНУТЬ^) сдвиг ВТОЛКНУТЬ (А) ДЕРЖАТЬ ДОПУСТИТЬ ВТОЛКНУТЬЦ сдвиг ВТОЛКНУТЬСД) сдвиг ОТВЕРГНУТЬ ВТОЛКНУТЬСЯ) СДВИГ втолкнытьсе) СДВИГ ОТВЕРГНУТЬ втолкнуться) ДЕРЖАТЬ ВТОЛКНУТЬ(£) СДВИГ ДОПУСТИТЬ ВТОЛКНУТЬСИ) СДВИГ втолкнуть 04) СДВИГ ВТОЛКНУТЬСС) ДЕРЖАТЬ ВЫТОЛКНУТЬ ДЕРЖАТЬ допустить ВТОЛКНУТЬЙ СДВИГ ВТОЛКНУТЬ(Я) СДВИГ ОТВЕРГНУТЬ ВЫТОЛКНУТЬ сдвиг ОТВЕРГНУТЬ ДОПУСТИТЬ ВТОЛКНУТЬ (Я) сдвиг ДОПУСТИТЬ Начальное содержимое магазина; V 18. Перестройте автомат рис. 5.13 таким образом, чтобы новый автомат допускал тот же язык, но никогда не зацикливался. 19. Поясните, почему МП-автомат не может распознать ни одно из следующих. множеств: а) jlWI/tX)}; б) «о wr}, где w — цепочка из нулей и единиц; в) {1по»Н-{1лОгл} п>0.
152 Гл. 5. Автоматы с магазинной памятью 20. Постройте МП-автомат, допускающий S-выражения Лиспа. Входной алфавит автомата таков: {АТОМ, (,), -, 4} 21. Предположим, МП-автомат таков, что при обработке любой входной цепочки число символов, записанных в его магазине, не превышает некоторой константы. Покажите, что такой МП-автомат можно промоделировать конечным автоматом. 22. Приведите пример такого множества цепочек, что его можно распознать МП-автоматом, в переходах которого используются обе операции — СДВИГ и ДЕРЖАТЬ, но нельзя распознать МП-автоматом, в котором используется только операция СДВИГ. 23. Покажите, что МП-автомат, который вталкивает в магазин маркер дна, можно смоделировать МП-автоматом, который этого не делает. 24. Покажите, что любой МП-автомат можно промоделировать на МП-автомате, магазинный алфавит которого состоит из маркера дна и еще двух символов. 25. Покажите, что для любого k>\ существует множество цепочек, которое можно распознать примитивным МП-автоматом с k состояниями, но нельзя распознать никаким примитивным МП-автоматом с k—1 состояниями. 26. Один из способов расширения возможностей конечного автомата состоит в том, что в его переходы добавляется операция ДЕРЖАТЬ. Покажите, что конечный распознаватель, в котором используется операция ДЕРЖАТЬ, может распознавать только регулярные множества. 27. Покажите, что если множество цепочек допускается МП-автоматом, то его дополнение также допускается некоторым МП-автоматом. 28. Пусть даиы МП-автомат и конечный распознаватель. Покажите, что можно построить МП-автомат, который распознавал бы пересечение множеств, допускаемых этими двумя автоматами. 29. Приведите пример такого множества цепочек, которое может распознать МП-автомат с магазинным алфавитом, содержащим маркер дна и еще два символа, но не может распознать никакой МП-автомат, чей магазинный алфавит состоит из маркера дна и еще одного символа. 30. Как по данному МП-автомату проверить, заканчивается ли выходом обработка каждой из его входных цепочек? -31 Постройте МП-автомат, допускающий множество истинных логических выражений Фортрана, (т. е. принимающих значение .TRUE.), в которых используются операции .AND., .OR. и .NOT., а операндами являются логические константы .TRUE, и .FALSE. . Вот, например, одно из таких выражений: .TRUE. .AND. .NOT. ( .FALSE. .OR. ( .FALSE. .AND. .TRUE. )) Входным алфавитом («словарем») автомата является {.TRUE., .FALSE., .NOT., .AND., .OR.,), (, 4}
6 Контекстно-свободные грамматики 6.1. Введение В этой главе мы изложим метод определения множеств цепочек,, основанный на понятии контекстно-свободной грамматики (сокращенно — КС-грамматики). В отличие от предыдущих методов описания таких множеств мощность метода контекстно-свободных грамматик достаточна, чтобы описывать почти все так называемые синтаксические свойства языков программирования. КС-грамматики на самом деле часто используются в руководствах по языкам программирования. Применение того или иного метода задания множества при описании языка программирования может оказаться важным шагом в построении компилятора для этого языка, если имеются систематические методы преобразования описания множества в программу, которая это множество обрабатывает. В последующих главах будут разработаны методы преобразования некоторых контекстно-свободных грамматик в МП-автоматы, которые распознают и даже транслируют множества, задаваемые этими грамматиками. Прежде чем вникать в эти методы, мы должны понять, как контекстно-свободные грамматики определяют множества (гл. 6) и. как можно определить перевод в терминах этих грамматик (гл. 7). 6.2. Формальные языки и формальные грамматики Многие понятия этой главы аналогичны понятиям, которые используются при изучении естественных языков, например английского или русского. На самом деле некоторые определения, появляющиеся в этой главе, первоначально были введены с целью описания естественных языков. Наиболее фундаментальным является понятие языка. С теоретической точки зрения слово «язык» — синоним термина «множество цепочек». Так, язык Фортран IV можно понимать как множество цепочек, задаваемое некоторым множеством правил. Наиболее интересные языки, такие, как Фортран IV, состоят из бесконечного множества цепочек. Чтобы отличать употребление слова «язык» в значении точно определенного множества цепочек от употребления этого слова
154 Гл. 6. Контекстно-свободные грамматики в повседневной речи, множество цепочек называют иногда формальным языком. Чтобы применить математический подход к проблемам, связанным с языками и их обработкой, мы должны ограничиться множествами цепочек, которые можно определить некоторым точным образом. Есть много способов точного задания таких множеств. Один способ, например, заключается в задании языка как множества, допускаемого каким-нибудь распознавателем цепочек вроде конечного автомата или автомата с магазинной памятью. Другой подход состоит в использовании методов, которые можно считать грамматическими. Термин «формальная грамматика» применим к любому определению формального языка, основанному на «грамматических правилах», с помощью которых можно порождать и анализировать цепочки аналогично тому, как грамматики используются при изучении естественных языков. В этой главе мы займемся особым видом формальных грамматик, называемых контекстно-свободными грамматиками. 6.3. Формальные грамматики: пример В этом разделе приводится формальная грамматика, которая в какой-то степени напоминает фрагмент грамматики русского языка,1) и задает формальный язык, состоящий из четырех русских предложений. В этой формальной грамматике используются элементы, играющие роль членов предложения или частей речи: (предложение > (подлежащее > (сказуемое > (дополнение > (прилагательное > (существительное > Мы заключаем их в угловые скобки, чтобы отличать их от слов из фактического словаря, составляющих предложения языка. В нашем примере словарь состоит из следующих пяти слов, или «символов»: ДОМ ДУБ ЗАСЛОНЯЕТ СТАРЫЙ (точка) 1) В оригинале пример из английского языка.— Прим, перев.
6.3. Формальные грамматики: пример 155 В грамматике имеются определенные правила, содержащие информацию о том, как из этих символов можно строить предложения языка. Одно из этих правил таково: 1. (предложение)-> (подлежащее) (сказуемое) (дополнение). Это правило интерпретируется следующим образом: «Предложение может состоять из подлежащего, за которым следуют сказуемое, затем дополнение и точка». В грамматике вполне могут быть и другие правила, задающие предложения другой структуры. Однако в данной грамматике таких правил нет. Остальные правила таковы: 2. (подлежащее) -*- (прилагательное) (существительное) 3. (дополнение) -> (прилагательное) (существительное) 4. (сказуемое) -^ЗАСЛОНЯЕТ 5. (прилагательное) -> СТАРЫЙ 6. (существительное )-> ДОМ 7. (существительное )-> ДУБ Применим эту грамматику для порождения (или вывода) предложения. По правилу 1 предложение имеет вид (подлежащее) (сказуемое) (дополнение). Так как, согласно правилу 2, подлежащим может быть комбинация (прилагательное) (существительное) ее можно подставить вместо подлежащего и получить предложение, которое имеет вид (прилагательное) (существительное) (сказуемое) (дополнение). Аналогичным образом можно применить правило 3, чтобы заменить (дополнение) и получить (прилагательное) (существительное) (сказуемое) (прилагательное ) (существительное). Теперь можно дважды применить правило^, чтобы, заменив (прилагательное), получить СТАРЫЙ (существительное) (сказуемое) СТАРЫЙ (существительное ). Применяя правила 6 и 7, заменяющие первое и второе (существительное), и правило 4, заменяющее (сказуемое), получаем готовое предложение: СТАРЫЙ ДОМ ЗАСЛОНЯЕТ СТАРЫЙ ДУБ.
156 Гл. 6. Контекстно-свободные грамматики Этот вывод можно наглядно изобразить в виде дерева (см. рис. 6. 1). Дерево показывает, какие правила применялись к различным промежуточным элементам, но скрывает порядок их применения. Таким образом, можно видеть, что результирующая цеппн- <предложейие> <подлежащее> <сказуе~мое> <доп'олнение> Л < прилагательное^ <суш.ествит> <ррилагательноё> <суш,естбит2> СТАРЫЙ ЛОМ ЗАСЛОНЯЕТ СТАРЫЙ Рис. 6.1, лав ка не зависит от порядка, в котором делались замены промежуточных элементов. Иногда говорят, что дерево представляет собой •«синтаксическую структуру» предложения. Идея вывода подсказывает другие интерпретации правил, подобных правилу (подлежащее>->- (прилагательное) (существительное) Вместо того чтобы говорить «(подлежащее) — это (прилагательное), за которым следует (существительное)», можно сказать, что (подлежащее) «порождает» (или «из него выводится», или «его можно заменить на») (прилагательное) (существительное). С помощью этой грамматики можно вывести также три других предложения, а именно: СТАРЫЙ ДУБ ЗАСЛОНЯЕТ СТАРЫЙ ДОМ. СТАРЫЙ ДОМ ЗАСЛОНЯЕТ СТАРЫЙ ДОМ. СТАРЫЙ ДУБ ЗАСЛОНЯЕТ СТАРЫЙ ДУБ. Эти три предложения и предложение, выведенное раньше, и есть все предложения, порождаемые данной грамматикой. Множество, состоящее из этих четырех предложений, называется языком, который определяется грамматикой (порождается ею или выводится в ней). €.4. Контекстно-свободные грамматики Грамматика, приведенная в предыдущем разделе,— это простой пример грамматики, принадлежащей интересующему нас классу контекстно-свободных грамматик. В данном разделе мы определим
6.4. Контекстно-свободные грамматики 157 этот тип грамматик и введем обозначения, принятые при их описании. Такие элементы, как (подлежащее) или (существительное), играющие роль членов предложения или частей речи, называются нетерминальными (вспомогательными) символами или просто нетерминалами. В контекстно-свободной грамматике может быть любое конечное число нетерминалов. При определении языков программирования нетерминалами служат такие элементы, как (оператор), (арифметическое выражение) и т.д. Такие элементы, как ДУБ, ЗАСЛОНЯЕТ, играющие роль слов из словаря языка, называются терминальными (основными) символами или просто терминалами. КС-грамматика может содержать любое конечное число терминалов. В языках программирования терминалами являются фактически используемые в них слова и символы, такие, как DO, -f и т. д. Правила грамматики иногда называются продукциями 1) и в общем виде выглядят так: Один нетерминал -> любая конечная цепочка из терминалов и нетерминалов Цепочка справа от стрелки может быть пустой. Пример такого правила: Иногда правило с пустой правой частью мы будем называть эпсилон-правилом. КС-грамматика может содержать любое конечное множество продукций. Пример продукции языка программирования: (оператор)-)- IF (логическое выражение) THEN (оператор) Один из нетерминалов выделен как начальный нетерминал или начальный символ (или аксиома), с которого должны начинаться выводы цепочек языка. Для естественных языков таким нетерминалом может быть (предложение), для языков программирования — (программа). Начальный символ мы будем часто обозначать через (S). Суммируя все сказанное, будем задавать КС-грамматику а) конечным множеством нетерминалов; б) конечным множеством терминалов, которое не пересекается с множеством нетерминалов; в) конечным множеством правил вида (Л ) -> а *) Обычно предпочитают термин «правило». Второй термин чаще используют, когда в грамматике встречаются правила разных типов (например, в атрибутных грамматиках, помимо продукций, есть правила вычисления атрибутов).— Прим. ред.
158 Гл. 6. Контекстно-свободные грамматики где С4 ) — нетерминал, а а — цепочка терминалов и нетерминалов (возможно, пустая); нетерминал (А > называется левой частью правила, а а — правой частью; г) одним нетерминальным символом, выделенным в качестве начального. Если множество правил приводится без специального указания множества нетерминалов и терминалов, то предполагается, что грамматика содержит в точности те нетерминалы и терминалы, которые встречаются в правилах. Пусть, например, даны четыре таких правила: 1. S-^aAbS 2. S-+b 3. А-у S Ac 4. Л->- е Если больше ничего не определено, предполагается, что множество нетерминалов — {S, А}, так как это те нетерминалы, которые встречаются в левых частях правил. Предполагается также, что множество терминалов — {а, Ь, с), так как это остальные символы, используемые в правилах. Понятно, что символ е в правиле 4 представляет пустую цепочку и не является символом грамматики. Правило 4 можно записать без е в таком виде: 4. А-+ Как было сказано в предыдущем абзаце, можно определять грамматику, задавая ее правила и начальный нетерминал. Нетерминальное и терминальное множества нужно задавать в явном виде лишь тогда, когда они не совпадают с множествами, которые получаются описанным спосо- 1. <s> —> а<А> b <S> б°м- Так, если бы мы хотели, чтобы терминальным множеством в последнем примере 2. <S>—*b было множество {а, Ь, с, d), то нам при- 3. <л> —>■ <s> <A> с шлось бы задать его в явном виде. В этом 4 <А._+ случае автомат, распознающий язык, вос- "8 принимал бы d как вход, хотя этот символ рис_ 6-2> не входит ни в одну допустимую цепочку. Чтобы легче было различать нетерминальные и терминальные символы, примем соглашение заключать нетерминалы в угловые скобки при их определении. В соответствии с этим соглашением правила из последнего примера записываются, как показано на рис. 6.2. Благодаря этому достаточно посмотреть на какую-нибудь цепочку вроде а{А)Ь{В), чтобы понять, какие символы нетерминальные, а какие терминальные. Хотя обозначения, используемые нами для описания грамматик, довольно широко распространены в соответствующей лите-
6.5. Выводы 159 ратуре, часто используется еще один способ записи, называемой формой Бэкуса — Наура или БНФ. В этих обозначениях -*- заменяется символом ::=, за которым может следовать любое число правых частей, разделенных вертикальной чертой |. Здесь также нетерминалы заключаются в угловые скобки. Пользуясь БНФ, мы запишем грамматику, приведенную на рис. 6.2, так: {S):=a(A)b{S)\b (Л>:= (S)(A)c\& Идею совмещения правых частей можно применять, разумеется, и при записи со стрелкой ->-, но для ясности мы предпочитаем писать каждое правило в отдельной строке, так, чтобы можно было нумеровать строки, как на рис. 6.2., и затем ссылаться на них по номерам. 6.5. Выводы В этом разделе мы более подробно обсудим, как грамматики используются для порождения цепочек языка. Правила грамматики используются для того, чтобы задавать способы подстановки или замены цепочек. Подстановка осуществляется путем замены некоторого нетерминала в какой-нибудь заданной цепочке терминалов и нетерминалов на правую часть правила, левой частью которого является этот нетерминал. Иногда мы будем говорить, что правило применяется к нетерминалу цепочки. Рассмотрим, например, такую грамматику с начальным нетерминалом (S): 1. (S)^a(A)(B)c 2. (S)->e 3. (A)^c(S){B) 4. Ш-»- {A)b 5. {B)^b(B) 6. (B)->a Если дана цепочка а<Л><В>с t и мы хотим применить правило 5 к нетерминалу (В), на который указывает стрелка, то результат соответствующей подстановки таков: а(А)Ь(В)с
160 Гл. 6. Контекстно-свободные грамматики Эту подстановку мы записываем так: а <Л> <В> с=ьа < Л> Ь <В> с t 5 Вертикальная стрелка указывает здесь на заменяемый нетерминал, число под стрелкой указывает номер применяемого правила, а символ =Ф отделяет цепочку до подстановки от цепочки, получающейся после подстановки. Иногда это обозначение будет использоваться без вертикальной стрелки, при этом указываются только цепочки до и после подстановки. Можно, например, писать а<А><В>с=ьа<А>Ь<В->с что является сокращенной записью утверждения «цепочка а(А)Ь(В)с может быть получена из цепочки а(А){В)с в результате одной подстановки». Однако такого сокращенного обозначения не всегда достаточно для описания выполняемых подстановок. Например, ту же. цепочку можно также получить путем следующей подстановки: а<Л><5>с=>а<Л>6<В>с t 4 Последовательность подстановок называется выводом. Цепочка acabac, например, может иметь следующий вывод, начинающийся с начального нетерминала: <S> => а < Л> <5> с =ф а <Л> Ъ <В> с=>ас <S> <B> b <В> с t t t t 14 3 6 =Ф ас <S> ab <5> с =t> acab <£> с =t> acabac t t 2 6 Каждая цепочка терминалов и нетерминалов, встречающаяся в выводе, называется промежуточной цепочкой этого вывода. Так, в описанном выше выводе семь промежуточных цепочек, включая начальную и заключительную цепочку (промежуточную цепочку, выводимую из начального символа, в литературе иногда называют сентенциальной формой или выводимой цепочкой). Мы часто будем употреблять слово «вывод», не указывая начальной цепочки вывода. В таких случаях предполагается, что начальной цепочкой является начальный нетерминал. Если имеется в виду другая начальная цепочка, это указывается явно.
6.6. Деревья 16! Существование вывода одной цепочки из другой обозначается с помощью символа Так, имея в виду, что «существует вывод цепочки acabac из цепочки (S)» или, что эквивалентно, «из цепочки (S) можно вывести цепочку acabac», мы будем писать <S> =t>* acabac Для единообразия допускается нулевое число подстановок в выводе. Например, Ь(А)с можно рассматривать как вывод нулевой длины, в котором b (А )с является одновременно начальной и заключительной цепочкой. Таким образом, для любой цепочки а мы можем написать так как а можно получить из себя самой с помощью последовательности подстановок длины нуль. Звездочка «*» здесь играет ту же роль, что и в итерации Клини, так как означает нуль или более применений отношения =*>. Если необходимо исключить слишком тривиальный вывод нулевой длины и указать, что «цепочку acabac можно получить из цепочки (5) с помощью вывода, длина которого больше нуля», то символ * заменяют на + и пишут <S> =t>+ acabac Язык, задаваемый грамматикой, мы определим как множество терминальных цепочек, которые можно вывести из начального символа грамматики. Иногда говорят, что язык «определяется» грамматикой, «порождается» ею или «выводится» в ней. Любой язык, который можно задать контекстно-свободной грамматикой, называется контекстно-свободным языком (или, кратко, КС-языком). В приведенном выше примере цепочку acabac можно вывести из начального символа грамматики, и поэтому acabac принадлежит языку, задаваемому грамматикой. С другой стороны, изучение этой грамматики показывает, что цепочка bb, например, не выводится из (S); таким образом, bb не принадлежит языку, задаваемому этой грамматикой. 6.6. Деревья Мы определили КС-язык, задаваемый некоторой грамматикой, как множество терминальных цепочек, которые можно вывести из начального символа. Можно построить дерево вывода цепочки КС- Ф. Льюис и др.
162 Гл. 6. Контекстно-свободные грамматики a <A> < В> с <5> a < A> <B>с Г а a <A> b <В> с <S> t /V V\ 3 a <A> <В> с / \ <A> b t 6 a с <$>ab <6>с <S> 2 a <A> <B>с / \ <A> b с <5> <B> t 1 a д a c a b a c <S> a <A> <B> с /\ 1 <A> b a <r <S> <B> 1 i OK 6 a c. <S > < В ■ h <B ■ с ^ s-~. I ^/w 6 а <Д > <6 > с / \ с < 5 > < В > t г a с a b <B> с <S> t >^\\ 6 а<л><е>с /\ t <Л > b с <S> <B> 1 i e Рис. 6.З.
6.6. Деревья 163 языка. Это легко сделать, интерпретируя подстановки как шаги построения дерева. Так как построение дерева легче продемонстрировать, чем описать, мы покажем это на примере. В предыдущем разделе была приведена грамматика и вывод в ней терминальной цепочки acabac. Этот вывод можно использовать для построения соответствующего дерева вывода, изображенного на рис. 6.3, где показан каждый шаг вывода и соответствующее ему дерево. На рис. 6.3, а начальному символу (S) соответствует дерево с одной вершиной (S). Когда к символу (S) применяется правило 1, он заменяется правой частью этого правила, а именно а(А)(В)с. Соответствующий шаг построения дерева состоит в том, что добавляются вершины, помеченные символами а, (А), (В) и с, к которым ведут дуги, исходящие из вершины, помеченной заменяемым символом. Результат — на рис. 6.3, б. Вывод и соответствующее ему построение дерева продолжается вплоть до рис. 6.3, ж. Окончательный вариант дерева называется деревом вывода терминальной цепочки acabac. В дереве вывода отражено, какие правила были применены во время вывода и к каким вхождениям нетерминалов они применялись. Однако дерево не несет никакой информации о порядке применения правил, кроме одного очевидного соображения, что правила должны применяться к каждой вершине дерева раньше, чем к нетерминальным вершинам, расположенным ниже ее. Так, рассматривая дерево на рис. 6.3, ж, можно заметить, что правило 4 применялось раньше правила 3, но невозможно сказать, в каком порядке применялись правило 2 и дважды правило 6. Поскольку порядок подстановок в дереве скрыт, может быть много выводов, соответствующих одному и тому же дереву вывода. Дерево на рис. 6.3, ж, например, соответствует также выводу <S> => а <Л> <Б> c=s>a <Л> b <B> с=$>ас <S> <fi> b <B> с t ! T t 14 3 2 => ас<В> b <В> с => acab <B> с =ф acabac 1 t 6 , 6 Такой вывод называется левым (или левосторонним) выводом, так как на каждом шаге заменяется самый левый нетерминальный символ. Для каждого дерева существует единственный левый вывод, так как благодаря условию выбора самого левого нетерминала место каждой подстановки устанавливается единственным образом, а по дереву можно определить то единственное правило, которое должно применяться при этой подстановке. Для каждого дерева существует также единственный правый (или правосторонний) вывод, который получается, если всегда заменять самый правый нетерминал. Правый вывод, соответствую- 6*
1G4 Гл. 6. Контекстно-свободные грамматики щий дереву на рис. 6.3, ж, таков: <.S>=$>a<,A><,B>c=$>a<A>ac=s>a<.A'>bac t t t t 1 6 4 3 =Ф ас <S> <Б> bac =Ф ас <S> о&ас =ф acabac t t 6 2 Многие методы обработки языков рассчитаны исключительно на левые или правые выводы, так как они очень удобны для систематической обработки. В подобных случаях пишем подразумевая, что «цепочка Р может быть получена из цепочки a в результате одной самой левой подстановки», а для обозначения того, что «цепочка Р может быть получена из цепочки а применением одной самой правой подстановки», мы пишем а=Фдр В таких случаях подстановку можно восстановить по двум промежуточным цепочкам. Понятно, например, что а < Л> <Б> с=>£ а < Л> b <Б> с может быть только результатом подстановки а<Л><Б>с =ф а<Л>b<Б>с t 4 и что a<,Ay<,B>c=^Ra<Ayb<B>c может быть результатом только такой подстановки: а<Л><Б>с =ф а<Л>6<В>с t 5 Кроме того, мы будем писать а=Ф1Р если существует левый вывод Р из а, и а=Ф«Р если существует правый вывод. Цепочке языка может соответствовать более, чем одно дерево, так '/ак она может иметь разные выводы, порождающие разные деревья. На самом деле это имеет место и для нашей цепочки acabac. Кроме дерева на рис. 6.3, ж, которое еще раз изображено на рис.
6.6. Деревья 165 €.4, а, ей соответствует также дерево, изображенное на рис. 6.4, б, которое построено по такому выводу: <S> =s>a<A> <B> c=pa<Ayb <Б> с =ф ас <S> <Б> 6 <5> с t t t t 15 3 6 =Ф ас <S> а& <Б> с =ф acab <Б> с =ф acabac t t 2 6 В этом выводе даже промежуточные цепочки совпадают с промежуточными цепочками первого вывода. Однако в данном случае правило 5 применяется там, где в первом выводе используется правило 4 и соответствующие деревья явно различны. <s> <s> <в> Рис 6.4. 1 Когда одна цепочка может иметь несколько деревьев вывода, говорят, что соответствующая грамматика неоднозначна. Все сказанное можно резюмировать следующим образом: 1. Каждой цепочке, выводимой в данной КС-грамматике, соответствует одно или несколько деревьев вывода. 2. Каждому дереву соответствует один или более выводов. 3. Каждому дереву соответствует единственный правый и единственный левый выводы. 4. Если каждой цепочке, выводимой в данной КС-грамматике, соответствует единственное дерево вывода, эта грамматика называется бднозначной; в противном случае ее называют неоднозначной.
166 Гл. 6. Контекстно-свободные грамматики 6.7. Грамматика для констант языка MINI-BASIC Нередко бывает необходимо найти контекстно-свободную грамматику дл^я какого-нибудь языка, который задан неформально. Мы продемонстрируем, как это делается, строя грамматику для констант MIM-BASIC'a. Присвоим нетерминалам имена, соответствующие цепочкам, которые могут быть из них выведены. Начальный нетерминал — (константа >. В руководстве по MINI-BASIC'y описаны два вида констант: с символом Е, служащим для представления степени числа 10, и без этого символа. Константы без Е выводятся с помощью правила 1. (константа)-»- (десятичное число) где (десятичное число > порождает последовательность цифр с возможной десятичной точкой. Константы с Е выводятся по правилу 2. (константа>-v (десятичное число) Е (целое) где (десятичное число) — тот же нетерминал, что и в предыдущем: правиле, а (целое) — нетерминал, порождающий последовательность цифр, перед которой может стоять знак -f или —. Чтобы завершить построение грамматики, нам нужно добавить правила для (десятичного числа) и (целого). Начнем с (целого). 3. (целое) ->- -f (целое без знака ) 4. (целое )-*-— (целое без знака) 5. (целое)-»- (целое без знака) где (целое без знака) — нетерминал, порождающий последовательность цифр. Для (целого без знака) в грамматику вводятся следующие правила: 6. (целое без знака)-»- d(целое без знака) 7. (целое без знака )-*-d где d представляет любую цифру. Эти два правила порождают, как легко видеть, последовательности цифр. Правила для (десятичного числа) можно выразить с помощыа нетерминала (целое без знака). 8. (десятичное число) -*• (целое без знака ) 9. (десятичное число)-»- (целое без знака). 10. (десятичное число)-»- .(целое без знака) 11. (десятичное число )->- (целое без знака). (целое без знака V Правило 8 — для чисел без десятичной точки; правило 9 — для чисел с десятичной точкой после последней цифры, правило 10 — для чисел с десятичной точкой в начале, а правило 11 — для
6.8. Грамматика для S-выражений Лиспа 167 чисел, в которых цифры стоят по обе стороны от десятичной точки. В правых частях этих правил используется только нетерминал (целое без знака), правила для которого уже приведены выше. <-.константи ^> ■< целое без знака > :целое > 'целое без знака > < целое без знака> 2 < целое без знака > Рис. 6.5. Таким образом, все нетерминалы описаны, и грамматика построе- яа. Например, константу 3.1 Е — 21 можно вывести с помощью следующего левого вывода: <константа)=><десятичное число) Е<целое>=> <иелое без знака> . <целое без знака> Е <целое>=4> 3. <целое без знака) Е <целое>=> 3.1 Е<иелое>=> 3.1 Е — <иелое без знака) =£ 3.1 Е—2<целое без знака) =^> 3.1 Е —21 Соответствующее дерево вывода показано на рис. 6.5. 6.8. Грамматика для S-выражений Лиспа В качестве еще одного примера построим грамматику для S-выражений языка Лисп. В руководстве по Лиспу S-выражения определяются рекурсивно в терминах других S-выражений и атомов, которые являются аналогами идентификаторов: «S-выражение — это либо атом, либо левая скобка, за которой
168 Гл. 6. Контекстно-свободные грамматики следуют S-выражение, точка, S-выражение и правая скобка». Если АТОМ считать терминальным символом, то грамматика для S-выражений выглядит так: <S>- ATOM Эта грамматика демонстрирует два привлекательных свойства КС- грамматик: они позволяют обрабатывать вложенные скобки и <5> АТОМ АТОМ АТОМ Рис. в. в. АТОМ могут определять множества цепочек рекурсивным образом. Вот пример вывода в этой грамматике: <S> =ф «S> • <S» =>(«S> • <S» • <S» =ф ((ATOM • <S» • <S» => ((ATOM-«S>-<S»)-<S»=> ((ATOM • (ATOM • <S») • <S» =Ф ((ATOM • (ATOM • ATOM)) • <S» => ((ATOM • (ATOM • ATOM)) • ATOM) Соответствующее дерево вывода показано на рис. 6.6. 6.9. Грамматика для арифметических выражений Иногда грамматика не только определяет, какие цепочки принадлежат языку, но и отражает некоторую «структуру» языка. Рассмотрим, например, такую грамматику:
6.9. Грамматика для арифметических выражений 169 1. <£>-* <£>+ (Т) 2. <£>-»- <Г> 3. (Г)-»- <Л * (F) 4. (Г)-*- <F> 5. {F)-+ (F)\ (P) 6. (F)-+ (P) 7. </>>->(<£» 8. (/>>->/ где (£> — начальный символ, а / представляет произвольное целое число. Эта грамматика, в основе которой лежит грамматика для арифметических выражений, приведенная в официальном сообще- <Е> + <Т> <Е> + <Т><Т> <F> /\\\> \ <Т> * <F> <Р> <1 / / //\\ ! F> <F> <P> ( <F> ) / / / /к <р> </■" / / <F> <Р> <F> + <Г> <Т> <F> <F> <P> <Р> 6 Рис 6.7.
170 Гл. 6. Контекстно-свободные грамматики нии об Алголе 60, порождает все арифметические выражения Алгола 60, в которых используются целые числа и операции +, * и f. На рис. 6.7 показано дерево вывода в этой грамматике выражения 1 + 2*3+(5+6)*7 где целые числа рассматриваются как вхождения терминального символа /. В описании языка программирования должно быть указано не только то, какие цепочки принадлежат языку, но также и то, как они должны выполняться. Поэтому описание арифметических выражений включает правила для определения операндов каждой операции в выражении. Например, операндами первой операции сложения + в приведенном выше выражении являются целое число 1 и результат вычисления подвыражения 2*3. Заметим, что на рис. 6.7 подвыражение 1+2*3 выводится из одной вершины, помеченной символом (£), эта вершина имеет три потомка: (Е), порождающий операнд 1, + и (Т), порождающий подвыражение 2*3. Аналогично каждая из трех остальных операций является потомком вершины,, два других потомка которой порождают операнды этой операции. Таким образом, в дереве отражено, какие части входной цепочк» образуют подвыражения, которые нужно вычислять, и каковы операнды каждой операции. В этом смысле дерево представляет собой. «структуру» выражения. 6.10. Разные грамматики для одного и того же языка Для любого КС-языка существует бесконечное число грамматик,, порождающих этот язык. Хотя большинство из этих грамматик чересчур сложны, часто оказывается, что для описания некоторого* языка полезно иметь несколько разных грамматик. Например, грамматика с начальным символом (£), приведенная ниже, порождает тот же язык арифметических выражений, что> и грамматика, приведенная в разд. 6.9. 1. {Е)-+ (Т) (£-список> 2. (£-список>-*-+ (ГХ-Е-список) 3. (Е-список) -*- е 4. <7>-* (F)<7-список) 5. (Г-список)-*- *(Р)(Г-список) 6. (Г-список > >- е 7. (F)-+ (P)(F-cmcoK) 8. (F-список)-»- f (PXF-список) 9. (F-список > ->- е 10. </>>+((£» 11. (P)-+l
6.11. Регулярные множества как контекстно-свободные языки 171 На самом деле нетерминалы (£), (Т), (F) и (Р) порождают в •обеих грамматиках одни и те же цепочки. Во-первых, заметим, что обе грамматики содержат одинаковые .правила для нетерминала (Р). Теперь рассмотрим нетерминал (F). В обеих грамматиках (F) трактуется как {Р), за которым следует нуль или более f (P). В грамматике разд. 6.9 л применений правила 5 и последующее применение правила 6 порождают список из (п+1) нетерминалов (Р), разделенных символами f. Например, <f > => <F> f <P> =» <F> f <P> t <P> =» <P>t<P>t<P> В новой грамматике эта же цепочка порождается применением правила 7 с последующими п применениями правила 8 и применением правила 9. Например, <F> => </>> <f-список> => <Ру | <Р> </7-список> =j> <Ру t <Р> t <Р> <F-cnHCOK> —> <Р> | <Р> | <Р> Нетерминал (F-список) предназначен для порождения списка, я который входят нуль или более f {P). Правило 8 порождает \{Р), за которым следует нетерминал (F-список), порождающий остальные |(Р). Правило 9 порождает пустой список. «Структура» выражения отражена в грамматике. Правый операнд каждой операции f порождается нетерминалом (Р >, непосредственно следующим за f в правиле 8. Левый операнд каждой f выводится из цепочки символов (Р) и f, предшествующей вхождению (F-списка >, к которому применяется правило 8. Рассмотрим теперь нетерминал (Г). В обоих множествах правил (Т) трактуется как (F), за которым следует список, состоящий из нуля или более «(F). Нетерминал (Г-список) предназначен для лорождения списка, состоящего из нуля или более элементов. Правила 4, 5 и 6 порождают первый элемент списка, продолжение списка и его окончание. Нетерминал (Е) интерпретируется аналогичным образом в соответствии с правилами 1, 2 и 3. 6.11. Регулярные множества жак контекстно-свободные языки В нашей книге введены два класса языков: регулярные множества « контекстно-свободные языки. В этом разделе будет показано, что класс КС-языков мощнее. В частности, мы докажем следующий «факт: Любое регулярное множество можно описать с помощью КС-грамматики.
172 Гл. б. Контекстно-свободные грамматики Другими словами, это означает, что регулярные множества являются КС-языками. Пусть задан конечный распознаватель для некоторого регулярного^ множества. КС-грамматику, порождающую это регулярное множество, можно получить следующим образом: 1. Терминальным множеством грамматики сделать входное множество автомата. 2. Нетерминальным множеством сделать множество состояний автомата, а начальным символом — его начальное состояние. 3. Если в автомате есть переход из состояния А в состояние В по входу х, то в грамматику надо ввести правило А->хВ 4. Если А — некоторое допускающее состояние автомата, то в грамматику надо ввести правило А^е Чтобы убедиться, что действительно построена грамматика для множества цепочек, определяемого автоматом, дадим нетерминалу, который соответствует состоянию Z, такую интерпретацию: (цепочка, допускаемая автоматом, начавшим работу в состоянии Z) Тогда правило А -»- хВ, построенное на шаге 3, интерпретируется следующим образом: цепочка, допускаемая автоматом, начавшим работу в состоянии А, может представлять собой символ х с приписанной к нему цепочкой, которая допускается автоматом, начавшим работу в состоянии В. Правило А -*- е, построенное на шаге 4, интерпретируется так: цепочка, допускаемая автоматом, начавшим работу в (допускающем) состоянии А, может быть пустой. Таким образом, правила грамматики отражают процесс работы конечного автомата. Благодаря такой интерпретации, можно установить однозначное соответствие между выводами и действиями конечного автомата. В качестве примера на рис. 6.8, а показан автомат, а на рис. 6.8,6 — построенная по нему грамматика, Цепочка aba допускается автоматом, так как она вызывает такую последовательность переходов: S-+A-+A-+B
6.12. Праволинейные грамматики 173 где S — начальное, а В — допускающее состояние автомата. Соответствующий вывод получается путем применения правила, соответствующего данному переходу и последующего удаления оставшегося нетерминала применением к нему эпсилон-правила. Так, цепочке aba соответствует такой вывод: S =» а А =» аЪА =» abaB =t> aba Если, наоборот, по данному выводу нужно найти соответствующую ему последовательность переходов, то по нетерминалу и самому a b А В S В А А a S - Ь В S - е А - Ь А В -» b А В -* е 6 Рис. 6.8. правому терминалу на каждом шаге можно установить новое состояние и вход, использованный при переходе в это состояние. Так, по выводу S=$>bB=>baS=S>ba получается такая последовательность переходов: S^B^S и применение заключительного эпсилон-правила показывает, чта состояние 5 является допускающим. 6.12. Праволинейные грамматики Рассмотрим грамматику специального вида, которая содержит правила двух типов: 04>-> х(В) или (Л)->б S -» а а А -» а В В -> а S
174 Гл. 6. Контекстно-свободные грамматики где х — некоторый терминальный символ. Процедуру предыдущего раздела можно обратить, чтобы строить конечный автомат, который распознает язык, порождаемый грамматикой такого вида. А именно обратная процедура выглядит так: 1) входным множеством автомата сделать терминальное множество грамматики; 2) в качестве множества состояний автомата использовать нетерминальное множество грамматики, а в качестве начального состояния — ее начальный нетерминал; 3) если в грамматике имеется правило (А)-+х{В) то ввести в автомат переход из состояния (А ) в состояние (В) по входу х\ 4) если в грамматике есть правило (А)-+е то сделать состояние (А ) допускающим. Результатом этого построения является недетерминированный автомат с одним начальным состоянием. Процедура построения из предыдущего раздела применима к недетерминированным автоматам такого типа, хотя она была описана как процедура, применяемая к детерминированным автоматам. Применив новую процедуру к грамматике на рис. 6.8, б, получаем рис. 6.8, а. Шаги вывода, как и прежде, соответствуют переходам автомата. Знание того, что грамматики указанного специального вида порождают регулярные множества, помогает нам описать задачи, решаемые конечными автоматами, а потом по грамматикам получить автоматы о помощью данной процедуры. Например, идентификаторы Алгола 60 можно задавать следующей грамматикой с начальным нетерминалом (идентификатор): (идентификатор >-► /(буквы и цифры) (буквы и цифры>-»■ /(буквы и цифры) (буквы и цифры) ->- d(буквы и цифры) (буквы и цифры)-»-к где / и d представляют соответственно букву и цифру. Так как грамматика имеет указанный специальный вид, ясно, что порождаемый ею язык можно распознавать конечным автоматом. Применив процедуру, получаем недетерминированный автомат, изображенный на рис. 6.9. Грамматики описанного выше специального вида, конечно, не единственный класс грамматик, порождающих регулярные множества. Еще один легко определяемый класс грамматик, обладающих свойством порождать регулярные множества,— это класс так называемых «праволинейных» грамматик. Грамматика
6.12. Праволинейные грамматики 175 называется праволинейной, если правая часть каждого ее правила содержит не более одного нетерминала, причем этот нетерминал является самым правым символом правой части. Другими словами, правила праволинейной грамматики могут иметь вид {А ) ->- w (В > либо (A)-+w где (А ) и (R) — нетерминалы, а а» — терминальная цепочка. г d Идентификатор < буквы и цисрры> < буквы и цифры > < буквы и цифры > < буквы и цирры> Рис. 6.9 Примером праволинейной грамматики может служить такая грамматика с начальным символом (5): 1. 2. 3. 4. 5. 6. (S)^a(A) {S)^bc (S)^ (A) (A)-+abb(S) {А)^с(А) (А)-* г Правила 1, 3, 4 имеют вид А -»■ w(B), а правила 2 и 6 имеют вид А -*■ w. В правилах 3 и 6 цепочка w пуста. Праволинейные грамматики легко преобразуются в грамматики специального вида, которые обсуждались выше. Продемонстрируем это на нашем последнем примере. Правило 4 не имеет надлежащего вида, так как в нем перед нетерминалом стоят три терминальных символа вместо одного. Чтобы это исправить, заменим правило 4 следующими тремя правилами: (A)-*a{bbS) (bbS)^ b(bS) (bS)-+ b(S) каждое из которых имеет нужный вид. В результате вместо одного правила (A)-*abb{S) мы будем применять три: < Л> =» a <bbS> =» ab <bS> =Ф abb <5> Правило 2 не имеет надлежащего вида прежде всего потому, что в его правой части после терминальных символов нет. нетер-
176 Гл. 6. Контекстно-свободные грамматики минала. Чтобы исправить это, заменим его двумя правилами: (5> -*- fee (эпсилон) (эпсилон) -> б Первое правило, содержащее (S) в левой части, нужно преобразовать Так же, как было преобразовано правило 4. Наконец, осталось правило 3, которое не имеет нужного вида, так как в его правой части стоит один нетерминал. Чтобы исправить это, заменим это правило на правила вида (5)->- правая часть правил для (А) для всех правил, содержащих (А) в левой части. В результате получилась такая новая грамматика: 1. (S)-vaM) 2а. (5 > -v b (с эпсилон) 26. (с эпсилон > -*- с (эпсилон) 2в. (эпсилон) -»- е За. (S)-a(bbS) 36. (5>->с(Л> Зв. (S)-ve 4а. (A)-*-a(bbS) 46. (bbS)^b(bS) 4в. (bS)-+b(S) 5. (А)^с(А) 6. (А ) -> е Преобразованную грамматику теперь можно использовать для построения конечного распознавателя, изображенного на рис. 6.10. a be <S> <сэпсилон> < эпсилон>. <А> <bbS> <bS> <A>,<bbS> <bbS> Рис. <c эпсиш> <bS> <S> 6.10. <A> <эпсилон> <A> 1 0 1 1 0 0 Хотя преобразованная праволинейная грамматика идеально подходит для построения конечного распознавателя, исходная непреобразованная грамматика, по всей видимости, дает более
6.12. П'раволииейные грамматики 177 естественное описание множества цепочек, так как в ней меньше правил и нетерминалов. Иногда, особенно после применения к грамматике преобразований, грамматика содержит правила вида (А > -> (А > Они означают, что нетерминал переводится сам в себя. Эти правила бесполезны, и их можно удалить из грамматики, не изменяя порождаемого ею языка. Приемы преобразования грамматик, излагаемые в этом разделе, можно объединить в единую процедуру, которая преобразует праволинейную грамматику в грамматику специального вида, описанного в начале раздела. 1. Если имеются правила вида где w — непустая терминальная цепочка, ввести новый нетерминал, например (эпсилон >, и добавить правило (эпсилон > -*■ е Затем заменить каждое из правил вида (А > -> w правилом {А > -*- w(эпсилон) 2. Заменить каждое правило вида (А)^(ц...ап{В) для п>\ на правила ^Ау-^а^а, ... апВ> <я, ... а„В> —> a,- <ai+i ... а„В> для 1 < i < п <апВ>-+а„<В> где {а{.. .апВ) для Ki'^n — это новые нетерминалы. 3. Если в грамматике есть нетерминал (В), такой, что имеются правила вида <Л>-> (В) то, во-первых, удалить правило (В)-у (В) если таковое имеется, и, во-вторых, заменить их правилами вида (А)-* у Для всех (А > и у, таких, что существуют правила <Л>^ <В> и (В)-+у
178 Гл. 6. Контекстно-свободные грамматики Если останутся правила, правая часть которых состоит из одного нетерминала, повторить шаг 3. В результате выполнения шага 3 полностью удаляются правила, содержащие в правой части один нетерминал (В). Если после шага 3 из грамматики уже удалены правила, правая часть которых состоит из одного данного нетерминала, то при последующих применениях шага 3 к другим нетерминалам этот нетерминал не может снова появиться в качестве правой части правила. Поэтому достаточно один раз применить шаг 3 к каждому нетерминалу, являющемуся правой частью какого-нибудь правила. Иногда в результате применения этой процедуры правила дублируются. Рассмотрим, например, так>ю грамматику с начальным символом (5): 1. (S)-+ab(A) 2. (А)^ ЬЬ(А) 3. (A)-+b(S) 4. (S)^e Шаг 1 процедуры не применяется, так как в грамматике нет правил подходящего вида. Шаг 2 применяется к правилам 1 и 2, в результате получается грамматика la. (S)-*a{bA) 16. (ЬА)-+Ь(А) 2а. (А)-+Ь(ЬА) 26. (ЬА)-+Ь(А) 3. (A)-*b<S) 4. (5)->е Однако правила 16 и 26 одинаковы, и одно из них нужно выкинуть. Шаг 3 не применяется к этой грамматике, так как она уже приняла нужную форму В качестве еще одной иллюстрации применим нашу процедуру к такой грамматике с начальным символом (S): 1. (S)-*a(A) 2. (S)-+ (A) 3. <Л>— (5) 4. (А > -»■ е Шаги процедуры 1 и 2 не применяются. Применение шага 3 к не- терминалу (А > дает правила 1. (S)^a(A) 2а. (5>-* (5) 26. (S)-ve 3. Ш-+ (S) 4. (А)-* в
6.13. Еще одна грамматика для констант языка MINI-BASIC 179 Применяя шаг 3 к нетерминалу (S), мы сначала удаляем правило 2а, а затем заменяем правило 3 двумя новыми правилами. Новая грамматика такова: 1. (S)-^a(A) 26. <S>-»-e За. (А)-у а (А) 36. (А ) -»■ е 4. (А ) -* г Правила 36 и 4 совпадают, поэтому одно из них нужно удалить, в результате чего будет получена грамматика требуемого вида, состоящая из четырех правил. В силу того, что существует процедура, преобразующая праволи- нейные грамматики в грамматики указанного специального вида, мы можем утверждать следующее: Язык, порождаемый праволинейной грамматикой, является регулярным. Чтобы посмотреть, как праволинейные грамматики возникают на практике, обратимся к описаниям переменных Алгола 60, которые имеют такой вид: ТИП ИДЕНТ, ИДЕНТ, ..., ИДЕНТ где ТИП и ИДЕНТ — лексемы. Эти описания довольно компактно задаются праволинейной грамматикой: (описание) ->- ТИП ИДЕНТ (список описанных переменных) (список описанных переменных > -»- , ИДЕНТ (список описанных переменных) (список описанных переменных > -*■ г 6.13. Еще одна грамматика для констант языка MINI-BASIC Грамматика, задающая константы языка MINI-BASIC, была приведена в разд. 6.7. Сейчас мы получим еще одну грамматику путем применения процедуры из разд. 6.11 к конечному распознавателю для констант MINI-BASIC'a. Распознаватель для констант MINI-BASIC'a без знака был построен в разд. 2.14 и изображен на рис. 2.24, б. В рассматриваемом примере мы просто добавим состояние ошибки ОШ и получим тем самым распознаватель на рис. 6.11. Имена входных символов были изменены, чтобы привести их в соответствие с именами из разд. 6.7. Применение процедуры из разд. 6.11 к этой таблице переходов дает грамматику с 40 правилами вида {А )-»- х(В) и тремя правилами вида (А ) -у е. Однако более глубокий анализ показывает, что объем грамматики несколько завышен Рассмотрим правила для
180 Гл. 6. Контекстно-свободные грамматики нетерминала (ОШ). Их можно найти по строке, соответствующей состоянию (ОШ), таблицы переходов, и выглядят они так: <OIH>->d(OIII> (ОШ > -v E (ОШ > (OIII>-v . (ОШ> <ОШ>-> +(ОШ> (ОШ > -* — (ОШ > Из этих правил ясно, что нетерминал (ОШ > не может участвовать в выводе ни одной терминальной цепочки, так как любая замена нетерминала (ОШ) дает новую цепочку, содержащую этот нетерминал. На самом деле, если (ОШ) интерпрети- d ё . + - руется как (цепочка, допускаемая автоматом, начавшим работу в состоянии ОШ) то понятно, что этот нетерминал вообще не порождает ни одной терминальной цепочки, так как ОШ — это состояние ошибки, обладающее тем свойством, что, перейдя в него, автомат не допускает впоследствии ни одной цепочки. Таким образом,если нетерминал (ОШ) хоть раз появится в выводе, все последующие промежуточные цепочки должны содержать (0111) и не могут быть терминальными. Так как нетерминал (ОШ) не порождает ни одной терминальной цепочки, мы можем исключить из грамматики все правила, содержащие (ОШ) в левой или правой части. Поскольку (ОШ) не участвует в порождении цепочек языка, удаление из грамматики этого нетерминала и всех его правил никак не повлияет на язык, порождаемый грамматикой. После выбрасывания всех правил, содержащих (ОШ), число правил исходной грамматики уменьшается с 43 до 16, и грамматика принимает такой вид: <0>^d<l> <0>^.<7> <l>-*d<l> 0 1 23 4 5 6 7 ОЩ 1 1 23 6 6 6 23 ОШ ОШ 4 4 ОШ ОЩ оЩ ОШ оШ 7 23 ОШ ОШ ОШ ОШ ош ОШ ОШ ОШ ОШ 5 ош ОШ' ОШ ОШ ОШ ОШ ОШ 5 ОШ ОШ ОШ ош о 1 1 о о 1 о о Рис. 6.11.
6.14. Лишние нетерминалы 181 <1>— <1>-> <1>— <23> — <23>-> <23> — <4> — <4> — <4>- <5>^ <6>- <6> — <7>-н Е<4> . <23> ■е d<23> Е<4> е ►d<6> ► + <5> — <5> ►d<6> ►d<6> е .d<23> Чтобы грамматика более наглядно передавала вид констант язы ка MINI-BASIC, дадим нетерминалам значащие имена. Например такие: <0> = <константа> <1> = <цифры возможно с десятичной точкой и порядком) <23> = <десятичные цифры и, возможно, порядок> <4> = <целое> <5> = <целое без знака> <6> = <цифры> <7> = <десятичное целое и, возможно, порядок> 6.14. Лишние нетерминалы В предыдущем разделе мы столкнулись с примером нетерминала, который не участвует в выводе терминальных цепочек и поэтому может быть исключен из грамматики вместе со всеми правилами, в которые он входит. Нетерминалы, которые не порождают ни одной терминальной цепочки, называются бесплодными или мертвыми. В предыдущем разделе бесплодным был нетерминал (ОШ >, так как все правила с (ОШ > в левой части содержали (ОШ > и в правой части. Нетерминалы могут оказаться бесплодными и вследствие других, более тонких причин. Рассмотрим, например, такую грамматику с начальным символом (S): 1. (S)-+a(S)a 2. (S)-+b(A)d 3. (S)-+c
182 Гл. 6. Контекстно-свободные грамматики 4. (A)^c(B)d 5. (A)^a{A)d 6. (B) — d(A)f Бесплодными здесь являются нетерминалы (А) и (В). Применив правило 4 к цепочке с символом {А ), можно получить цепочку, содержащую (В), а применив к этой цепочке правило 6, можно снова получить цепочку, содержащую (А ). Но независимо от того, в каком порядке делаются подстановки, цепочка, содержащая (А > или (В), всегда переводится в цепочку, которая также содержит (А ) или (В). Поэтому правила, в которые входят (А > или (В), можно исключить из грамматики, оставив в ней лишь правила 1 и 3. Грамматика, состоящая из правил 1 и 3, порождает те же терминальные цепочки, что и грамматика с правилами 1—6. Аналогичная ситуация имеет место и в случае, когда в грамматике есть нетерминалы, которых невозможно достичь из начального нетерминала. Рассмотрим, например, такую грамматику с начальным символом (S): 1. (S)-+a{S)b 2. (S)-+c 3. (A)—b(S) 4. (А)-+а Ни одно из двух правил с начальным символом (5) в левой части правила 1 и 2 не содержит в правой части символа {А). Ни одна цепочка, выводимая из (S), не может содержать (А). Поэтому (А) не может участвовать в выводе цепочки из (S), хотя сам по себе этот нетерминал не является бесплодным. Нетерминал (А > можно удалить из грамматики, оставив лишь правила 1 и 2. Нетерминалы, которые не появляются ни в одной цепочке, выводимой из начального символа, называются недостижимыми нетерминалами. Нетерминалы, которые бесплодны или недостижимы, называются лишними (бесполезными). Если грамматика получена путем применения какого-нибудь механического способа типа процедуры из разд. 6.11, то в ней часто появляются лишние нетерминалы. Обычно надо проверять, нельзя ли упростить такую грамматику, выбросив лишние нетерминалы. Даже в грамматиках, составленных вручную, лишние нетерминалы могут возникнуть вследствие ошибок разработчика. Поэтому поиск лишних нетерминалов часто может оказаться полезным при отладке грамматики, составленной вручную. Опишем процедуру обнаружения лишних нетерминалов. Процедура состоит из двух частей: одна — для обнаружения бесплодных нетерминалов, другая — для обнаружения недостижимых нетерминалов. Сначала нужно выполнить процедуру для бесплодных нетерминалов, так как при их удалении из грамматики другие нетерминалы могут стать недостижимыми.
6.14. Лишние нетерминалы 183 Мы называем терминальный или нетерминальный символ продуктивным (живым), если из него выводится какая-нибудь терминальная цепочка (т. е. если он не является бесплодным нетерминалом). Процедура обнаружения бесплодных нетерминалов основана на следующем свойстве продуктивных символов: Свойство А: Если все символы правой части правила продуктивны, то продуктивен и символ, стоящий в ее левой части. Чтобы убедиться в правильности этого утверждения, заметим, что терминальную цепочку можно получить из символа, стоящего в левой части такого правила, применив к нему сначала это правило, а затем заменив каждый нетерминал правой части на одну из цепочек, благодаря которым он является продуктивным. Основная идея процедуры состоит в том, что список начинается нетерминалами, которые являются «заведомо продуктивными», а затем для обнаружения других продуктивных нетерминалов используется свойство А, и список пополняется. Шаги процедуры таковы: 1. Составить список нетерминалов, для которых найдется хотя бы одно правило, правая часть которого не содержит нетерминальных символов. 2. Если найдено такое правило, что все нетерминалы, стоящие в его правой части, уже занесены в список, то добавить в список нетерминал, стоящий в его левой части. 3. Если на шаге 2 список больше не пополняется новыми нетерминалами, то получен список всех продуктивных нетерминалов грамматики, а все нетерминалы, не попавшие в него, являются бесплодными. То, что список, полученный на шаге 3, содержит только продуктивные нетерминалы, объясняется тем фактом, что в него заносились только нетерминалы, продуктивные по свойству Д. Чтобы убедиться, что список содержит все продуктивные нетерминалы, заметим, что если дана последовательность правил, используемых в выводе терминальной цепочки из данного нетерминала, можно рассмотреть эту последовательность в обратном порядке и показать таким образом, что все нетерминалы, участвующие в выводе, продуктивны по свойству А. . Чтобы продемонстрировать эту процедуру в действии, рассмотрим грамматику с начальным символом (5), изображенную на рис. 6.12, а. Благодаря правилу 9 на шаге 1 в список можно поместить нетерминал (С). Затем на шаге 2 можно добавить нетерминал (Л >, воспользовавшись правилом 5. Наконец, благодаря правилу 2 в список можно добавить (S). Дальнейшие попытки применить шаг 2 оказываются безуспешными, и это говорит о том, что продуктивными нетерминалами являются (С), (А) и <S>, а ос-
184 Гл. 6. Контекстно-свободные грамматики Рис 4. 5. ра 9. 6.12. (A)^c(S)(A) (А}-*с(С)(С) <о- <с>- ^с<5> 6 тавшийся нетерминал (В > — бесплодный. Удалив все правила, связанные с (В), получаем грамматику, изображенную на рис. 6.12, б. Символ называется достижимым в грамматике, если он может появиться в какой-нибудь цепочке, выводимой из начального не- 1. (S)^a(A)(B)(S) 2. (S) ~+ Ь (С) (А) (С) d 2. <5> - Ь (С) (А) <С> d 3. {А)^Ъ{А)(В) ' 4. (A)^c(S)(A) 5. (А)-+с{С){С) 6. (В)-*Ь(А){В} 7. (B)^c(S)(B) 8. (C)-c(Sy 9. <С>-с ' а терминала (т. е. если он не является недостижимым). Процедура обнаружения недостижимых символов грамматики основана на следующем свойстве достижимых символов: Свойство Б: Если нетерминал в левой части правила является достижимым, то достижимы и все символы правой части этого правила. Это свойство выполняется, так как можно сначала вывести цепочку, содержащую символ, который является левой частью правила, и потом применить к ней это правило. Основная идея процедуры заключается в том, что список начинается нетерминалами, которые «заведомо достижимы», а затем для обнаружения других достижимых нетерминалов используется свойство Б, и список пополняется. Шаги процедуры таковы: 1. Образовать одноэлементный список, состоящий из начального нетерминала. 2. Если найдено правило, левая часть которого уже имеется в списке, то включить в список все нетерминалы, содержащиеся в правой его части. 3. Если на шаге 2 новые нетерминалы в список больше не поступают, то получен список всех достижимых нетерминалов, а нетерминалы, не попавшие в него, являются недостижимыми. Список, полученный на шаге 3, содержит только достижимые летерминалы, так как в него вносились только нетерминалы, до-
6.14. Лишние нетерминалы 185 стижимые по свойству Б. В окончательном списке должны оказаться все достижимые нетерминалы, так как, используя свойство Б и последовательность правил, благодаря которой достижим дан- 1. <5>- а (А) (В) 2. (S) - (Е) 3. (А)^ d (D)(A) А. (А)-* е 5. (В) - Ъ (Е) 6. (В) - f 7. <С>- с (А) (В) 8. <С>- d(S) <Л> 9. (С)- а 10. (D)-^ e (А) 11. <£>- f(A) 12. <£)-. g (7 Рис. 1.' <5> - я (А) (В) 2. <5> - <£> 3. (А)^ d(D)(A) 4. {А)-* е 5. <В> - *"■<£> 6. <В>- / 10. <Z>>- e (А) 11. <£>^ / (А) 12. <£>- ^ 6 6.13. ный нетерминал, можно показать, что все нетерминалы, участвующие в выводе, попадают в список. Чтобы продемонстрировать процедуру в действии, рассмотрим грамматику с начальным символом (S), изображенную на рис. 1. (S)-+ac 2. (S)^b(A) 3. (А)-*С(В)(С); 4. (B)-*a(S)(A) 5. (С)-Ь(С) 6. <С>-</ а Рис. 6.14 1. <S> —ас 5. (С)-^Ь(С) 6. <С> —</ 6 1. <S> — а с в 6.13, а. На шаге 1 мы начинаем список, помещая в него начальный символ (S). Применяя шаг 2 к правилам, содержащим (5) в левой части, а именно к правилам 1 и 2, обнаруживаем, что надо добавить в список нетерминалы (Л >, (В) и (Е>. Применяя шаг 2 к правилу 3, выясняем, что нужно добавить также нетерминал (D). При проверке остальных правил новые нетерминалы в список не заносятся,
186 Гл. 6. Контекстно-г.ппбодные грамматики и мы заключаем, что достижимыми являются нетерминалы (Ь >, {А ), (В), (D) и (Е), а оставшийся нетерминал (С) — недостижимый. Удалив правила, содержащие (С), получаем грамматику, изображенную на рис. 6.13,6 Проиллюстрируем, наконец, применение обеих процедур на примере грамматики с начальным символом (S), изображенной на ри.-. 6.14, а. Применив процедуру для бесплодных символов, мы обнаруживаем, что символы {А) и (В) являются бесплодными. Удалив правила, содержащие эти символы, получаем грамматик\ на рис. 6.14, б. Теперь, осуществив проверку на недостижимость, выясняем, что символ (С) недостижим. Удалив правила с этим символом, получаем грамматику, изображенную на рис. 6.14, «. Теперь понятно, что язык, порождаемый грамматикой на рис. 6.14, а содержит одну цепочку ас. Заметим, что нетерминал (С) стал недостижимым лишь после удаления правила 3. Таким образом, если бы мы устраняли сначала недостижимые нетерминалы, а затем бесплодные, то нам не удалось бы до конца упростить грамматику. 6.15. Грамматика MINl-BASIC'a для руководства по этому языку Руководство по языку MINI-BASIC, приведенное в приложении А, предназначено для объяснения языка тем, кто не имеет опыта работы с формальными языками. Структуры языка обьясняются в руководстве на примерах. Мы надеемся, что, рассмотрев несколько примеров, читатель сможет самостоятельно составлять конструкции того или иного вида. Слабость такого подхода в том, что читатель тем не менее не сможет составить точного представления о том, что в языке допустимо, а что нет. В помощь более опытном) пользователю полезно дополнить элементарное описание языка контекстно-свободной грамматикой, чтобы читатель, знакомый с формальными языками, мог более точно определить, какие конструкции разрешены в языке. В этом разделе мы приводим грамматику MINI-BASIC'a, которая могла бы в свою очередь послужить в качестве приложения к руководству по языку. Терминалами грамматики являются символы, которые обычно входят в программу на MINI-BASIC'e. Символ пробела не входит в терминальное множество, так как правило «пробелы игнорируются» прекрасно понимается и не будучи встроенным в грамматику. Начальный нетерминал Для начального нетерминала, называемого (список операторов), есть два правила:
в.15. Грамматика MINI-BASIC'a для руководства по этому языку 187 1. (список операторов>-> (число) (оператор) CR (список операторов ) 2. (список операторов)->- (число) END CR Символ CR означает «возврат каретки». Эти правила задают программу как список операторов, перед которыми стоят номера строк, причем последним оператором является оператор END. Рассмотрим теперь каждый из операторов. Пустой оператор Строка, которая вообще не содержит оператора. 3. (оператор) -> е Оператор присваивания 4. (оператор) -»- LET (переменная )= (выражение) GOTO-onepamop (оператор перехода) 5. (оператор) -»- GOTO (число) IF-onepamop (условный оператор) 6. (оператор) -> ^(выражение) (знак отношения) (выражение) GOTO (число) GOSUВ-оператор (оператор перехода на подпрограмму) 7. (оператор) -*- GOSUB (число) RETURN-оператор (оператор возврата) 8. (оператор)-> RETURN FOR-onepamop (заголовок цикла) 9. (оператор) -»- FOR (переменная )= (выражение) ТО (выражение) 10. (оператор) -> FOR (переменная )= (выражение) ТО (выражение) STEP (выражение) NEXT-onepamop (конец цикла) 11. (оператор)-»-NEXT (переменная ) REM-onepamop (комментарий) 12. (оператор) -> REM (литеры) Так как «литер» можно использовать очень много1), мы не будем выписывать правила для нетерминала (литеры), а заметим лишь, что он порождает произвольные цепочки, составленные из любых символов, кроме символа CR. *) Можно считать, что среди них и все русские буквы.— Прим. ред.
188 Гл. 6. Контекстно-свободные грамматики Выражения Грамматика для выражений — расширенный вариант грамматики, приведенной в разд. 6.9. Правила 13 и 14 порождают унарные операции + и —. 13. (выражение)-> + (терм) 14. (выражение)-*-— (терм) 15. (выражение)-»- (выражение) + (терм) 16. (выражение)-»- (выражение)— (терм) 17. (выражение)-*■ (терм) 18. (терм) -*■ (терм)*(множитель) 19. (терм )-> (терм)/(множитель) 20. (терм)-»- (множитель) 21. (множитель)-»- (множитель) f (первичное) 22. (множитель)-»- (первичное) 23. (первичное) -*• ((выражение >) 24. (первичное)-»- (переменная) 25. (первичное)-»- (константа без знака) Числа и константы 26. (число)-*- (цифра)(цифры) 27. (цифры) -*■ (цифра) (цифры) 28. (цифры) -*■ е Правила для нетерминала (цифра) будут приведены позже. 29. (константа без знака)-»- (число)(порядок > 30. (константа без знака)-*- (число). (цифры)(порядок) 31. (константа без знака)-»-. (число)(порядок) 32. (порядок >-»-Е + (число) 33. (порядок ) -*■ Е— (число) 34. (порядок) -*■ Е (число) 35. (порядок )-> е Переменные 36. (переменная ) -»- (буква) 37. (переменная)-»- (буква) (цифра) Другие правила Для экономии места мы будем писать несколько правил в одной строчке 38—43. (знак отношения) -»- = | О I < I <= I > | >= 44—53. (цифра)-* 0|1|2|3|4|5|6|7|8|9 54-79. (буква) -»- A|B|C|D|E|F|G|H|I|JIKIL| MINIOIPIQIRISITIUIVIWI X I Y I Z
в.15. Грамматика MINI-BASIC'a для руководства по этому языку 189 Некоторые аспекты языка MINI-BASIC не определяются этой грамматикой, например: а) никакие две строки программы на MINI-BASIC'e не могут начинаться с одного и того же номера строки; б) номера строк, используемые в операторах GOTO, GOSUB и IF, должны действительно встречаться в начале каких- нибудь строк программы; в) каждому FOR-оператору должен соответствовать NEXT- оператор с той же переменной, и эти операторы должны быть правильно вложены (если внутри циклов встречаются другие циклы). Опубликованные КС-грамматики для таких известных языков программирования, как Алгол и ПЛ/I, также дают неполное определение этих языков. Обычно язык программирования задается с помощью КС-грамматики плюс некоторые неформальные описания. Таким образом, язык программирования должен состоять из цепочек, которые порождаются этой грамматикой, и удовлетворяют дополнительным ограничениям, содержащимся в неформальном описании. Грамматика порождает все программы, принадлежащие языку, плюс некоторые дополнительные программы, которые языку не принадлежат; грамматика MINI-BASIC'a, например, порождает некоторые программы, в которых две строки имеют один и тот же номер. Свойства языка, которые в руководстве описываются грамматикой, принято называть синтаксическими или грамматическими свойствами, а свойства, которые грамматикой не описываются — семантическими свойствами. В руководстве по языку семантические свойства описываются обычно на естественном языке. Слова «синтаксический», и «семантический» не следует воспринимать здесь слишком серьезно. Если при описании какого-нибудь языка программирования используются разные грамматики, то некоторые свойства языка, которые не описываются одной грамматикой (и, следовательно, называются семантическими), могут быть описаны с помощью другой грамматики (и, следовательно, для этой грамматики будут синтаксическими)1). Примером такой ситуации может служить то, что грамматика, которая фактически используется в синтаксическом блоке нашего компилятора с языка MINI- BASIC (см. разд. 10.1 и 12.8), именно грамматически описывает требование, чтобы каждому FOR-оператору соответствовал некоторый NEXT-оператор. *) «Синтаксический» отнюдь не означает «описываемый КС-грамматикой». Так называемые контекстные условия языков программирования не описываются, как отмечено далее, никакой КС-грамматикой, но тоже являются синтаксическими.— Прим. ред.
190 Гл. 6. Контекстно-свободные грамматики Однако имеются такие аспекты языков программирования, которые в принципе не могут быть описаны контекстно-свободными грамматиками. Например, если допускаются сколь угодно длинные номера строк, то можно доказать, что ни одна КС-грамматика не порождает в точности те программы на MINI-BASIC'e, которые удовлетворяют ограничению, что никакие две строки программы не имеют одинаковых номеров. Еще одним примером является то, что КС-грамматика не может гарантировать, что порождаемые ею программы на Алголе удовлетворяют требованию, чтобы идентификаторы не описывались в одном блоке дважды. Есть еще и такие аспекты языков программирования, которые можно описать контекстно-свободной грамматикой, но при этом грамматика будет чрезмерно громоздкой. Так, можно найти грамматику, порождающую такие программы на MINI-BASIC'e, для которых выполняется требование, что каждый FOR-оператор и соответствующий ему NEXT-оператор на самом деле используют одну и ту же переменную Однако эта грамматика будет слишком громоздкой; поэтому мы решили задать это ограничение неграмматическими средствами. На практике КС-грамматики используются для описания языка программирования лишь в той степени, в какой это можно сделать компактно. При описании остальных аспектов языка обычно используются неформальные методы '). 6.16. Замечания по литературе Контекстно-свободные грамматики были впервые формализованы Хомским [1956, 1957, 1959]. Многие свойства КС-языков впервые рассмотрены в работе Бар-Хиллела, Перлеса и Шамира [1961]. Связь между регулярными множествами и праволинейными грамматиками была установлена Хомским и Миллером [1958]. Обзор КС-грамматик дан в работе Хомского [1963]. Формы Бэкуса — Наура были впервые применены для описания языка программирования в сообщении об Алголе 60 [Наур и др. 1960, 1962]. Контекстно-свободным языкам полностью посвящена математическая книга Гинзбурга [1966]. В книге Ахо и Ульмана [1972а] обсуждаются методы получения распознавателей для произвольных КС-грамматик. ') В последние годы достигнут немалый прогресс в разработке методов формального описания контекстно-зависимого синтаксиса и особенно семантики.— Прим. ред.
Упражнения 191 Упражнения 1. Найдите КС-грамматику для каждого из следующих языков: а) {1'Ю"'|я>т>0}; б) {1"0"1"10'л!п, т>0}; в) {1»0"Ч"»0"|п, т>0}; г) {l"0'}U{0",lm}. где л, m>0; д) {13п + 20"Ю0}; е) {wawr}, где ш— произвольная цепочка из нулей и единиц; ж) Все цепочки в алфавите {0, 1}, содержащие равное количество нулей и единиц; з) Все цепочки в алфавите {0, 1 }, содержащие равное количество нулей и единиц и такие, что каждая подцепочка, взятая с левого конца, содержит единиц не меньше, чем нулей; и) {1"0гаИ '!+Р>т>0}." 2. КС-грамматика порождает дерево вывода, изображенное на рисунке <S> <А > <S><B><BXA> b b а) Постройте левый вывод, соответствующий этому дереву. б) Сколько выводов соответствует этому дереву? в) Нарисуйте дерево вывода терминальной цепочки ab. 3. Опишите языки, порождаемые следующими грамматиками с начальным нетерминалом (S): а) (S)-*10<S>0 (S)^a(A) (А)-+ЫА) {А)-*а б) (S)-+(S)(S) <S>-*lC4>0 (А)-+\(А)0 (Л>-*е в) {S)-+\(A) <S>-*<B>0 С4>—1С4> МЫО (вМвЮ (в)—(О <С)-*1(С>0 <С>-*е г) {S)-+B(A)(D)C (D)-+(G)l {A)-+A(G)S (G)-+e Д) {S)-+a(S)(S) <S>—a •4. Какие из приведенных ниже цепочек можно вывести в данной грамматике с начальным нетерминалом (S)? В каждом случае постройте левый вывод, правый вывод и дерево вывода.
192 Гл. б. Контекстно-свободные грамматики (S)-+a(A)c(B) (А)-+(В)а(В) (S)^(B)d{S) <А)^а(В)с (B)-+a(S)c(A) (A)->a (В)-+с{А)(В) (Л)->6 а) аасЪ б) aababcbadcd в) aacbccb г) aacabcbcccaacdca д) aacabcbcccaacbca 5. Найдите КС-грамматику для выражений в лямбда-исчислении, описываемых следующим образом: Выражение в лямбда-исчислении — это переменная или символ К, за которым* следует переменная, а далее либо выражение, либо левая скобка, выражение, еще одно выражение и правая скобка. 6. Может ли цепочка иметь два левых вывода и лишь один правый? 7. Покажите, что если некоторый язык является контекстно-свободным, то контекстно-свободным будет и язык, который получается, если в конце каждой цепочки исходного языка приписать маркер конца, считающийся в новом языке терминальным символом. 8. Найдите КС-грамматику для двух типов условных операторов Фортрана. 9. Найдите КС-грамматику для условных операторов Кобола. 10. Нарисуйте дерево вывода следующей программы на Алголе, пользуясь стандартной грамматикой из Сообщения об Алголе (Наур и др. [1963]): begin integer Al; Al := 12; end 11. Найдите КС-грамматику для операторов Снобола. 12. Найдите КС-грамматику для регулярных выражений (Хенни [1968]), в которых используются операции +, * и конкатенация, изображаемая просто приписыванием аргументов друг к другу. 13. Опишите язык, порождаемый следующей грамматикой, и нарисуйте деревья вывода для трех цепочек этого языка. Ш)-++(р№ (r)^*{P)(R) (R)^i(p)(R) <Я)->е 14. Найдите грамматику, порождающую то же множество арифметических выражений, что и грамматика из разд. 6.9, но имеющую всего лишь один нетерминал. 15. а) Найдите КС-грамматику для логических выражений, составленных из логических переменных, констант, скобок и знаков операций отрицания (—i) дизъюнкции (v) и конъюнкции (л). Приоритеты обычные: ~1 выполняется перед л, а л — перед V. б) Добавьте к указанному языку первичные логические выражения, каждое из которых представляет собой арифметическое выражение, за которым следует знак отношения (>, ^, =, ф, <, <) и еще одно арифметическое выражение, и постройте соответствующую грамматику. 16. Найдите праволинейную грамматику для языка, распознаваемого лексическим блоком компилятора с MINI-BASIC'a. 17. Обозначим буквой L язык, распознаваемый автоматом, который изображен на рисунке
Упражнения 193 А В С В С В А В А 0 1 0 Найдите праволинейные грамматики для языков; а) L+e в) L* б) L-L г) L + 18. Рассмотрим грамматику S^Sa S^Sb S-wi а) Найдите праволинейную грамматику, которая порождает этот же язык, б) Нарисуйте дерево вывода цепочки ababb в каждой из этих грамматик. 19. Найдите праволинейные грамматики для языков и/или автоматов, описанных в следующих упр. из гл. 2. а) 1 д) 13 и) 17 б) 3 е) 14 к) 18 в) 4 ж) 15 л) 22 г) 6 з) 16 м) 23 20. Покажите, что следующие две праволинейные грамматики порождают один и тот же язык: а) (начальный нетерминал — (X)) W—0 <У)->1(К> U)-*0<V) (Y)-*l (X)-+\(Z) <Z>-*0<Z) (Y)-+Q{X) (Z)-+\{X) б) (начальный нетерминал — (А)) C4)-*0(B> (D)^O(A) C4>-*1<£> <D)->1(D> <B>-*0<i4> <D>-H3 <B)^l{F) (E)^0(C) (B)-^ (E)-yl(A) <СЫ)<0 (F)^O(A) <O-*l04> (F)-+\(B) (F)^e 21. Найдите лишние нетерминалы в следующей грамматике с начальным нетерминалом (S): (S)^a{A){B)(C) (S)^b(C){E)(S) (S)-+a{E) (A)^b{E) (A)-+(S)(C)(D) (A)^d (B)-+d(F){S) <fl>-m<fl><0 {C)-+a(E){S) (C)^b(E) (D)-+a(A)(C) <D>—d (E)-+a{C){E) <£)-^e (F)-*(A){B) (F)^a(F) Ф. Льюио и др.
194 Гл. 6. Контекстно-свободные грамматики 22. Найдите праволинейную грамматику без лишних нетерминалов для каждого из автоматов на рис. 6.15. А В С D А,В С С В.С C.D а А 1 В С 1 D Е F С Н 1 В 1 А F С И А D D G F В D Н F В F А 0 1 0 0 0 0 1 0 0 Рис. 6.15. 23. 24. 25. Каково значение каждого из следующих выражений MINI-BASIC'a? Каковы их значения в Фортране (предполагается, что символ f заменен на **)? а) -2*2 б) 2+2/2+2 в) —2*2 г) 2f2/2t2 д) 2f2f2t2 Опишите словами язык, порождаемый этой грамматикой, где роль начального символа играет (£}: (Е)МТ) (T)MT){F)* (F)^(P) Найдите грамматику для арифметических выражений, аналогичную грамматике, описанной в разд. 6.9, с той лишь разницей, что знаки унарных операций плюс и минус могут стоять перед каждым числом или переменной (например, допустима цепочка 3*—4) и интерпретируются согласно одному из следующих соглашений: а) унарные операции имеют такой же приоритет, что и бинарное сложение и вычитание (например, — 2|2=—(2;2)=— 4);
Упражнения 195 г) унарные операции всегда выполняются первыми (например, — 2f2=(—2)f2=4). 26. Нарисуйте деревья выводов, соответствующие грамматике из разд. 6.15, для таких программ на языке MINI-BASIC: a) 05 LET XI = -С * 3 * (+1 - 7) 10 END b) 15 JOB XI = 1 ТО 12 52P0RX2= 1TO10 г< let xi = хг +1 55 NEXT X? 5Ь NEXT XI 57 END 27. Используя терминальный алфавит грамматики из разд. 6.9, напишите грамматику, которая порождала бы все составленные из этих терминалов цепочки, не являющиеся арифметическими выражениями (в смысле грамматики из разд. 6.9). 28. В обычной алгебре переменные обозначаются одной буквой, и поэтому знак операции умножения часто опускается (так, 2z означает удвоение г). Найдите КС-грамматику для многочленов, составленных из чисел, однобуквенных переменных, и операций -f-, — и t, причем операция умножения подразумевается между соседними переменными, а также между стоящими рядом переменной и целым числом. 29. Опишите общие процедуры, которые по данным КС-грамматикам Gt и G2, порождающим языки L, и L2, строят КС-грамматику для: а) ицЦ б) LVL„ в) L\ 30. Найдите КС-грамматику, которая порождала бы операторы FORMAT в Фортране (за исключением холлеритовых, т. е. типа Н). 31. Найдите КС-грамматику для языка BASIC в том виде, как он описан в каком- нибудь подходящем руководстве '). Какие свойства языка не описываются вашей грамматикой? 32. Найдите КС-грамматику, порождающую S-выражения Лиспа, для представления которых используется а) точечная запись, б) списочная запись, в) Лисп — метаязык. 33. Пусть в программе на MIXI-BASIC'e можно использовать только три номера строки: 1, 2 и 3. Напишите КС-грамматику, которая порождала бы все программы на MINI-BASIC'e, удовлетворяющие требованию, что все строки начинаются с разных номеров и все номера строк, используемые в операторах GOTO, IF и QOSUB, действительно встречаются в начале каких-нибудь строк программы. 34. Как определить, содержит ли язык, порождаемый грамматикой, бесконечное число цепочек? ') См. примечание во введении на стр. 9.— Прим. ред.
196 Гл. 6. Контекстно-свободные грамматики 35. Как определить, порождает ли грамматика хоть одну терминальную цепочку? 36. Покажите, что если контекстно-свободный язык не содержит пустой цепочки, то для этого языка существует КС-грамматика, в которой нет эпсилон-правил. 37. Приведите пример грамматики, в которой нет терминальных символов, но определяемый ею язык содержит по крайней мере одну цепочку. 38. Приведите пример грамматики, такой, что каждая цепочка порождаемого ею языка имеет бесконечное число деревьев вывода. 39. Каким свойством должна обладать грамматика, чтобы каждому дереву соответствовал лишь один вывод? 40. а) Будем писать х&у, если либо х=&у, либо у=$>х. Предположим также, что * ФФ означает нуль или более применений отношения ФФ. Приведите пример КС-грамматики с начальным нетерминалом (S), для которой множество терминальных цепочек до, таких, что {S)=$>*w, не совпадает с множеством * терминальных цепочек до, таких, что (S)G$w. б) Как выглядело бы «дерево вывода», в котором используется отношение ФФ ? 41. Покажите, что язык а*-\-Ь* нельзя породить никакой КС-грамматикой с одним нетерминалом. 42. Покажите, что для любой грамматики существует константа, такая, что для каждой цепочки порождаемого грамматикой языка существует вывод, число шагов которого меньше, чем длина цепочки, умноженная на эту константу. 43. Найдите КС-грамматику, порождающую все правильно построенные формулы исчисления высказываний. 44. Постройте процедуру, с помощью которой можно определить, порождают ли две праволинейные грамматики один и тот же язык. 45. Для каждого из следующих выражений нарисуйте их деревья вывода в грамматиках из разд. 6.9 и 6.10: а) /*/+/ б) /•(/+/)
7 Синтаксически управляемые процессы обработки языков 7.1. Введение Изучая конечные автоматы, мы развили теорию, охватывающую проблемы распознавания. Причем когда конечные автоматы использовались в практических задачах, такие аспекты обработки цепочек, как выходы из распознавания и вычисление значений, решались нами с помощью переходных процедур, задаваемых в зависимости от конкретного случая. Так как почти всегда оказывалось, что нужные процедуры могут быть описаны кратко и построены просто, мы решили, что теория конечных распознавателей является адекватной теоретической базой для разработки конечных процессоров. В последующих главах мы займемся распознаванием контекстно- свободных языков автоматами с магазинной памятью. В отличие от конечного распознавателя для МП-распознавателя строить соответствующие расширения достаточно трудно, поэтому теория распознавания контекстно-свободных языков сама по себе не обеспечивает адекватной теоретической базы для построения компиляторов. Цель данной главы — заложить выходящий за границы теории распознавания теоретический фундамент обработки контекстно-свободных языков, на основе которого легко обеспечить необходимые расширения. Все методы проектирования, рассматриваемые в последующих главах, основываются на технике, в которой процесс обработки контекстно-свободного языка определяется в терминах обработки каждого отдельного правила соответствующей грамматики. Для описания процесса обработки, основанного на этой технике, обычно используется прилагательное «синтаксически управляемый». Синтаксически управляемые методы в данной книге основываются на математическом понятии «транслирующей грамматики», которое вводится в разд. 7.3, и понятии «атрибутной транслирующей грамматики», которое будет рассмотрено в разд. 7.7. 7.2. Польская запись На протяжении всей данной главы в качестве иллюстрирующего примера используется задача трансляции арифметических выражений. В этом разделе мы дадим один конкретный пример того, во что можно переводить такие выражения.
198 Гл. 7. Синтаксически управляемые процессы обработки языков Обычный метод записи арифметических выражений, описываемый грамматикой из разд. 6.9, известен под названием инфиксной записи. Однако существуют другие способы описания того, как нужно комбинировать арифметические величины. Одним из таких способов является так называемая постфиксная польская запись,. разработанная польским математиком Я. Лукасевичем. < операнд > < операнд > < операнд > < операнд > < операнд > а Ь Рис. 7.1. В постфиксной польской записи знак операции следует сразу за ее операндами. Множество польских выражений с операциями + и * можно породить при помощи грамматики (операнд)->- (операнд) (операнд)+ (операнд) -> (операнд)(операнд)* (операнд >-> / где / обозначает любую переменную. В дальнейших рассуждениях эти переменные представляются малыми латинскими буквами. Каждому выражению в инфиксной записи соответствует выражение в постфиксной польской записи. .Например, постфиксной записью для а*Ъ будет аЫ, а для а*Ь+с будет аЬ*с+. На рис. 7. 1. показано дерево вывода последней цепочки. Выражение показывает, что к операнду аЬ* (т. е. произведению а и Ь) и операнду с надо применить операцию +• Для другого примера инфиксного выражения а+Ь*с соответствующей постфиксной польской записью будет аЬс*+. Дерево вывода этой цепочки показано на рис. 7.2. Выражение состоит из операнда а, операнда be* и знака операции +. Постфиксная польская запись не содержит скобок, даже когда соответствующие инфиксные выражения должны заключаться в
7.3. Транслирующие грамматики 199 скобки. Например, инфиксное выражение (а+Ь)*с записывается как dQ-\-c*. В качестве последнего примера возьмем выражение а+Ь* (c+d)*{e+f), постфиксной польской записью для которого будет abcd+*ef+*+ <Операнд> < операнд> < операнд > < операнд > Ь с Рис. 7.2. Постфиксная польская запись обеспечивает другой язык для записи математических формул. Некоторые компиляторы имеют синтаксический блок, который буквально переводит инфиксные выражения в соответствующие польские записи. Многие другие компиляторы выполняют перевод, аналогичный переводу в польские записи. Такой перевод обсуждается далее в этой главе. 7.3. Транслирующие грамматики Допустим, что нам нужно построить процессор, получающий в качестве входа инфиксное выражение и печатающий на выходе эквивалентное выражение в постфиксной польской записи. Мы хотим далее, чтобы проектирование этого процессора основывалось на распознавателе, который каждый раз, когда должен быть выдан ■символ, вызывает процедуру печати. Хотя у нас еще нет детального плана процесса распознавания, представим себе, как мог бы ■происходить этот процесс. Предположим, например, что на входе — цепочка а+Ь*с. Действия процессора, соответствующие вводу « выводу, могли бы происходить по такому «сценарию»: : операнду
200 Гл. 7. Синтаксически управляемые процессы обработки языков ЧИТАТЬ(а) ПЕЧАТАТЬ(а) ЧИТАТЬ (+) ЧИТАТЬ(Ь) ПЕЧАТАТЬ^) ЧИТАТЬ(*) ЧИТАТЬ(с) ПЕЧАТАТЬ(с) ПЕЧАТАТЬ(*> ПЕЧАТАТЬ(Ч-). Этот сценарий правдоподобен, поскольку символы, а, Ь и с печатаются, как только они поступают на вход, а операторы печатаются сразу после того, как напечатаны оба операнда. Слово ЧИТАТЬ не должно восприниматься слишком буквально, поскольку многие автоматы не содержат операции ЧИТАТЬ. Например, примитивный МП-автомат сдвигается на входной символ, держится на нем в течение неопределенного числа переходов, а затем сдвигается с этого символа. Последовательность действий ввода и вывода можно описать, не используя слов ЧИТАТЬ и ПЕЧАТАТЬ, следующим образом: a{a}+b{b}*c{c}{*}{+). Операции ввода представлены самими входными символами, а операции вывода — символами, заключенными в скобки. Эта последовательность представляет собой пример того, что мы называем последовательностью актов х). Результатом указанных в ней операций ПЕЧАТАТЬ будет выходная цепочка, состоящая из символов, заключенных в скобки, а именно abc*+. С целью создания математической модели перевода пару скобок и заключенный в них выход будем рассматривать как единый символ, называемый символом действия. Так, приведенная выше последовательность актов содержит пять символов действия, а именно {а}, {Ь}, {с}, {*} и {+}. В этом примере символы действия рассматриваются как имена процедур, которые печатают выходные символы, заключенные в скобки. В других приложениях символы действия используются для представления более общих процедур. Приведенная выше последовательность актов просто говорит нам о том, как можно обработать одно конкретное инфиксное выражение. Для того чтобы показать, как обрабатывать все инфиксные выражения, можно описать множество или язык последовательностей актов. Наша цель — описание таких языков с помощью контекстно-свободных грамматик. Сейчас мы разработаем такое описание для рассматриваемого примера. В данном случае мы построим КС- грамматику, описывающую множество последовательностей актов для перевода инфиксной записи в постфиксную польскую запись. Обычно исходным пунктом при разработке контекстно-свободного описания языка последовательностей актов служит грамматика для входного языка, так как она описывает входную часть последовательности актов. Грамматика для инфиксных выражений (с начальным нетерминалом (£)) такова: *) Авторы различают последовательность актов (activity sequence) и последовательность действий (action sequence), в последнюю не входят входные символы, рассматриваемые как акты чтения,— Прим. ред,
7.3. Транслирующие грамматики 201 1. (£>^ Ш)+(Т) 2. (£>-> <7> 3. (Г)-»- (Т)*(Р) 4. <7>-*- (Р) 5. </»>-*-(<£» 6. (Р>->я 7. <Р>^6 8. </>>-* с Для удобства изложения грамматика содержит три конкретных имени переменных а, Ь, и с. Чтобы построить грамматику для последовательностей актов, мы просто опишем действия, соответствующие каждой правой части правил грамматики. Например, чтобы напечатать а после того, как а прочитано, лравило 6 изменится следующим образом: (Р) -> а{а). Чтобы напечатать знак сложения после того, как напечатаны оба его операнда, правило 1 заменяется на (£)-> {Е)+(Т){+}. Это новое правило можно выразить словами «обработка (£) состоит из обработки (Е), чтения +, обработки (7") и печатания +». После аналогичных изменений в других правилах новая грамматика будет такой: 1. <£>-> (Е)+(Т){ + ) 2. <£>-^ (Т) 3. <Г>-»- <Г>*<Р>{*} 4. <Т>-> (Р) 5. </>>-*«£» 6. (Р)-+а{а) 7. (P)-+b{b) • 8. (Р)-+с{с} Эта новая грамматика представляет собой то, что мы называем транслирующей грамматикой или грамматикой перевода. В силу соответствия между правилами инфиксной грамматики и правилами транслирующей грамматики вывод входной последовательности в инфиксной грамматике можно использовать для того, чтобы получить последовательность актов для этой входной последовательности с помощью транслирующей грамматики. Это делается просто путем применения соответствующих правил в соответствующих местах. Например, рассмотрим инфиксное выражение (а+Ь)*с. Левый вывод этой цепочки получается применением последовательности правил 2, 3, 4,5, 1, 2, 4, 6, 4, 7, 8. Эта же последовательность правил транслирующей грамматики, примененная к соответствующим нетерминалам (в данном случае к самым левым нетерминалам), дает вывод последовательности актов
202 Гл. 7, Синтаксически управляемые процессы обработки языков для этой входной последовательности. Соответствующие выводы имеют вид <£> => <Г> => <Г> * </>> => </>> * </>> => «£» * </>> => «£> + <Г» * <Р> =>* (а + Ь) * с <£> => <Г> => <Г> * </>> •{*} => </>> * </>> {*} =$«Е»*<Р>{*\=>«Е> + <Т>{ + })*<Р>\*\ =>*(а{а}+Ь{Ь}{ + })*с{с}{*} Некоторые более тривиальные шаги в выводе здесь не приведены- Они имеют место там, где появляется символ => *. Приведенные выше идеи мы сформулируем в виде математической модели с помощью следующих определений. Транслирующей грамматикой или грамматикой перевода называется контекстно-свободная грамматика, множество' терминальных символов которой разбито на множество входных символов и множество символов действия. Цепочки языка, определяемого транслирующей грамматикой, называются последовательностями актов. Пример транслирующей грамматики: входные символы = {а, Ь, с} символы действия = {х, у, г} нетерминальные символы = {(А), (В)} начальный нетерминал = (А > правила = {А > -> а (А )х{В) (A)-+z (В)^ (В)с (В)^Ьу Для того чтобы символы действия при чтении отличались от нетерминальных и входных символов, мы примем соглашение заключать их в фигурные скобки. При таком соглашении, взглянув на любую цепочку символов, можно сразу увидеть, какие из символов являются нетерминалами, какие — входными символами и какие — символами действий. Например, если только что приведенная грамматика была бы построена с символами действий {х}, {у} и {г} вместо х, у и г, то множество правил имело бы вид (А)^а{А){х}(В) (А)^ {г} (В>-> {В)с {В)^Ь{у} и можно легко различить вхождения терминалов, нетерминалов к символов действия.
7.4. Синтаксически управляемый перевод 203 В многих применениях, например при переводе из инфиксной записи в польскую, подразумевается, что каждый символ действия представляет процедуру, которая осуществляет выдачу символа, заключенного в скобки. Когда надо подчеркнуть, что подразумевается именно такая интерпретация, будем называть соответствующую грамматику грамматикой, транслирующей в цепочки, или грамматикой цепочечного перевода. Таким образом, термин «грамматика, транслирующая в цепочки» указывает на то, что данная тран- •слирующая грамматика предназначена для описания перевода вход- яых цепочек в выходные цепочки. В этом заключается отличие от общей интерпретации символов действия, как представляющих произвольные процедуры. 7.4. Синтаксически управляемый перевод .Математически мы рассматриваем перевод как множество пар, где первый элемент принадлежит множеству объектов, которые надо перевести, а второй элемент — множеству объектов, которые являются результатами перевода. В переводах, обсуждаемых в данной книге, первый элемент пары — это цепочка входного языка, а второй элемент — последовательность, представляющая результат перевода входной цепочки. Когда эти пары получаются с помощью некоторой грамматики, перевод иногда называют синтаксически управляемым переводом. В этом разделе мы используем понятие последовательности актов как основу одного класса синтаксически управляемых переводов. Вначале мы установим, как последовательность действий можно использовать для задания пары. Пусть дана последовательность актов, состоящая из входных символов и символов действия, мы будем использовать термин входная последовательность или входная цепочка для обозначения последовательности входных символов, полученной из последовательности актоз путем вычеркивания всех символов действия, и термин подпоследовательность действий для обозначения последовательности символов действия, полученной из последовательности актов путем вычеркивания всех входных символов. Мы говорим, что входная подпоследовательность образует пару с подпоследовательностью действий. При этом определении для последовательности актов а{а}+ +Ь{Ь}*с{с}{*}{ + }, обсуждавшейся в прошлом разделе, входная цепочка а-\-Ь*с образует пару с подпоследовательностью действий 1*){Ь){с}{»){+}.
204 Гл. 7. Синтаксически управляемые процессы обработки языков Для данной транслирующей грамматики множество пар можно получить, образуя пары из входной подпоследовательности каждой последовательности актов и подпоследовательности действий. Это множество пар называется переводом, определяемым данной транслирующей грамматикой. При таком определении перевод, определяемый грамматикой предшествующего раздела, транслирующей инфиксную запись в польскую,— это множество пар, в которых первый элемент — это инфиксное выражение, а второй — последовательность символов действия, говорящих о том, как напечатать эквивалентное выражение в польской записи. Некоторые транслирующие грамматики обладают тем свойством,, что одна и та же входная цепочка может встретиться более чем в одной последовательности актов или образовывать пары с более чем одной подпоследовательностью действий. Эта ситуация более полно обсуждается в разд. 7.11. Здесь же мы лишь отметим, что- нас интересуют в основном только те случаи, когда каждая входная подпоследовательность является частью одной последовательности актов и, следовательно, имеет только один перевод. Говоря о переводах, мы часто используем понятие в