Текст
                    СИСТЕМНОЕ
А. Ю. Молчанов
ПРОГРАММНОЕ
ОБЕСПЕЧЕНИЕ
ЛАБОРАТОРНЫЙ ПРАКТИКУМ


А. Ю. Молчанов СИСТЕМНОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ЛАБОРАТОРНЫЙ ПРАКТИКУМ 300.piter.com Издательская программа 300 лучших учебников для высшей школы в честь 300-летия Санкт-Петербурга осуществляется при поддержке Министерства образования РФ ПИТЕР Москва • Санкт-Петербург • Нижний Новгород • Воронеж Ростов-на-Дону • Екатеринбург • Самара • Новосибирск Киев * Харьков • Минск 2005
ББК 32.973.23-018я7 УДК 004.45(075) М76 Молчанов А. Ю. М76 Системное программное обеспечение. Лабораторный практикум. — СПб.: Питер, 2005. — 284 с.: ил. ISBN 5-469-00391-4 В книге рассматриваются базисные теоретические основы, необходимые для построения компиляторов, основные технологические приемы и методы их реализации. В ней приведены различные варианты заданий для выполнения лабораторного практикума по курсу «Системное программное обеспечение», а также примеры выполнения этих заданий. В каждом примере подробно рассматриваются все особенности его выполнения, как на этапе подготовки необходимой математической базы, так и на этапе программной реализации. В лабораторных работах автор обращает внимание на основные сложности, связанные с ее выполнением, а также на возможные типичные ошибки и недочеты, дает рекомендации по возможностям программной реализации, отличным от кода, приводимого в примерах. Книга ориентирована на студентов, обучающихся в технических вузах по специальностям, связанным с вычислительной техникой. Но она будет также полезна всем, чья деятельность так или иначе касается разработки программного обеспечения. ББК 32.973.23-018я7 УДК 004.45(075) Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книгй. ISBN 5-469-00391-4 © ЗАО Издательский дом «Питер», 2005
^alaHaus,^. Краткое содержание Введение................................................ 10 Лабораторная работа № 1 Организация таблиц идентификаторов .......................13 Лабораторная работа № 2 Проектирование лексического анализатора ..................39 Лабораторная работа № 3 Построение простейшего дерева вывода .....................60 Лабораторная работа № 4 Генерация и оптимизация объектного кода...................95 Курсовая работа .........................................133 Приложение 1 Функция переходов конечного автомата для лабораторной работы № 2..............................181 Приложение 2 Функция переходов конечного автомата для курсовой работы ................................... 184 Приложение 3 Тексты программных модулей для курсовой работы...........190 Приложение 4 Примеры входных и результирующих файлов для курсовой работы .....................................268 Литература ..............................................279 Алфавитный указатель.....................................282
Содержание Введение ......................................................ю От издательства ............................................12 ЛАБОРАТОРНАЯ РАБОТА № 1 Организация таблиц идентификаторов............................13 Цель работы ................................................13 Краткие теоретические сведения .............................13 Назначение таблиц идентификаторов.......................13 Принципы организации таблиц идентификаторов ............14 Простейшие методы построения таблиц идентификаторов..........................................15 Построение таблиц идентификаторов по методу бинарного дерева ....................................... 17 Хэш-функции и хэш-адресация.............................19 Хэш-адресация с рехэшированием..........................21 Хэш-адресация с использованием метода цепочек ..........23 Комбинированные способы построения таблиц идентификаторов..........................................25 Требования к выполнению работы .............................27 Порядок выполнения работы ..............................27 Требования к оформлению отчета .........................27 Основные контрольные вопросы............................28 Варианты заданий ...........................................28 Пример выполнения работы....................................29 Задание для примера.....................................29 Выбор и описание хэш-функции............................30 Описание структур данных таблиц идентификаторов.........31 Организация таблиц идентификаторов......................34 Текст программы ........................................35 Выводы по проделанной работе............................37
Содержание 7 ^aiaHa.us,^i ЛАБОРАТОРНАЯ РАБОТА № 2 Проектирование лексического анализатора ........................39 Цель работы .................................................39 Краткие теоретические сведения ............................. 39 Назначение лексического анализатора.......................39 Проблема определения границ лексем........................40 Таблица лексем и содержащаяся в ней информация ...........42 Построение лексических анализаторов (сканеров)............43 Требования к выполнению работы ..............................45 Порядок выполнения работы ................................45 Требования к оформлению отчета ...........................46 Основные контрольные вопросы..............................46 Варианты заданий ............................................46 Пример выполнения работы.....................................48 Задание для примера ......................................48 Грамматика входного языка ................................48 Описание конечного автомата для распознавания лексем входного языка.............................................49 Реализация лексического анализатора.......................53 Текст программы распознавателя............................57 Выводы по проделанной работе .............................59 ЛАБОРАТОРНАЯ РАБОТА № 3 Построение простейшего дерева вывода............................60 Цель работы ................................................ 60 Краткие теоретические сведения ..............................60 Назначение синтаксического анализатора ...................60 Проблема распознавания цепочек КС-языков..................62 Виды распознавателей для КС-языков .......................63 Построение синтаксического анализатора ...................65 Грамматики предшествования ...............................69 Алгоритм «сдвиг-свертка» для грамматик операторного предшествования............................................74 Требования к выполнению работы ..............................76 Порядок выполнения работы ................................76 Требования к оформлению отчета ...........................77 Основные контрольные вопросы..............................78 Варианты заданий ............................................78 Варианты исходных грамматик...............................78 Исходные грамматики и типы допустимых лексем .............79 Примечание ...............................................80 Пример выполнения работы.....................................80 Задание для примера...................................... 80 Построение матрицы операторного предшествования...........81 Реализация синтаксического распознавателя.................88 Текст программы распознавателя............................91 Выводы по проделанной работе .............................93
8 Содержание ЛАБОРАТОРНАЯ РАБОТА № 4 Генерация и оптимизация объектного кода ........................95 Цель работы .................................................95 Краткие теоретические сведения ..............................95 Общие принципы генерации кода.............................95 Синтаксически управляемый перевод.........................97 Способы внутреннего представления программ................99 Многоадресный код с неявно именуемым результатом (триады) .... 100 Схемы СУ-перевода ........................................101 Общие принципы оптимизации кода...........................103 Принципы оптимизации линейных участков ..................107 Свертка объектного кода .................................107 Исключение лишних операций ..............................110 Общий алгоритм генерации и оптимизации объектного кода...112 Требования к выполнению работы .............................113 Порядок выполнения работы ...............................113 Требования к оформлению отчета ..........................114 Основные контрольные вопросы................................114 Варианты заданий .......................................... 115 Пример выполнения работы....................................115 Задание для примера......................................115 Построение схем СУ-перевода..............................115 Пример генерации списка триад............................118 Реализация генератора списка триад.......................120 Текст программы генератора списка триад .................128 Выводы по проделанной работе ............................131 Курсовая работа ...............................................133 Цель работы ................................................133 Порядок выполнения работы ..................................134 Требования к содержанию пояснительной записки ..............135 Задание на курсовую работу .................................136 Варианты заданий ...........................................138 Порядок оценки результатов работы ..........................140 Рекомендации по выполнению работы...........................142 Пример выполнения курсовой работы ..........................143 Задание для примера выполнения работы ...................144 Грамматика входного языка ...............................144 Описание выбранного способа организации таблицы идентификаторов..................................146 Описание лексического анализатора .......................146 Описание синтаксического анализатора.....................150 Внутреннее представление программы и генерация кода .....158 Описание используемого метода оптимизации ...............171 Текст программы компилятора..............................173 Выводы по проделанной работе.............................179
Nalaftausliik Содержание 9 ПРИЛОЖЕНИЕ 1 Функция переходов конечного автомата для лабораторной работы № 2 ............................. 181 ПРИЛОЖЕНИЕ 2 Функция переходов конечного автомата для курсовой работы .......................................184 ПРИЛОЖЕНИЕ 3 Тексты программных модулей для курсовой работы .190 Модуль структуры данных для таблицы идентификаторов .....190 Модуль таблицы идентификаторов на основе хэш-адресации в комбинации с бинарным деревом.......................195 Модуль описания всех типов лексем ....................198 Модуль описания структуры элементов таблицы лексем ...200 Модуль заполнения таблицы лексем по исходному тексту программы ..................................203 Модуль описания матрицы предшествования и правил исходной грамматики.............................. 213 Модуль описания структур данных синтаксического анализатора и реализации алгоритма «сдвиг-свертка» ...............216 Модуль описания допустимых типов триад................223 Модуль вычисления значений триад при свертке объектного кода....................................224 Модуль описания структур данных триад....................225 Модуль, реализующий алгоритмы оптимизации списков триад.231 Модуль создания списка триад на основе дерева разбора.237 Модуль построения ассемблерного кода по списку триад .244 Модуль интерфейса с пользователем.....................255 ПРИЛОЖЕНИЕ 4 Примеры входных и результирующих файлов для курсовой работы .......................................268 Пример 1. Вычисление факториала .........................268 Пример 2. Иллюстрация работы функций оптимизации.........271 Литература ................................................279 Основная литература .....................................279 Дополнительная литература.............................. 279 Алфавитный указатель.................................... 282
Введение Эта книга является логическим продолжением и дополнением учебника «Си- стемное программное обеспечение»1, вышедшего в свет в 2003 году. Главной це- левой аудиторией книги «Системное программное обеспечение» были студенты технических вузов, обучающиеся по специальности «Вычислительные машины, комплексы, системы и сети» и родственным с ней направлениям, поэтому мате- риал книги был подобран исходя из требований стандарта этой специальности для курса «Системное программное обеспечение». Программа этого курса пре- дусматривает практические занятия в виде лабораторных работ, а также выпол- нение курсовой работы по итогам курса. Поэтому автор посчитал разумным доба- вить к сухим теоретическим выкладкам необходимый живой практический мате- риал, проиллюстрированный конкретными примерами реализации. Некоторая часть материала, касающаяся базовых теоретических основ, в этой книге перекликается с уже опубликованным материалом книги «Системное про- граммное обеспечение». Но автор посчитал необходимым кратко привести здесь только те теоретические выкладки, без Которых невозможно построить логиче- ское изложение материала. Подразумевается, что читатели уже знакомы с осно- вами курса «Системное программное обеспечение», поэтому в соответствующих местах всегда даются ссылки на литературу — в основном на базовые книги курса [1-3,7], а также на книги по курсу «Операционные системы» [3, 5, 6]. Поскольку оба курса («Системное программное обеспечение» и «Операционные системы») тесно взаимосвязаны, читателям этой книги необходимо знать их основы, чтобы понятии практически применять изложенный в книге материал (совсем недавно, в старой редакции образовательного стандарта, оба этих курса составляли единое целое [3]). Книга может оказаться полезной не только студентам, но и специалистам, чья деятельность напрямую связана с созданием средств обработки текстов и струк- турированных текстовых команд. Некоторые практические приемы, описанные в книге и проиллюстрированные в примерах программного кода, будут полезны 1 Молчанов А. Ю. Системное программное обеспечение: Учебник для вузов. — СПб.: Питер, 2003. — 396 с.
Введение 11 не только тем, кто создает или изучает трансляторы, компиляторы или любые другие распознаватели для формальных языков, но и вообще всем разработчикам программного обеспечения. Для понимания практических примеров необходимо знание языка программиро- вания Object Pascal и хотя бы общее представление о системе программирования Delphi, а также знание языка ассемблера процессоров типа Intel 80x86. В ряде слу- чаев для сравнения и понимания примеров синтаксических конструкций реко- мендуется знать язык программирования С. Соответствующие сведения можно почерпнуть в дополнительной литературе, приведенной в конце книги [13, 23- 25,28,31,32,37,39,41,44]. Все практические примеры созданы автором в системе программирования Delphi 5 на языке Object Pascal с использованием примитивных классов из библиотеки VCL. Но автор приложил все усилия, чтобы они не были привязаны ни к версии системы программирования, ни к особенностям исходного языка. Поэтому жела- ющие без проблем могут перенести их под любую версию Delphi, а при необходи- мости переписать, например на C++, для чего требуются только самые элемен- тарные знания языка. Программный код, приводимый в примерах, ни в коей мере не претендует на вы- сокую эффективность. При его создании автор в первую очередь думал об иллю- стративности кода, о его способности наглядно отражать тс теоретические посыл- ки, которые есть в книге. И тем не менее использованные методы и приемы, по мнению автора, могут служить не только примером реализации элементов ком- пилятора, но и иллюстрацией хорошего стиля программирования — но пусть об этом лучше судят сами читатели. Возможности дальнейшего совершенствования кода чаще всего специально заложены в примерах, а в тексте книги указано, в чем эти возможности заключаются. При проведении занятий преподаватели могут использовать эти моменты для дополнительных заданий по теме книги. Структура книги проста: она содержит описания четырех лабораторных работ и од- ной курсовой работы. Каждая лабораторная работа снабжена краткими теорети- ческими выкладками, имеет перечень вариантов заданий, рекомендации по выпол- нению и оформлению результатов работы, а также пример выполнения. В каждой лабораторной работе автор обращает внимание на основные сложности, связанные с ее выполнением, а также на возможные типичные ошибки и недочеты, дает реко- мендации по возможностям программной реализации, отличным от кода, приводи- мого в примерах. Все лабораторные работы связаны с реализацией составных частей компилятора. Первая работа посвящена организации таблиц идентификаторов, вторая — созда- нию лексического анализатора, третья — созданию синтаксического анализатора и четвертая — генерации и оптимизации результирующего кода. Работы имеют раз- ную сложность выполнения: по мнению автора, первые две работы элементарно просты, третья — более сложная и, наконец, четвертая имеет максимальную слож- ность. Это следует учитывать преподавателям при планировании выполнения ра- бот и обучающимся при их выполнении. Кроме того, все четыре работы взаимосвя- заны — каждая последующая работа использует материал предыдущей, поэтому для обучающихся желательно иметь один номер варианта на выполнение всех работ
12 Введение (взаимосвязь работ и преимущества такого подхода наглядно проиллюстрированы в примерах их выполнения). Курсовая работа предусматривает создание простейшего компилятора для задан- ного входного языка. Она основана на том же теоретическом материале, что и ла- бораторные работы, но требует комплексного подхода к освоению материала и к выполнению задания. Кроме того, в курсовой работе обучающимся предос- тавляется большая самостоятельность в выборе методов и приемов реализации задания, чем в лабораторных работах. Практическая направленность данной книги требует использования значитель- ного объема программного кода для реализации и иллюстрации выполняемых примеров в лабораторных работах и курсовой работе. К сожалению, из-за огра- ничений по объему нет возможности включить весь программный код в книгу. Поэтому автор счел необходимым привести в книге только программный код, свя- занный с курсовой работой (часть которого используется также и в лаборатор- ных работах). Остальной программный код можно найти на веб-сайте издатель- ства «Питер». Приводимый в книге практический материал не претендует на полноту охвата всего курса «Системное программное обеспечение». Автор считает необходимым дополнить его работами по программированию параллельных взаимодействую- щих процессов [3, 5], а также методами разработки программного обеспечения в распределенных системах (по технологиям построения систем «клиент-сервер» и многоуровневой архитектуре [7]). Автор надеется, что ему удастся в ближай- шее время подготовить соответствующий материал. От издательства Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Подробную информацию о наших книгах вы найдете па веб-сайте издательства: http://www.piter.com.
ЛАБОРАТОРНАЯ РАБОТА № 1 Организация таблиц идентификаторов Цель работы Цель работы^ изучить основные методы организации таблиц идентификаторов, получить представление о преимуществах и недостатках, присущих различным методам организации таблиц идентификаторов. Для выполнения лабораторной работы требуется написать программу, которая получает на входе набор идентификаторов, организует таблицы идентификато- ров с помощью заданных методов, позволяет осуществить многократный поиск произвольного идентификатора в таблицах и сравнить эффективность методов организации таблиц. Список идентификаторов считать заданным в виде тексто- вого файла. Длина идентификаторов ограничена 32 символами. Краткие теоретические сведения Назначение таблиц идентификаторов При выполнении семантического анализа, генерации кода и оптимизации ре- зультирующей программы компилятор должен оперировать характеристиками основных элементов исходной программы — переменных, констант, функций и других лексических единиц входного языка. Эти характеристики могут быть получены компилятором на этапе синтаксического анализа входной програм- мы (чаще всего при анализе структуры блоков описаний переменных и кон- стант), а также дополнены на этапе подготовки к генерации кода (например при распределении памяти).
14 Лабораторная работа № 1 • Организация таблиц идентификаторов Набор характеристик, соответствующий каждому элементу исходной программы, зависит от типа этого элемента, от его смысла (семантики) и, соответственно, от той роли, которую он исполняет в исходной и результирующей программах. В каж- дом конкретном случае этот набор характеристик может быть свой в зависимости от синтаксиса и семантики входного языка, от архитектуры целевой вычислитель- ной системы и от структуры компилятора. Но есть типовые характеристики, ко- торые чаще всего присущи тем или иным элементам исходной программы. На- пример для переменной — это ее тип и адрес ячейки памяти, для константы — ее значение, для функции — количество и типы формальных аргументов, тип воз- вращаемого результата, адрес вызова кода функции. Более подробную информа- цию о характеристиках элементов исходной программы, их анализе и использо- вании можно найти в [1,3,7]. Главной характеристикой любого элемента исходной программы является его имя. Именно с именами переменных, констант, функций и других элементов входного языка оперирует разработчик программы — поэтому и компилятор должен уметь анализировать эти элементы по их именам. Имя каждого элемента должно быть уникальным. Многие современные языки программирования допускают совпадения (неуннкальность) имен переменных и функций в зависимости от их области видимости и других условий исходной программы. В этом случае уникальность имен должен обеспечивать сам компи- лятор — о том, как решается эта проблема, можно узнать в [1-3, 7], здесь же бу- дем считать, что имена элементов исходной программы всегда являются уникаль- ными. Таким образом, задача компилятора заключается в том, чтобы хранить некото- рую информацию, связанную с каждым элементом исходной программы, и иметь доступ к этой информации по имени элемента. Для решения этой задачи компи- лятор организует специальные хранилища данных, называемые таблицами иден- тификаторов, или таблицами символов. Таблица идентификаторов состоит из набора полей данных (записей), каждое из которых может соответствовать одно- му элементу исходной программы. Запись содержит всю необходимую компиля- тору информацию о данном элементе и может пополняться по мере работы ком- пилятора. Количество записей зависит от способа организации таблицы иденти- фикаторов, но в любом случае их не может быть меньше, чем элементов в исходной программе. В принципе, компилятор может работать не с одной, а с несколькими таблицами идентификаторов — их количество и структура зависят от реализации компилятора [1, 2]. Принципы организации таблиц идентификаторов Компилятор пополняет записи в таблице идентификаторов по мере анализа ис- ходной программы и обнаружения в ней новых элементов, требующих размеще- ния в таблице. Поиск информации в таблице выполняется всякий раз, когда ком- пилятору необходимы сведения о том или ином элементе программы. Причем следует заметить, что поиск элемента в таблице будет выполняться компилято-
^alaHaus^k Краткие теоретические сведения 15 ром существенно чаще, чем помещение в нее новых элементов. Так происходит потому, что описания новых элементов в исходной программе, как правило, встре- чаются гораздо реже, чем эти элементы используются. Кроме того, каждому до- бавлению элемента в таблицу идентификаторов в любом случае будет предше- ствовать операция поиска — чтобы убедиться, что такого элемента в таблице нет. На каждую операцию поиска элемента в таблице компилятор будет затрачивать время, и поскольку количество элементов в исходной программе велико (от еди- ниц до сотен тысяч в зависимости от объема программы), это время будет суще- ственно влиять на общее время компиляции. Поэтому таблицы идентификаторов должны быть организованы таким образом, чтобы компилятор имел возможность максимально быстро выполнять поиск нужной ему записи таблицы по имени эле- мента, с которым связана эта запись. Можно выделить следующие способы организации таблиц идентификаторов: □ простые и упорядоченные списки; □ бинарное дерево; □ хэш-адресация с рехэшированием; □ хэш-адресация по методу цепочек; □ комбинация хэш-адресации со списком или бинарным деревом. Далее будет дано краткое описание всех вышеперечисленных способов органи- зации таблиц идентификаторов. Более подробную информацию можно найти в [3, 7]. Простейшие методы построения таблиц идентификаторов В простейшем случае таблица идентификаторов представляет собой линейный неупорядоченный список, пли массив, каждая ячейка которого содержит данные о соответствующем элементе таблицы. Размещение новых элементов в такой таб- лице выполняется путем записи информации в очередную ячейку массива или списка по мере обнаружения новых элементов в исходной программе. Поиск нужного элемента в таблице будет в этом случае выполняться путем по- следовательного перебора всех элементов и сравнения их имени с именем иско- мого элемента, пока не будет найден элемент с таким же именем. Тогда если за единицу времени принять время, затрачиваемое компилятором на сравнение двух строк (в современных вычислительных системах такое сравнение чаще всего вы- полняется одной командой), то для таблицы, содержащей N элементов, в среднем будет выполнено N/2 сравнений. Время, требуемое на добавление нового элемента в таблицу (Тд), не зависит от чис- ла элементов в таблице (N). Но если У велико, то поиск потребует значительных затрат времени. Время поиска (Тп) в такой таблице можно оценить как Ти = O(N). Поскольку именно поиск в таблице идентификаторов является наиболее часто вы- полняемой компилятором операцией, такой способ организации таблиц иденти-
16 Лабораторная работа № 1 • Организация таблиц идентификаторов фикаторов является неэффективным. Он применим только для самых простых ком- пиляторов, работающих с небольшими программами. Поиск может быть выполнен более эффективно, если элементы таблицы отсор- тированы (упорядочены) естественным образом. Поскольку поиск осуществля- ется по имени, наиболее естественным решением будет расположить элементы таблицы в прямом или обратном алфавитном порядке. Эффективным методом поиска в упорядоченном списке из Аэлементов является бинарный, или логариф- мический-, поиск. Алгоритм логарифмического поиска заключается в следующем: искомый символ сравнивается с элементом (А + 1 )/2 в середине таблицы; если этот элемент не является искомым, то мы должны просмотреть только блок элементов, пронуме- рованных от 1 до (А + 1 )/2 - 1, или блок элементов от (А + 1)/2 + 1 до Ав зависи- мости от того, меньше или больше искомый элемент того, с которым его сравни- ли. Затем процесс повторяется над нужным блоком в два раза меньшего размера. Так продолжается до тех пор, пока либо искомый элемент не будет найден, либо алгоритм не дойдет до очередного блока, содержащего один или два элемента (с которыми можно выполнить прямое сравнение искомого элемента). Так как на каждом шаге число элементов, которые могут содержать искомый элемент, сокращается в два раза, максимальное число сравнений равно 1 + log2 N. Тогда время поиска элемента в таблице идентификаторов можно оценить как Тн = O(log2 А). Для сравнения: при А = 128 бинарный поиск требует самое боль- шее 8 сравнений, а поиск в неупорядоченной таблице — в среднем 64 сравне- ния. Метод называют «бинарным поиском», поскольку на каждом шаге объем рассматриваемой информации сокращается в два раза, а «логарифмическим» — поскольку время, затрачиваемое на поиск нужного элемента в массиве, имеет логарифмическую зависимость от общего количества элементов в нем. Недостатком логарифмического поиска является требование упорядочивания таблицы идентификаторов. Так как массив информации, в котором выполняется поиск, должен быть упорядочен, время его заполнения уже будет зависеть от чис- ла элементов в массиве. Таблица идентификаторов зачастую просматривается компилятором еще до того, как она заполнена, поэтому требуется, чтобы условие упорядоченности выполнялось на всех этапах обращения к ней. Следовательно, для построения такой таблицы можно пользоваться только алгоритмом прямого упорядоченного включения элементов. Если пользоваться стандартными алгоритмами, применяемыми для организации упорядоченных массивов данных, то среднее время, необходимое на помещение всех элементов в таблицу, можно оценить следующим образом: Г = O(Alog2 А) + &-О(А2). Здесь k— некоторый коэффициент, отражающий соотношение между времена- ми, затрачиваемыми компьютером на выполнение операции сравнения и опера- ции переноса данных. При организации логарифмического поиска в таблице идентификаторов обеспе- чивается существенное сокращение времени поиска нужного элемента за счет увеличения времени на помещение нового элемента в таблицу. Поскольку добав-
^aialLausj^i Краткие теоретические сведения 17 ление новых элементов в таблицу идентификаторов происходит существенно реже, чем обращение к ним, этот метод следует признать более эффективным, чем метод организации неупорядоченной таблицы. Однако в реальных компиляторах этот метод непосредственно также не используется, поскольку существуют более эффективные методы. Построение таблиц идентификаторов по методу бинарного дерева Можно сократить время поиска искомого элемента в таблице идентификаторов, не увеличивая значительно время, необходимое на ее заполнение. Для этого надо отказаться от организации таблицы в виде непрерывного массива данных. Существует метод построения таблиц, при котором таблица имеет форму бинар- ного дерева. Каждый узел дерева представляет собой элемент таблицы, причем корневым узлом становится первый элемент, встреченный компилятором при заполнении таблицы. Дерево называется бинарным, так как каждая вершина в нем может иметь не более двух ветвей. Для определенности будем называть две ветви «правая» и «левая». Рассмотрим алгоритм заполнения бинарного дерева. Будем считать, что алгоритм работаете потоком входных данных, содержащим идентификаторы. Первый иден- тификатор, как уже было сказано, помещается в вершину дерева. Все дальнейшие идентификаторы попадают в дерево по следующему алгоритму: 1. Выбрать очередной идентификатор из входного потока данных. Если очеред- ного идентификатора нет, то построение дерева закончено. 2. Сделать текущим узлом дерева корневую вершину. 3. Сравнить имя очередного идентификатора с именем идентификатора, содер- жащегося в текущем узле дерева. 4. Если имя очередного идентификатора меньше, то перейти к шагу 5, если рав- но — прекратить выполнение алгоритма (двух одинаковых идентификаторов быть не должно!), иначе — перейти к шагу 7. 5. Если у текущего узла существует левая вершина, то сделать ее текущим узлом и вернуться к шагу 3, иначе — перейти к шагу 6. 6. Создать новую вершину, поместить в нее информацию об очередном иденти- фикаторе, сделать эту новую вершину левой вершиной текущего узла и вер- нуться к шагу 1. 7. Если у текущего узла существует правая вершина, то сделать ее текущим уз- лом и вернуться к шагу 3, иначе — перейти к шагу 8. 8. Создать новую вершину, поместить в нее информацию об очередном иденти- фикаторе, сделать эту новую вершину правой вершиной текущего узла и вер- нуться к шагу 1. Рассмотрим в качестве примера последовательность идентификаторов Ga, DI, М22, Е, А12, ВС, F. На рис. 1.1 проиллюстрирован весь процесс построения бинарного де- рева для этой последовательности идентификаторов.
18 Лабораторная работа № 1 • Организация таблиц идентификаторов Рис. 1.1. Заполнение бинарного дерева для последовательности идентификаторов Ga, D1, М22, Е, А12, ВС, F Поиск элемента в дереве выполняется по алгоритму, схожему с алгоритмом за- полнения дерева: 1. Сделать текущим узлом дерева корневую вершину. 2. Сравнить имя искомого идентификатора с именем идентификатора, содержа- щимся в текущем узле дерева. 3. Если имена совпадают, то искомый идентификатор найден, алгоритм завер- шается, иначе надо перейти к шагу 4. 4. Если имя очередного идентификатора меньше, то перейти к шагу 5, иначе — перейти к шагу 6. 5. Если у текущего узла существует левая вершина, то сделать ее текущим узлом и вернуться к шагу 2, иначе — искомый идентификатор не найден, алгоритм завершается. 6. Если у текущего узла существует правая вершина, то сделать ее текущим уз- лом и вернуться к шагу 2, иначе — искомый идентификатор не найден, алго- ритм завершается. Для данного метода число требуемых сравнений и форма получившегося дерева зависят от того порядка, в котором поступают идентификаторы. Например, если в рассмотренном выше примере вместо последовательности идентификаторов Ga,
Краткие теоретические сведения 19 DI, М22, Е, А12, ВС, F взять последовательность А12, ВС, DI, Е, F, Ga, М22, то дерево выродит- ся в упорядоченный однонаправленный связный список. Эта особенность являет- ся недостатком данного метода организации таблиц идентификаторов. Другими недостатками метода являются: необходимость хранить две дополнительные ссыл- ки на левую и правую ветви в каждом элементе дерева и работа с динамическим выделением памяти при построении дерева. Если предположить, что последовательность идентификаторов в исходной про- грамме является статистически неупорядоченной (что в целом соответствует дей- ствительности), то можно считать, что построенное бинарное дерево будет невы- рожденным. Тогда среднее время на заполнение дерева (Т:{) и на поиск элемента в нем (Ти) можно оценить следующим образом [3, 7]: Т,~ XOClo^JV); r„-0(log2W). Несмотря на указанные недостатки, метод бинарного дерева является довольно удачным механизмом для организации таблиц идентификаторов. Он нашел свое применение в ряде компиляторов. Иногда компиляторы строят несколько различ- ных деревьев для идентификаторов разных типов и разной длины [1, 2,3, 7]. Хэш-функции и хэш-адресация В реальных исходных программах количество идентификаторов столь велико, что даже логарифмическую зависимость времени поиска от их числа нельзя признать удовлетворительной. Необходимы более эффективные методы поиска информа- ции в таблице идентификаторов. Лучших результатов можно достичь, если при- менить методы, связанные с использованием хэш-функций и хэш-адресации. Хэш-функцией F называется некоторое отображение множества входных элемен- тов R на множество целых неотрицательных чисел Z: F(r) = n,re R, п g Z. Сам тер- мин «хэш-функция» происходит от английского термина «hash function» (hash — «мешать», «смешивать», «путать»). Множество допустимых входных элементов R называется областью определе- ния хэш-функции. Множеством значений хэш-функции F называется подмно- жество М из множества целых неотрицательных чисел Z: М с Z, содержащее все возможные значения, возвращаемые функцией F: VrIR: F(r) е М и \fm G М: Зг g R: F(r) = m. Процесс отображения области определения хэш-функции на множество значений называется хэшированием. При работе с таблицей идентификаторов хэш-функция должна выполнять ото- бражение имен идентификаторов на множество целых неотрицательных чисел. Областью определения хэш-функции будет множество всех возможных имен иден- тификаторов. Хэш-адресация заключается в использовании значения, возвращаемого хэш-функ- цией, в качестве адреса ячейки из некоторого массива данных. Тогда размер масси- ва данных должен соответствовать области значений используемой хэш-функции.
20 Лабораторная работа № 1 • Организация таблиц идентификаторов Следовательно, в реальном компиляторе область значений хэш-функции никак не должна превышать размер доступного адресного пространства компьютера. Метод организации таблиц идентификаторов, основанный на использовании хэш- адресации, заключается в помещении каждого элемента таблицы в ячейку, адрес которой возвращает хэш-функция, вычисленная для этого элемента. Тогда в иде- альном случае для помещения любого элемента в таблицу идентификаторов достаточно только вычислить его хэш-функцию и обратиться к нужной ячейке массива данных. Для поиска элемента в таблице также необходимо вычислить хэш- функцию для искомого элемента и проверить, не является ли заданная ею ячейка массива пустой (если она не пуста — элемент найден, если пуста — не найден). Первоначально таблица идентификаторов должна быть заполнена информацией, которая позволила бы говорить о том, что все се ячейки являются пустыми. Этот метод весьма эффективен, поскольку как время размещения элемента в таб- лице, так и время его поиска определяются только временем, затрачиваемым на вычисление хэш-функции, которое в общем случае несопоставимо меньше вре- мени, необходимого для многократных сравнений элементов таблицы. Метод имеет два очевидных недостатка. Первый из них — неэффективное ис- пользование объема памяти под таблицу идентификаторов: размер массива для ее хранения должен соответствовать всей области значений хэш-функции, в то время как реально хранимых в таблице идентификаторов может быть существен- но меньше. Второй недостаток — необходимость соответствующего разумного выбора хэш-функции. Этот недостаток является настолько существенным, что не позволяет непосредственно использовать хэш-адресацию для организации таб- лиц идентификаторов. Проблема выбора хэш-функции не имеет универсального решения. Хэширова- ние обычно происходит за счет выполнения над цепочкой символов некоторых простых арифметических и логических операций. Самой простой хэш-функцией для символа является код внутреннего представления в компьютере литеры сим- вола. Эту хэш-функцию можно использовать и для цепочки символов, выбирая первый символ в цепочке. Очевидно, что такая примитивная хэш-функция будет неудовлетворительной: при ее использовании возникнет проблема — двум различным идентификаторам, на- чинающимся с одной и той же буквы, будет соответствовать одно и то же значе- ние хэш-функции. Тогда при хэш-адресации в одну и ту же ячейку таблицы иден- тификаторов должны быть помещены два различных идентификатора, что явно невозможно. Такая ситуация, когда двум или более идентификаторам соответ- ствует одно и то же значение хэш-функции, называется коллизией. Естественно, что хэш-функция, допускающая коллизии, не может быть исполь- зована для хэш-адресации в таблице идентификаторов. Причем достаточно полу- чить хотя бы один случай коллизии на всем множестве идентификаторов, чтобы такой хэш-функцией нельзя было пользоваться. Но возможно ли построить хэш- функцию, которая бы полностью исключала возникновение коллизий? Для полного исключения коллизий хэш-функция должна быть взаимно однознач- ной: каждому элементу из области определения хэш-функции должно соответство- вать одно значение из ее множества значений, и наоборот — каждому значению из
Краткие теоретические сведения 21 множества значений этой функции должен соответствовать только один элемент из ее области определения. Тогда любым двум произвольным элементам из облас- ти определения хэш-функции будут всегда соответствовать два различных ее зна- чения. Теоретически для идентификаторов такую хэш-функцию построить можно, так как и область определения хэш-функции (все возможные имена идентифика- торов), и область ее значений (целые неотрицательные числа) являются бесконеч- ными счетными множествами, поэтому можно организовать взаимно однозначное отображение одного множества на другое. Но на практике существует ограничение, делающее создание взаимно однознач- ной хэш-функции для идентификаторов невозможным. Дело в том, что в реаль- ности область значений любой хэш-функции ограничена размером доступного адресного пространства компьютера. Множество адресов любого компьютера с традиционной архитектурой может быть велико, но всегда конечно, то есть огра- ничено. Организовать взаимно однозначное отображение бесконечного множе- ства на конечное даже теоретически невозможно. Можно, конечно, учесть, что длина принимаемой во внимание части имени идентификатора в реальных ком- пиляторах на практике также ограничена — обычно она лежит в пределах от 32 до 128 символов (то есть и область определения хэш-функции конечна). Но и тогда количество элементов в конечном множестве, составляющем область определе- ния хэш-функции, будет превышать их количество в конечном множестве облас- ти ее значений (количество всех возможных идентификаторов больше количе- ства допустимых адресов в современных компьютерах). Таким образом, создать взаимно однозначную хэш-функцию на практике невозможно. Следовательно, невозможно избежать возникновения коллизий. Поэтому нельзя организовать таблицу идентификаторов непосредственно на ос- нове одной только хэш-адресации. Но существует методы, позволяющие исполь- зовать хэш-функции для организации таблиц идентификаторов даже при нали- чии коллизий. Хэш-адресация с рехэшированием Для решения проблемы коллизии можно использовать много способов. Одним из них является метод рехэширования (или расстановки). Согласно этому методу, если для элемента А адрес п0 = h(A), вычисленный с помощью хэш-функции h, указывает на уже занятую ячейку, то необходимо вычислить значение функции = h{(A) и проверить занятость ячейки по адресу nv Если и она занята, то вы- числяется значение Л2(Л), и так до тех пор, пока либо не будет найдена свободная ячейка, либо очередное значение Л/Л) не совпадет с/г(Л). В последнем случае считается, что таблица идентификаторов заполнена и места в ней больше нет — выдается информация об ошибке размещения идентификатора в таблице. Тогда поиск элемента А в таблице идентификаторов, организованной таким об- разом, будет выполняться по следующему алгоритму: 1. Вычислить значение хэш-функции п = h(A) для искомого элемента А. 2. Если ячейка по адресу п пустая, то элемент не найден, алгоритм завершен, иначе необходимо сравнить имя элемента в ячейке п с именем искомого эле-
22 Лабораторная работа № 1 • Организация таблиц идентификаторов мента Л. Если они совпадают, то элемент найден и алгоритм завершен, иначе i := 1 и перейти к шагу 3. 3. Вычислить п. = h^A). Если ячейка по адресу пустая или п = п., то элемент не найден и алгоритм завершен, иначе — сравнить имя элемента в ячейке и,. с име- нем искомого элемента А. Если они совпадают, то элемент найден и алгоритм завершен, иначе г := i + 1 и повторить шаг 3. Алгоритмы размещения и поиска элемента схожи по выполняемым операциям. Поэтому они будут иметь одинаковые оценки времени, необходимого для их вы- полнения. При такой организации таблиц идентификаторов в случае возникновения колли- зии алгоритм помещает элементы в пустые ячейки таблицы, выбирая их опреде- ленным образом. При этом элементы могут попадать в ячейки с адресами, кото- рые потом будут совпадать со значениями хэш-функции, что приведет к возник- новению новых, дополнительных коллизий. Таким образом, количество операций, необходимых для поиска или размещения в таблице элемента, зависит от запол- ненности таблицы. Для организации таблицы идентификаторов по методу рехэширования необходи- мо определить все хэш-функции hi для всех i. Чаще всего функции ht определяют как некоторые модификации хэш-функции h. Например, самым простым методом вычисления функции Л/А) является ее организация в виде Л/А) = (Л(А) + р) mod Nm, где р; — некоторое вычисляемое целое число, aNm- максимальное значе- ние из области значений хэш-функции h. В свою очередь, самым простым подхо- дом здесь будет положить pf = i. Тогда получаем формулу Л (А) = (Л(А) + i) mod Nm. В этом случае при совпадении значений хэш-функции для каких-либо элементов поиск свободной ячейки в таблице начинается последовательно от текущей по- зиции, заданной хэш-функцией h(A). Этот способ нельзя признать особенно удачным: при совпадении хэпьадресов элементы в таблице начинают группироваться вокруг них, что увеличивает чис- ло необходимых сравнений при поиске и размещении. Но даже такой примитив- ный метод рехэширования является достаточно эффективным средством органи- зации таблиц идентификаторов при неполном заполнении таблицы. Среднее время на помещение одного элемента в таблицу и на поиск элемента в таблице можно снизить, если применить более совершенный метод рехэширо- вания. Одним из таких методов является использование в качестве р. для функ- ции h^A) = (А(А) + р;) mod Nm последовательности псевдослучайных целых чи- сел рр р2, ..., pk. При хорошем выборе генератора псевдослучайных чисел длина последовательности k = Nm. Существуют и другие методы организации функций рехэширования А;(А), осно- ванные на квадратичных вычислениях или, например, на вычислении произведе- ния по формуле: h^A) = (A(A)^z) mod ЛГт, где N'm — ближайшее простое число, мень- шее Nm. В целом рехэширование позволяет добиться неплохих результатов для эф- фективного поиска элемента в таблице (лучших, чем бинарный поиск и бинарное дерево), но эффективность метода сильно зависит от заполненности таблицы иден- тификаторов и качества используемой хэш-функции — чем реже возникают кол-
Краткие теоретические сведения 23 лизни, тем выше эффективность метода. Требование неполного заполнения табли- цы ведет к неэффективному использованию объема доступной памяти. Оценки времени размещения и поиска элемента в таблицах идентификаторов при использовании различных методов рехэширования можно найти в [1, 3,7]. Хэш-адресация с использованием метода цепочек Неполное заполнение таблицы идентификаторов при применении рехэширова- ния ведет к неэффективному использованию всего объема памяти, доступного компилятору. Причем объем неиспользуемой памяти будет тем выше, чем боль- ше информации хранится для каждого идентификатора. Этого недостатка можно избежать, если дополнить таблицу идентификаторов некоторой промежуточной хэш-таблицей. В ячейках хэш-таблицы может храниться либо пустое значение, либо значение указателя на некоторую область памяти из основной таблицы идентификаторов. Тогда хэш-функция вычисляет адрес, по которому происходит обращение снача- ла к хэш-таблице, а потом уже через нее по найденному адресу — к самой таблице идентификаторов. Если соответствующая ячейка таблицы идентификаторов пу- ста, то ячейка хэш-таблицы будет содержать пустое значение. Тогда вовсе не обя- зательно иметь в самой таблице идентификаторов ячейку для каждого возмож- ного значения хэш-функции — таблицу можно сделать динамической, так чтобы ее объем рос по мере заполнения (первоначально таблица идентификаторов не содержит ни одной ячейки, а все ячейки хэш-таблицы имеют пустое значение). Такой подход позволяет добиться двух положительных результатов: во-первых, нет необходимости заполнять пустыми значениями таблицу идентификаторов — это можно сделать только для хэш-таблицы; во-вторых, каждому идентификатору бу- дет соответствовать строго одна ячейка в таблице идентификаторов. Пустые ячей- ки в таком случае будут только в хэш-таблице, и объем неиспользуемой памяти не будет зависеть от объема информации, хранимой для каждого идентификатора, — для каждого значения хэш-функции будет расходоваться только память, необходи- мая для хранения одного указателя на основную таблицу идентификаторов. На основе этой схемы можно реализовать еще один способ организации таблиц идентификаторов с помощью хэш-функции, называемый методом цепочек. В этом случае в таблицу идентификаторов для каждого элемента добавляется еще одно поле, в котором может содержаться ссылка на любой элемент таблицы. Пер- воначально это поле всегда пустое (никуда не указывает). Также необходимо иметь одну специальную переменную, которая всегда указывает на первую свободную ячейку основной таблицы идентификаторов (первоначально она указывает на начало таблицы). Метод цепочек работает по следующему алгоритму: 1. Во все ячейки хэш-таблицы поместить пустое значение, таблица идентифика- торов пуста, переменная FreePtr (указатель первой свободной ячейки) указы- вает на начало таблицы идентификаторов.
24 Лабораторная работа № 1 • Организация таблиц идентификаторов 2. Вычислить значение хэш-функции п для нового элемента А. Если ячейка хэш- таблицы по адресу п пустая, то поместить в нее значение переменной FreePtr и перейти к шагу 5; иначе перейти к шагу 3. 3. Выбрать из хэш-таблицы адрес ячейки таблицы идентификаторов т и перей- ти к шагу 4. 4. Для ячейки таблицы идентификаторов по адресу тп проверить значение поля ссылки. Если оно пустое, то записать в него адрес из переменной FreePtr и пе- рейти к шагу 5; иначе выбрать из поля ссылки новый адрес m и повторить шаг 4. 5. Добавить в таблицу идентификаторов новую ячейку, записать в нее информа- цию для элемента?! (поле ссылки должно быть пустым), в переменную FreePtr поместить адрес за концом добавленной ячейки. Если больше пет идентифи- каторов, которые надо поместить в таблицу, то выполнение алгоритма закон- чено, иначе перейти к шагу 2. Поиск элемента в таблице идентификаторов, организованной таким образом, бу- дет выполняться по следующему алгоритму: 1. Вычислить значение хэш-функции п для искомого элемента А. Если ячейка хэш-таблицы по адресу п пустая, то элемент не найден и алгоритм завершен, иначе выбрать из хэш-таблнцы адрес ячейки таблицы идентификаторов тп. 2. Сравнить имя элемента в ячейке таблицы идентификаторов по адресу тп с име- нем искомого элемента А. Если они совпадают, то искомый элемент найден и алгоритм завершен, иначе перейти к шагу 3. 3. Проверить значение поля ссылки в ячейке таблицы идентификаторов по ад- ресу т. Если оно пустое, то искомый элемент не найден и алгоритм завершен; иначе выбрать из поля ссылки адрес т и перейти к шагу 2. При такой организации таблиц идентификаторов в случае возникновения колли- зии алгоритм помещает элементы в ячейки таблицы, связывая их друг с другом последовательно через поле ссылки. При этом элементы не могут попадать в ячей- ки с адресами, которые потом будут совпадать со значениями хэш-функции. Та- ким образом, дополнительные коллизии не возникают. В итоге в таблице возни- кают своеобразные цепочки связанных элементов, откуда и происходит название данного метода — «метод цепочек». На рис. 1.2 проиллюстрировано заполнение хэш-таблицы и таблицы идентифи- каторов для ряда идентификаторов: Др Л2,Л3, А,, А5 при условии, что Л(Л,) = Л(Л2) = = А(А5) = и,; Л(А3) = и2; Л(А4) = п4. После размещения в таблице для поиска иден- тификатора A t потребуется одно сравнение, для Л2 — два сравнения, для А3 — одно сравнение, для А4 — одно сравнение и для А5 — три сравнения (попробуйте срав- нить эти данные с результатами, полученными с использованием простого рехэ- ширования для тех же идентификаторов). Метод цепочек является очень эффективным средством организации таблиц иден- тификаторов. Среднее время на размещение одного элемента и на поиск элемен- та в таблице для него зависит только от среднего числа коллизий, возникающих при вычислении хэш-функции. Накладные расходы памяти, связанные с необхо-
^lalaltaus^. Краткие теоретические сведения 25 димостью иметь одно дополнительное поле указателя в таблице идентификато- ров на каждый ее элемент, можно признать вполне оправданными, так как возни- кает экономия используемой памяти за счет промежуточной хэш-таблицы. Этот метод позволяет более экономно использовать память, но требует организации работы с динамическими массивами данных. Рис. 1.2. Заполнение таблицы идентификаторов при использовании метода цепочек Комбинированные способы построения таблиц идентификаторов Кроме рехэширования и метода цепочек можно использовать комбинированные методы для организации таблиц идентификаторов с помощью хэш-адресации. В этом случае для исключения коллизий хэш-адресация сочетается с одним из ранее рассмотренных методов — простым списком, упорядоченным списком или бинарным деревом, который используется как дополнительный метод упорядо- чивания идентификаторов, для которых возникают коллизии. Причем, посколь- ку при качественном выборе хэш-функции количество коллизий обычно неве- лико (единицы или десятки случаев), даже простой список может быть вполне удовлетворительным решением при использовании комбинированного метода.
26 Лабораторная работа № 1 • Организация таблиц идентификаторов При таком подходе возможны два варианта: в первом случае, как и для метода цепочек, в таблице идентификаторов организуется специальное дополнительное поле ссылки. Но в отличие от метода цепочек оно имеет несколько иное значение: при отсутствии коллизий для выборки информации из таблицы используется хэш- функция, поле ссылки остается пустым. Если же возникает коллизия, то через поле ссылки организуется поиск идентификаторов, для которых значения хэш- функции совпадают — это поле должно указывать на структуру данных для до- полнительного метода: начало списка, первый элемент динамического массива или корневой элемент дерева. Во втором случае используется хэш-таблица, аналогичная хэш-таблице для метода цепочек, Если по данному адресу хэш-функции идентификатор отсутствует, то ячей- ка хэш-таблицы пустая. Когда появляется идентификатор с данным значением хэш- функции, то создается соответствующая структура для дополнительного метода, в хэш-таблицу записывается ссылка на эту структуру, а идентификатор помещает- ся в созданную структуру по правилам выбранного дополнительного метода. В первом варианте при отсутствии коллизий поиск выполняется быстрее, но вто- рой вариант предпочтительнее, так как за счет использования промежуточной хэш- таблицы обеспечивается более эффективное использование памяти. Как и для метода цепочек, для комбинированных методов время размещения и время поиска элемента в таблице идентификаторов зависит только от среднего числа коллизий, возникающих при вычислении хэш-функции. Накладные расхо- ды памяти при использовании промежуточной хэш-таблицы минимальны. Очевидно, что если в качестве дополнительного метода использовать простой список, то получится алгоритм, полностью аналогичный методу цепочек. Если же использовать упорядоченный список или бинарное дерево, то метод цепочек и комбинированные методы будут иметь примерно равную эффективность при незначительном числе коллизий (единичные случаи), но с ростом количества кол- лизий эффективность комбинированных методов по сравнению с методом цепо- чек будет возрастать. Недостатком комбинированных методов является более сложная организация алгоритмов поиска и размещения идентификаторов, необходимость работы с ди- намически распределяемыми областями памяти, а также большие затраты време- ни на размещение нового элемента в таблице идентификаторов по сравнению с ме- тодом цепочек. То, какой конкретно метод применяется в компиляторе для организации таблиц идентификаторов, зависит от реализации компилятора. Один и тот же компилятор может иметь даже несколько разных таблиц идентификаторов, организованных на основе различных методов. Как правило, применяются комбинированные методы. Создание эффективной хэш-функции — это отдельная задача разработчиков ком- пиляторов, и полученные результаты, как правило, держатся в секрете. Хорошая хэш-функция распределяет поступающие на ее вход идентификаторы равномер- но на все имеющиеся в распоряжении адреса, чтобы свести к минимуму количе- ство коллизий. В настоящее время существует множество хэш-функций, но, как было показано выше, идеального хэширования достичь невозможно.
Требования к выполнению работы 27 Хэш-адресация — это метод, который применяется не только для организации таблиц идентификаторов в компиляторах. Данный метод нашел свое применение и в операционных системах, и в системах управления базами данных [5, 6, 11]. Требования к выполнению работы Порядок выполнения работы Во всех вариантах задания требуется разработать программу, которая может обес- печить сравнение двух способов организации таблицы идентификаторов с по- мощью хэш-адресации. Для сравнения предлагаются способы, основанные на ис- пользовании рехэширования или комбинированных методов. Программа должна считывать идентификаторы из входного файла, размещать их в таблицах с помо- щью заданных методов и выполнять поиск указанных идентификаторов по тре- бованию пользователя. В процессе размещения и поиска идентификаторов в таб- лицах программа должна подсчитывать среднее число выполненных операций сравнения для сопоставления эффективности используемых методов. Для организации таблиц предлагается использовать простейшую хэш-функцию, которую разработчик программы должен выбрать самостоятельно. Хэш-функция должна обеспечивать работу не менее чем с 200 идентификаторами, допустимая длина идентификатора должна быть не менее 32 символов. Запрещается исполь- зовать в работе хэш-функции, взятые из примера выполнения работы. Лабораторная работа должна выполняться в следующем порядке: 1. Получить вариант задания у преподавателя. 2. Выбрать и описать хэш-функцию. 3. Описать структуры данных, используемые для заданных методов организации таблиц идентификаторов. 4. Подготовить и защитить отчет. 5. Написать и отладить программу на ЭВМ. 6. Сдать работающую программу преподавателю. Требования к оформлению отчета Отчет по лабораторной работе должен содержать следующие разделы: □ задание по лабораторной работе; □ описание выбранной хэш-функции; □ схемы организации таблиц идентификаторов (в соответствии с вариантом за- дания); □ описание алгоритмов поиска в таблицах идентификаторов (в соответствии с ва- риантом задания); □ текст программы (оформляется после выполнения программы на ЭВМ);
28 Лабораторная работа № 1 • Организация таблиц идентификаторов □ результаты обработки заданного набора идентификаторов (входного файла) с помощью методов организации таблиц идентификаторов, указанных в вари- анте задания; □ анализ эффективности используемых методов организации таблиц идентифи- каторов и выводы по проделанной работе. Основные контрольные вопросы □ Что такое таблица символов и для чего она предназначена? Какая информа- ция может храниться в таблице символов? □ Какие цели преследуются при организации таблицы символов? □ Какими характеристиками могут обладать лексические элементы исходной программы? Какие характеристики являются обязательными? □ Какие существуют способы организации таблиц символов? □ В чем заключается алгоритм логарифмического поиска? Какие преимущества он дает по сравнению с простым перебором и какие он имеет недостатки? □ Расскажите о древовидной организации таблиц идентификаторов. В чем ее преимущества и недостатки? □ Что такое хэш-функции и для чего они используются? В чем суть хэш-адресации? □ Что такое коллизия? Почему она происходит? Можно ли полностью избежать коллизий? □ Что такое рехэширование? Какие методы рехэширования существуют? □ Расскажите о преимуществах и недостатках организации таблиц идентифика- торов с помощью хэш-адресации и рехэширования. □ В чем заключается метод цепочек? □ Расскажите о преимуществах и недостатках организации таблиц идентифика- торов с помощью хэш-адресации и метода цепочек. □ Как могут быть скомбинированы различные методы организации хеш-таблиц? □ Расскажите о преимуществах и недостатках организации таблиц идентифика- торов с помощью комбинированных методов. Варианты заданий В табл. 1.1 перечислены методы организации таблиц идентификаторов, исполь- зуемые в заданиях. Таблица 1.1. Методы организации таблиц идентификаторов № метода Способ разрешения коллизий 1 Простое рехэширование 2 Рехэширование с использованием псевдослучайных чисел
^aiaHauSt^i Пример выполнения работы 29 № метода Способ разрешения коллизий 3* Рехэширование с помощью произведения 4 Метод цепочек 5 6 Простой список Упорядоченный список 7 Бинарное дерево В табл. 1.2 даны варианты заданий на основе методов организации таблиц иден- тификаторов, перечисленных в табл. 1.1. Таблица 1.2. Варианты заданий № варианта Первый метод организации таблицы Второй метод организации таблицы 1 1 5 2 1 6 3 1 7 4 2 1 5 2 5 6 2 6 7 3 5 8 3 6 9 3 7 10 7 5 11 4 6 12 4 7 13 1 4 14 2 4 15 3 4 16 2 3 Пример выполнения работы Задание для примера В качестве примера выполнения лабораторной работы возьмем сопоставление двух методов: хэш-адресации с рехэшированием на основе псевдослучайных чисел и комбинации хэш-адресации с бинарным деревом. Если обратиться к при- веденной выше табл. 1.1, то такой вариант задания будет соответствовать ком- бинации методов 2 и 7 (в табл. 1.2 среди вариантов заданий такая комбинация отсутствует).
30 Лабораторная работа № 1 • Организация таблиц идентификаторов Выбор и описание хэш-функции Для хэш-адресации с рехэшированием в качестве хэш-функции возьмем функ- цию, которая будет получать на входе строку, а в результате выдавать сумму ко- дов первого, среднего и последнего элементов строки. Причем если строка содер- жит менее трех символов, то один и тот же символ будет взят и в качестве перво- го, и в качестве среднего, и в качестве последнего. Будем считать, что прописные и строчные буквы в идентификаторах различны1. В ка- честве кодов символов возьмем коды таблицы ASCII, которая используется в вычис- лительных системах на базе ОС типа Microsoft Windows. Тогда, если положить, что строка из области определения хэш-функции содержит только цифры и буквы анг- лийского алфавита, то минимальным значением хэш-функции будет сумма трех ко- дов цифры «О», а максимальным значением — сумма трех кодов литеры «z». Таким образом, область значений выбранной хэш-функции в терминах языка Object Pascal может быть описана как: (Ord('О')+Ord('О')+Ord('О’))..(Ord('z')+Ord(' z ‘)+Ord('z')) Диапазон области значений составляет 223 элемента, что удовлетворяет требова- ниям задания (не менее 200 элементов). Длина входных идентификаторов в дан- ном случае ничем не ограничена. Для удобства пользования опишем две констан- ты, задающие границы области значений хэш-функции: HASH_MIN = Ord('0‘)+Ord(’О’)+Ord('О'); HASH_MAX = Ord(’z')+Ord(.'z')+Ord('z'). Сама хэш-функция без учета рехэширования будет вычислять следующее выра- жение: Ord(sName[1]) + Ord(sName[(Length(sName)+l) div 2]) + Ord(sName[Length(sName)] здесь sName — это входная строка (аргумент хэш-функции). Для рехэширования возьмем простейший генератор последовательности псевдо- случайных чисел, построенный на основе формулы F= i-Hl mod Н2, где ^не- простые числа, выбранные таким образом, чтобы Ht было в диапазоне от Н2/2 до Н2. Причем, чтобы этот генератор выдавал максимально длинную последователь- ность во всем диапазоне от HASH MIN до HASH MAX, Н2 должно быть максимально близ- ко к величине HASH MAX-HASH MIN + 1. В данном случае диапазон содержит 223 эле- мента, и поскольку 223 — простое число, то возьмем Н2 = 223 (если бы размер диа- пазона не был простым числом, то в качестве Н2 нужно было бы взять ближайшее к нему меньшее простое число). В качестве Нх возьмем Х2Т.Нх = 127. Опишем соответствующие константы: REHASH1 = 127; REHASH2 =223: 1 Программные модули, реализующие таблицы символов, построены таким образом, что в зависимо- сти от условий компиляции они могут либо различать, либо не различать прописные и строчные буквы. Условие компиляции реализовано через макрокоманды компилятора Delphi 5 в функции Upper в модулеТЫЕ1ет (листинг П3.1, приложение 3). О принципах, па основе которых выполня- ются макрокоманды и условная компиляция, можно подробно узнать в [7, 13, 23, 25, 28, 321.
Пример выполнения работы 31 ftalattausliV. Тогда хэш-функция с учетом рехэширования будет иметь следующий вид: function VarHash(const sName:string; INum: Integer) .-longlnt: begin Result:=(Ord(sName[l])+Ord(sName[(Length(sName)+l) div 2]) + Ord(sName[Length(sName)]) - HASH_MIN + 1Num*REHASHl mod REHASH2) mod (HASH_MAX-HASH_MIN+1) + HASH_MIN; if Result < HASH_MIN then Result := HASH_MIN: end; Входные параметры этой функции: sName — имя хэшируемого идентификатора, iNum — индекс рехэшированиея (если iNum = 0, то рехэширование отсутствует). Строка проверки величины результата (Result < HASH MIN) добавлена, чтобы исклю- чить ошибки в тех случаях, когда на вход функции подается строка, содержащая символы вне диапазона 'О'.. 'z' (поскольку контроль входных идентификаторов отсутствует, это имеет смысл). Для комбинации хэш-адресации и бинарного дерева можно использовать более простую хэш-функцию — сумму кодов первого и среднего символов входной стро- ки. Диапазон значений такой хэш-функции в терминах языка Object Pascal будет выглядеть так: (Ord('0')+0rd('0'))..(Ord('z')+0rd(’z’)) Этот диапазон содержит менее 200 элементов, однако функция будет удовлетво- рять требованиям задания, так как в комбинации с бинарным деревом она будет обеспечивать обработку неограниченного количества идентификаторов (макси- мальное количество идентификаторов будет ограничено только объемом доступ- ной оперативной памяти компьютера). Без применения рехэширования эта хэш-функция будет выглядеть значительно проще, чем описанная выше хэш-функция с учетом рехэширования: function VarHashtconst sName: string): longlnt; begin Result:=(Ord(sName[l])+Ord(sName[(Length(sName)+l) div 2]) - HASH_MIN) mod (HASH_MAX-HASH_MIN+1) + HASH_MIN; If Result < HASH_MIN then Result -.= HASH_MIN; end. Описание структур данных таблиц идентификаторов В первую очередь необходимо описать структуру данных, которая будет использо- вана для хранения информации об идентификаторах в таблицах идентификаторов. Для обеих таблиц (с рехэшированием на основе генератора псевдослучайных чи- сел и в комбинации с бинарным деревом) будем использовать одну и ту же струк- туру. В этом случае в таблицах будут храниться неиспользуемые данные, но про- граммный код будет проще. В качестве учебного примера такой подход оправдан.
32 Лабораторная работа № 1 • Организация таблиц идентификаторов Структура данных таблицы идентификаторов (назовем ее TVarlnfo) должна со- держать в обязательном порядке поле имени идентификатора (поле sName: string), а также поля дополнительной информации об идентификаторе по усмотрению разработчиков компилятора. В лабораторной работе не предусмотрено хранение какой-либо дополнительной информации об идентификаторах, поэтому в каче- стве иллюстрации информационного поля включим в структуру TVarlnfo допол- нительную информационную структуру TAddVarlnfo (поле plnfo: TAddVarlnfo). Поскольку в языке Object Pascal для полей и переменных, описанных как class, хранятся только ссылки на соответствующую структуру, такой подход не приве- дет к значительным расходам памяти, но позволит в будущем хранить любую ин- формацию, связанную с идентификатором, в отдельной структуре данных (по- скольку предполагается использовать создаваемые программные модули в по- следующих лабораторных работах). В данном случае другой подход невозможен, так как заранее не известно, какие данные необходимо будет хранить в таблицах идентификаторов. Но разработчик реального компилятора, как правило, знает, какую информацию требуется хранить, и может использовать другой подход — непосредственно включить все необходимые поля в структуру данных таблицы идентификаторов (в данном случае — в структуру TVarlnfo) без использования промежуточных структур данных и ссылок. Первый подход, реализованный в данном примере, обеспечивает более эконом- ное использование оперативной памяти, но является более сложным и требует работы с динамическими структурами, второй подход более прост в реализации, но менее экономно использует память. Какой из двух подходов выбрать, решает разработчик компилятора в каждом конкретном случае (второй подход будет про- иллюстрирован позже в примере к лабораторной работе № 4). Для работы со структурой данных TVarlnfo потребуются следующие функции: □ функции создания структуры данных и освобождения занимаемой памяти — реализованы как constructor Create и destructor Destroy; □ функции доступа к дополнительной информации — в данной реализации это procedure Setinfo и procedure Clearinfo. Эти функции будут общими для таблицы идентификаторов с рехэшированием и для комбинированной таблицы идентификаторов. Однако для комбинированной таблицы идентификаторов в структуру данных TVarlnfo потребуется также включить дополнительные поля данных и функции, обеспечивающие организацию бинарного дерева: □ ссылки на левую («меньшую») и правую («большую») ветвь дерева — реали- зованы как поля данных minEl. maxEl: TVarlnfo; □ функции добавления элемента в дерево — function AddEICnt и function AddElem; □ функции поиска элемента в дереве — function FindEICnt и function FindElem; □ функция очистки информационных полей во всем дереве — procedure Cl earAl 1 Info; □ функция вывода содержимого бинарного дерева в одну строку (для получе- ния списка всех идентификаторов) — function GetElList.
^alatlaus^il. Пример выполнения работы 33 Функции поиска и размещения элемента в дереве реализованы в двух экземплярах, так как одна из них выполняет подсчет количества сравнений, а другая — нет. Поскольку на функции и процедуры не расходуется оперативная память, в резуль- тате получилось, что при использовании одной и той же структуры данных для разных таблиц идентификаторов в таблице с рехэшированием будет расходоваться неиспользуемая память только на хранение двух лишних ссылок (minEl и maxEl). Полностью вся структура данных TVarlnfo и связанные с ней процедуры и функ- ции описаны в программном модуле TblElem. Полный текст этого программного модуля приведен в листинге П3.1 в приложении 3. Надо обратить внимание на один важный момент в реализации функции поиска идентификатора в дереве (function TVarlnfo.FindEICnt). Если выполнять сравне- ние двух строк (в данном случае — имени искомого идентификатора sN и имени идентификатора в текущем узле дерева sName) с помощью стандартных методов сравнения строк языка Object Pascal, то фрагмент программного кода выглядел бы примерно так: if sN < sName then begin end else if sN > sName then begin end else ... В этом фрагменте сравнение строк выполняется дважды: сначала проверяется отношение «меньше» (sN < sName), а потом — «больше» (sN > sName). И хотя в про- граммном коде явно это не указано, для каждого из этих операторов будет вызва- на библиотечная функция сравнения строк (то есть операция сравнения может выполниться дважды!). Чтобы этого избежать, в реализации предложенной в при- мере выполняется явный вызов функции сравнения строк, а потом обрабатыва- ется полученный результат: i StrComp(PChar(sN).PChar(sName)): if i < 0 then begin end el se if i > 0 then begin end else .... 2 Зак. 68
34 Лабораторная работа № 1 • Организация таблиц идентификаторов В таком варианте дважды может быть выполнено только сравнение целого числа с нулем, а сравнение строк всегда выполняется только один раз, что существенно увеличивает эффективность процедуры поиска. Организация таблиц идентификаторов Таблицы идентификаторов реализованы в виде статических массивов размером HASH MIN.. HASH MAX, элементами которых являются структуры данных типа TVarlnfo. В языке Object Pascal, как было сказано выше, для структур таких типов хранят- ся ссылки. Поэтому для обозначения пустых ячеек в таблицах идентификаторов будет использоваться пустая ссылка — nil. Поскольку в памяти хранятся ссылки, описанные массивы будут играть роль хэш- таблиц, ссылки из которых указывают непосредственно на информацию в табли- цах идентификаторов. На рис. 1.3 показаны условные схемы, наглядно иллюстрирующие организацию таблиц идентификаторов. Схема 1 иллюстрирует таблицу идентификаторов с ре- хэшированием на основе генератора псевдослучайных чисел, схема 2 — таблицу идентификаторов на основе комбинации хэш-адресации с бинарным деревом. Ячейки с надписью «nil» соответствуют незаполненным ячейкам хэш-таблицы. Схема 1 Поля ссылок в элементах таблицы не используются, а потому не имеют значения Схема 2 H2(Ai) = H2(Ak),Ai>Ak; H2(Aj) = H2(Al).Aj>A| H-i и Н2 - соответствующие хэш-функции Рис. 1.3. Схемы организации таблиц идентификаторов Для каждой таблицы идентификаторов реализованы следующие функции: □ функции начальной инициализации хэш-таблицы — InitTreeVar и InitHashVar; □ функции освобождения памяти хэш-таблицы — ClearTreeVar и ClearHashVar; □ функции удаления дополнительной информации в таблице — ClearTreelnfo и ClearHashlnfo; □ функции добавления элемента в таблицу идентификаторов — AddTreeVar и Add- HashVar;
^aiaHaus,^. Пример выполнения работы 35 □ функции поиска элемента в таблице идентификаторов — GetTreeVar и GetHashVar; □ функции, возвращающие количество выполненных операций сравнения при размещении или поиске элемента в таблице — GetTreeCount и GetHashCount. Алгоритмы поиска и размещения идентификаторов для двух данных методов орга- низации таблиц были описаны выше в разделе «Краткие теоретические сведения», поэтому приводить их здесь повторно нет смысла. Они реализованы в виде четы- рех перечисленных выше функций (AddTreeVar и AddHashVar — для размещения эле- мента; GetTreeVar и GetHashVar — для поиска элемента). Функции поиска и разме- щения элементов в таблице в качестве результата возвращают ссылку на элемент таблицы (структура которого описана в модуле TblElem) в случае успешного вы- полнения и нулевую ссылку — в противном случае. Надо отметить, что функции размещения идентификатора в таблице организова- ны таким образом, что если на момент помещения нового идентификатора в таб- лице уже есть идентификатор с таким же именем, то функция не добавляет но- вый идентификатор в таблицу, а возвращает в качестве результата ссылку на ра- нее помещенный в таблицу идентификатор. Таким образом, в таблице не может быть двух и более идентификаторов с одинаковым именем. При этом наличие оди- наковых идентификаторов во входном файле не воспринимается как ошибка — это допустимо, так как в задании не предусмотрено ограничение на наличие со- впадающих имен идентификаторов. Все перечисленные функции описаны в двух программных модулях: FncHash — для таблицы идентификаторов, построенной на основе рехэшировапия с использова- нием генератора псевдослучайных чисел, и FncTree — для таблицы идентификато- ров, построенной па основе комбинации хэш-адресации и бинарного дерева. Кроме массивов данных для организации таблиц идентификаторов и функций работы с ни- ми эти модули содержат также описание переменных, используемых для подсчета количества выполненных операций сравнения при размещении и поиске иденти- фикатора в таблицах. Полные тексты обоих модулей (FncHash и FncTree) можно найти на веб-сайте из- дательства, в файлах FncHash.pas и FncTree.pas. Кроме того, текст модуля FncTree приведен в листинге П3.2 в приложении 3. Хочется обратить внимание на то, что в разделах инициализации (initialization) обоих модулей вызывается функция начального заполнения таблицы идентифи- каторов, а в разделах завершения (finalization) обоих модулей — функция осво- бождения памяти. Это гарантирует корректную работу модулей при любом по- рядке вызова остальных функций, поскольку Object Pascal сам обеспечивает свое- временный вызов программного кода в разделах инициализации и завершения модулей. Текст программы Кроме перечисленных выше модулей необходим еще модуль, обеспечивающий интерфейс с пользователем. Этот модуль (FormLabi) реализует графическое окно TLablForm на основе класса TForm библиотеки VCL. Он обеспечивает интерфейс
36 Лабораторная работа № 1 • Организация таблиц идентификаторов средствами Graphical User Interface (GUI) в ОС типа Windows на основе стан- дартных органов управления из системных библиотек данной ОС. Кроме про- граммного кода (файл FormLabi.pas) модуль включает в себя описание ресурсов пользовательского интерфейса (файл FormLabi.dfm). Более подробно принципы организации пользовательского интерфейса на основе GUI и работа систем про- граммирования с ресурсами интерфейса описаны в [3, 5, 6,7]. Кроме описания интерфейсной формы и ее органов управления модуль FormLabi содержит три переменные (ICountNum, ICountHash, ICountTree), служащие для накоп- ления статистических результатов по мере выполнения размещения и поиска идентификаторов в таблицах, а также функцию (procedure ViewStati Stic) для ото- бражения накопленной статистической информации на экране. Интерфейсная форма, описанная в модуле, содержит следующие основные орга- ны управления: □ поле ввода имени файла (EditFile), кнопка выбора имени файла из каталогов файловой системы (BtnFile), кнопка чтения файла (BtnLoad); □ многострочное поле для отображения прочитанного файла (Listldents); □ поле ввода имени искомого идентификатора (EditSearch); □ кнопка для поиска введенного идентификатора (BtnSearch) — этой кнопкой однократно вызывается процедура поиска (procedure SearchStr); □ кнопка автоматического поиска всех идентификаторов (BtnAll Search) — этой кнопкой процедура поиска идентификатора (procedure SearchStr) вызывается циклически для всех считанных из файла идентификаторов (для всех, пере- численных в поле Li stldents); □ кнопка сброса накопленной статистической информации (BtnReset); □ поля для отображения статистической информации; □ кнопка завершения работы с программой (BtnExit). Внешний вид этой формы приведен на рис. 1.4. Функция чтения содержимого файла с идентификаторами (procedure TLablForm. BtnLoadClick) вызывается щелчком по кнопке BtnLoad. Она организована таким об- разом, что сначала содержимое файла читается в многострочное поле Li stldents, а затем все прочитанные идентификаторы записываются в две таблицы иденти- фикаторов. Каждая строка файла считается отдельным идентификатором, пробе- лы в начале и в конце строки игнорируются. При ошибке размещения идентифи- катора в одной из таблиц выдается предупреждающее сообщение (например, если будет считано более 223 различных идентификаторов, то рехэширование станет невозможным и будет выдано сообщение об ошибке). Функция поиска идентификатора (procedure TLablForm.SearchStr) вызывается од- нократно щелчком по кнопке BtnSearch (процедура procedureTLablForm. BtnSearchCl ick) или многократно щелчком по кнопке BtnAllSearch (процедура procedure TLablForm. BtnAllSearchClick). Поиск идет сразу в двух таблицах, результаты поиска и накоп- ленная статистическая информация отображаются в соответствующих полях.
Пример выполнения работы 37 Рис. 1.4. Внешний вид интерфейсной формы для лабораторной работы № 1 Полный текст программного кода модуля интерфейса с пользователем и описа- ние ресурсов пользовательского интерфейса находятся в архиве, располагаю- щемся на веб-сайте издательства, в файлах FormLabi .pas и FormLabi .dfm соот- ветственно. Полный текст всех программных модулей, реализующих рассмотренный пример для лабораторной работы № 1, можно найти в архиве, располагающемся на веб- сайте, в подкаталогах LABS и COMMON (в подкаталог COMMON вынесены те про- граммные модули, исходный текст которых не зависит от входного языка и зада- ния по лабораторной работе). Главным файлом проекта является файл LAB 1 .DPR в подкаталоге LABS. Кроме того, текст модуля FncTree приведен в листинге П3.1 в приложении 3. Выводы по проделанной работе В результате выполнения написанного программного кода для ряда тестовых фай- лов было установлено, что при заполнении таблицы идентификаторов до 20% (до 45 идентификаторов) для поиска и размещения идентификатора с использованием рехэширования на основе генератора псевдослучайных чисел в среднем требует- ся меньшее число сравнений, чем при использовании хэш-адресации в комбина- ции с бинарным деревом. При заполнении таблицы от 20% до 40% (примерно 45- 90 идентификаторов) оба метода имеют примерно равные показатели, но при за- полнении таблицы более, чем на 40% (90-223 идентификаторов), эффективность комбинированного метода по сравнению с методом рехэширования резко возрас-
. 38 Лабораторная работа № 1 • Организация таблиц идентификаторов тает. Если на входе имеется более 223 идентификаторов, рехэширование пол- ностью перестает работать. Таким образом, установлено, что комбинированный метод работоспособен даже при наличии простейшей хэш-функции и дает неплохие результаты (в среднем 3-5 сравнений на входных файлах, содержащих 500-700 идентификаторов), в то время как метод на основе рехэширования для реальной работы требует более сложной хэшгфункции с диапазоном значений в несколько тысяч или десятков тысяч.
ЛАБОРАТОРНАЯ РАБОТА № 2 Проектирование лексического анализатора Цель работы Цель работы, изучение основных понятий теории регулярных грамматик, озна- комление с назначением и принципами работы лексических анализаторов (ска- неров), получение практических навыков построения сканера на примере задан- ного простейшего входного языка. Краткие теоретические сведения Назначение лексического анализатора Лексический анализатор (или сканер) — это часть компилятора, которая читает литеры программы на исходном языке и строит из них слова (лексемы) исходно- го языка. На вход лексического анализатора поступает текст исходной програм- мы, а выходная информация передается для дальнейшей обработки компилято- ром на этапе синтаксического анализа и разбора. Лексема (лексическая единица языка) — это структурная единица языка, которая состоит из элементарных символов языка и не содержит в своем составе других структурных единиц языка. Лексемами языков программирования являются иден- тификаторы, константы, ключевые слова языка, знаки операций и т. п. Состав
40 Лабораторная работа № 2 • Проектирование лексического анализатора возможных лексем каждого конкретного языка программирования определяется синтаксисом этого языка. С теоретической точки зрения лексический анализатор не является обязатель- ной, необходимой частью компилятора. Его функции могут выполняться на эта- пе синтаксического анализа. Однако существует несколько причин, исходя из которых в состав практически всех компиляторов включают лексический анализ. Это следующие причины: □ упрощается работа с текстом исходной программы на этапе синтаксического разбора и сокращается объем обрабатываемой информации, так как лексиче- ский анализатор структурирует поступающий на вход исходный текст програм- мы и удаляет всю незначащую информацию; □ для выделения в тексте и разбора лексем возможно применять простую, эф- фективную и хорошо проработанную теоретически технику анализа, в то вре- мя как на этапе синтаксического анализа конструкций исходного языка ис- пользуются достаточно сложные алгоритмы разбора; □ лексический анализатор отделяет сложный по конструкции синтаксический анализатор от работы непосредственно с текстом исходной программы, струк- тура которого может варьироваться в зависимости от версии входного языка — при такой конструкции компилятора при переходе от одной версии языка к дру- гой достаточно только перестроить относительно простой лексический анали- затор. Функции, выполняемые лексическим анализатором, и состав лексем, которые он выделяет в тексте исходной программы, могут меняться в зависимости от версии компилятора. В основном лексические анализаторы выполняют исключение из текста исходной программы комментариев и незначащих пробелов, а также вы- деление лексем следующих типов: идентификаторов, строковых, символьных и числовых констант, знаков операций, разделителей и ключевых (служебных) слов входного языка. В большинстве компиляторов лексический и синтаксический анализаторы — это взаимосвязанные части. Где провести границу между лексическим и синтакси- ческим анализом, какие конструкции анализировать сканером, а какие — синтак- сическим распознавателем, решает разработчик компилятора. Как правило, лю- бой анализ стремятся выполнить на этапе лексического разбора входной програм- мы, если он может быть там выполнен. Возможности лексического анализатора ограничены по сравнению с синтаксическим анализатором, так как в его основе лежат более простые механизмы. Более подробно о роли лексического анализа- тора в компиляторе и о его взаимодействии с синтаксическим анализатором мож- но узнать в [1-4,7]. Проблема определения границ лексем В простейшем случае фазы лексического и синтаксического анализа могут вы- полняться компилятором последовательно. Но для многих языков программиро- вания информации на этапе лексического анализа может быть недостаточно для однозначного определения типа и границ очередной лексемы.
Краткие теоретические сведения 41 Иллюстрацией такого случая может служить пример оператора программы на языке Фортран, когда по части текста DO 10 1=1... невозможно определить тип оператора (а соответственно, и границы лексем). В случае D0 10 1=1.15 это будет присвоение вещественной переменной D010I значения константы 1.15 (пробелы в Фортране игнорируются), а в случае DO 10 1=1,15 это цикл с перечислением от 1 до 15 по целочисленной переменной I до метки 10. Другая иллюстрация из более современного языка программирования C++ — опе- ратор присваивания k=i+++++j;, который имеет только одну верную интерпрета- цию (если операции разделить пробелами): k = 1++ + ++J:. Если невозможно определить границы лексем, то лексический анализ исходного текста должен выполняться поэтапно. Тогда лексический и синтаксический ана- лизаторы должны функционировать параллельно, поочередно обращаясь друг к другу. Лексический анализатор, найдя очередную лексему, передает ее синтак- сическому анализатору, тот пытается выполнить анализ считанной части исход- ной программы и может либо запросить у лексического анализатора следующую лексему, либо потребовать от него вернуться на несколько шагов назад и попро- бовать выделить лексемы с другими границами. При этом он может сообщить ин- формацию о том, какую лексему следует ожидать. Более подробно такая схема взаимодействия лексического и синтаксического анализаторов описана в [3, 7]. Параллельная работа лексического и синтаксического анализаторов, очевидно, более сложна в реализации, чем их последовательное выполнение. Кроме того, такой подход требует больше вычислительных ресурсов и в общем случае боль- шего времени на анализ исходной программы, так как допускает возврат назад и повторный анализ уже прочитанной части исходного кода. Тем не менее слож- ность синтаксиса некоторых языков программирования требует именно такого подхода — рассмотренный ранее пример программы на языке Фортран не может быть проанализирован иначе. Чтобы избежать параллельной работы лексического и синтаксического анализа- торов, разработчики компиляторов и языков программирования часто идут на разумные ограничения синтаксиса входного языка. Например, для языка C++ принято соглашение, что при возникновении проблем с определением границ лек- семы всегда выбирается лексема максимально возможной длины. В рассмотренном выше примере для оператора k=i+++++j; это приведет к тому, что при чтении четвертого знака + из двух вариантов лексем (+ — знак сложения в C++, а ++ — оператор инкремента) лексический анализатор выберет самую длин- ную — ++ (оператор инкремента) — и в целом весь оператор будет разобран как k = 1++ ++ + j; (знаки операций разделены пробелами), что неверно, так как семан- тика языка C++ запрещает два оператора инкремента подряд. Конечно, неверный анализ операторов, аналогичных приведенному в примере (желающие могут убе- диться в этом на любом доступном компиляторе языка C++), — незначительная плата за увеличение эффективности работы компилятора и не ограничивает воз- можности языка (тот же самый оператор может быть записан в виде k=i++ + ++j;, что исключит любые неоднозначности в его анализе). Однако таким же путем для языка Фортран пойти нельзя — разница между оператором присваивания и опе- ратором цикла слишком велика, чтобы ею можно было пренебречь.
42 Лабораторная работа № 2 • Проектирование лексического анализатора В дальнейшем будем исходить из предположения, что все лексемы могут быть однозначно выделены сканером на этапе лексического анализа. Для всех совре- менных языков программирования это действительно так, поскольку их синтак- сис разрабатывался с учетом возможностей компиляторов. Таблица лексем и содержащаяся в ней информация Результатом работы лексического анализатора является перечень всех найден- ных в тексте исходной программы лексем с учетом характеристик каждой лексе- мы. Этот перечень лексем можно представить в виде таблицы, называемой таб- лицей лексем. Каждой лексеме в таблице лексем соответствует некий уникальный условный код, зависящий от типа лексемы, и дополнительная служебная инфор- мация. Таблица лексем в каждой строке должна содержать информацию о виде лексемы, ее типе и, возможно, значении. Обычно структуры данных, служащие для организации такой таблицы, имеют два поля: первое — тип лексемы, второе — указатель на информацию о лексеме. Кроме того, информация о некоторых типах лексем, найденных в исходной про- грамме, должна помещаться в таблицу идентификаторов (или в одну из таблиц идентификаторов, если компилятор предусматривает различные таблицы иден- тификаторов для различных типов лексем). ВНИМАНИЕ-------------------------------------------------------- Не следует путать таблицу лексем и таблицу идентификаторов — это две принципиально раз- ные таблицы, обрабатываемые лексическим анализатором. Таблица лексем фактически содержит весь текст исходной программы, обрабо- танный лексическим анализатором. В нее входят все возможные типы лексем, кроме того, любая лексема может встречаться в ней любое количество раз. Табли- ца идентификаторов содержит только определенные тины лексем — идентифи- каторы и константы. В нее не попадают такие лексемы, как ключевые (служеб- ные) слова входного языка, знаки операций и разделители. Кроме того, каждая лексема (идентификатор или константа) может встречаться в таблице идентифи- каторов только один раз. Также можно отмстить, что лексемы в таблице лексем обязательно располагаются в том же порядке, что и в исходной программе (поря- док лексем в ней не меняется), а в таблице идентификаторов лексемы располага- ются в любом порядке так, чтобы обеспечить удобство поиска. В качестве примёрз можно рассмотреть некоторый фрагмент исходного кода на языке Object Pascal и соответствующую ему таблицу лексем, представленную в табл. 2.1: begin for 1:=1 to N do fg := fg * 0.5
^aiatiaus,^ Краткие теоретические сведения 43 Таблица 2.1. Лексемы фрагмента программы на языке Pascal Лексема Тип лексемы Значение begin Ключевое слово х, for Ключевое слово х2 i Идентификатор i : 1 := Знак присваивания s, 1 Целочисленная константа 1 to Ключевое слово х3 N Идентификатор N : 2 do Ключевое слово х4 fg Идентификатор fg : 3 := Знак присваивания s. fg Идентификатор Fg : 3 * Знак арифметической операции А. 0.5 Вещественная константа 0.5 Поле «значение» в табл. 2.1 подразумевает некое кодовое значение, которое бу- дет помещено в итоговую таблицу лексем в результате работы лексического ана- лизатора. Конечно, значения, которые записаны в примере, являются условны- ми. Конкретные коды выбираются разработчиками при реализации компилято- ра. Важно отметить также, что устанавливается связь таблицы лексем с таблицей идентификаторов (в примере это отражено некоторым индексом, следующим пос- ле идентификатора за знаком «:», а в реальном компиляторе определяется его ре- ализацией). Построение лексических анализаторов (сканеров) Лексический анализатор имеет дело с такими объектами, как различного рода кон- станты и идентификаторы (к последним относятся и ключевые слова). Язык опи- сания констант и идентификаторов в большинстве случаев является регулярным, то есть может быть описан с помощью регулярных грамматик [ 1 -4,7]. Распознава- телями для регулярных языков являются конечные автоматы (КА). Существуют правила, с помощью которых для любой регулярной грамматики может быть пост- роен КА, распознающий цепочки языка, заданного этой грамматикой. Более подробно о построении КА на основе грамматик для регулярных языков можно узнать в [3, 7, 26]. Любой КА может быть задан с помощью пяти параметров: M(Q,E,6,g0,F), где: Q — конечное множество состояний автомата; Е — конечное множество допустимых входных символов (входной алфавит КА); б — заданное отображение множества Q-E во множество подмножеств P(Q) 5: Q-E —» P(Q) (иногда б называют функцией переходов автомата);
44 Лабораторная работа № 2 • Проектирование лексического анализатора <70 G Q — начальное состояние автомата; F с Q — множество заключительных состояний автомата. Другим способом описания КА является граф переходов — графическое представ- ление множества состояний и функции переходов КА. Граф переходов КА — это нагруженный однонаправленный граф, в котором вершины представляют состо- яния КА, дуги отображают переходы из одного состояния в другое, а символы нагрузки (пометки) дуг соответствуют функции перехода КА. Если функция пе- рехода КА предусматривает переход из состояния q в q 'по нескольким символам, то между ними строится одна дуга, которая помечается всеми символами, по ко- торым происходит переход из q в q Недетерминированный КА неудобен для анализа цепочек, так как в нем могут встречаться состояния, допускающие неоднозначность, то есть такие, из которых выходит две или более дуги, помеченные одним и тем же символом. Очевидно, что программирование работы такого КА — нетривиальная задача. Для простого программирования функционирования КА M(QX5,<7O,F) он должен быть детер- минированным — в каждом из возможных состояний этого КА для любого вход- ного символа функция перехода должна содержать не более одного состояния: VaIV, VglQ; либо 5(а,<?) = {г} re Q, либо 8(а,<?) = 0. Доказано, что любой недетерминированный КА может быть преобразован в де- терминированный КА так, чтобы их языки совпадали [3, 7, 26] (говорят, что эти КА эквивалентны). Кроме преобразования в детерминированный КА любой КА может быть миними- зирован — для него может быть построен эквивалентный ему детерминированный КА с минимально возможным количеством состояний. Алгоритмы преобразования КА в детерминированный КА и минимизации КА подробно описаны в [3, 7, 26]. Можно написать функцию, отражающую функционирование любого детермини- рованного КА. Чтобы запрограммировать такую функцию, достаточно иметь пе- ременную, которая бы отображала текущее состояние КА, а переходы из одного состояния в другое на основе символов входной цепочки могут быть построены с помощью операторов выбора. Работа функции должна продолжаться до тех пор, пока не будет достигнут конец входной цепочки. Для вычисления результата функ- ции необходимо по ее завершении проанализировать состояние КА. Если это одно из конечных состояний, то функция выполнена успешно и входная цепочка при- нимается, если нет, то входная цепочка не принадлежит заданному языку. Однако в общем случае задача лексического анализатора шире, чем просто про- верка цепочки символов лексемы на соответствие ее входному языку. Он должен правильно определить конец лексемы (об этом было сказано выше) и выполнить те или иные действия по запоминанию распознанной лексемы (занесение ее в таб- лицу лексем). Набор выполняемых действий определяется реализацией компи- лятора. Обычно эти действия выполняются сразу же при обнаружении конца рас- познаваемой лексемы. Во входном тексте лексемы не ограничены специальными символами. Определе- ние границ лексем — это выделение тех строк в общем потоке входных символов,
^ialattaus,^ Требования к выполнению работы 45 для которых надо выполнять распознавание. Если границы лексем всегда опреде- ляются (а выше было принято именно такое соглашение), то их можно определить по заданным терминальным символам и по символам начала следующей лексемы. Терминальные символы — это пробелы, знаки операций, символы комментариев, а также разделители (запятые, точки с запятой и др.). Набор таких терминальных символов может варьироваться в зависимости от входного языка. Важно отметить, что знаки операций сами также являются лексемами и необходимо не пропустить их при распознавании текста. Таким образом, алгоритм работы простейшего сканера можно описать так: □ просматривается входной поток символов программы на исходном языке до обнаружения очередного символа, ограничивающего лексему; □ для выбранной части входного потока выполняется функция распознавания лексемы; □ при успешном распознавании информация о выделенной лексеме заносится в таблицу лексем, и алгоритм возвращается к первому этапу; □ при неуспешном распознавании выдается сообщение об ошибке, а дальнейшие действия зависят от реализации сканера: либо его выполнение прекращается, - либо делается попытка распознать следующую лексему (идет возврат к перво- му этапу алгоритма). Работа программы-сканера продолжается до тех пор, пока не будут просмотрены все символы программы на исходном языке из входного потока. Требования к выполнению работы Порядок выполнения работы Для выполнения лабораторной работы требуется написать программу, которая выполняет лексический анализ входного текста в соответствии с заданием и по- рождает таблицу лексем с указанием их типов и значений. Текст на входном язы- ке задается в виде символьного (текстового) файла. Программа должна выдавать сообщения о наличии во входном тексте ошибок, которые могут быть обнаруже- ны на этапе лексического анализа. Длину идентификаторов и строковых констант можно считать ограниченной 32 сим- волами. Программа должна допускать наличие комментариев неограниченной дли- ны во входном файле. Форму организации комментариев предлагается выбрать самостоятельно. Лабораторная работа должна выполняться в следующем порядке: 1. Получить вариант задания у преподавателя. 2. Построить описание КА, лежащего в основе лексического анализатора (в виде набора множеств и функции переходов или в виде графа переходов). 3. Подготовить и защитить отчет.
46 Лабораторная работа № 2 • Проектирование лексического анализатора 4. Написать и отладить программу на ЭВМ. 5. Сдать работающую программу преподавателю. Требования к оформлению отчета Отчет должен содержать следующие разделы: □ Задание по лабораторной работе. □ Описание КС-грамматики входного языка в форме Бэкуса—Наура. □ Описание алгоритма работы сканера или граф переходов КА для распознава- ния цепочек (в соответствии с вариантом задания). □ Текст программы (оформляется после выполнения программы на ЭВМ). □ Выводы по проделанной работе. Основные контрольные вопросы □ Что такое трансляция, компиляция, транслятор, компилятор? □ Из каких процессов состоит компиляция? Расскажите об общей структуре компилятора. □ Какую роль выполняет лексический анализ в процессе компиляции? □ Что такое лексема? Расскажите, какие типы лексем существуют в языках про- граммирования. □ Как могут быть связаны между собой лексический и синтаксический анализ? □ Какие проблемы могут возникать при определении границ лексем в процессе лексического анализа? Как решаются эти проблемы? □ Что такое таблица лексем? Какая информация хранится в таблице лексем? □ В чем разница между таблицей лексем и таблицей идентификаторов? □ Что такое грамматика? Дайте определения грамматики. Как выглядит описа- ние грамматики в форме Бэкуса—Наура. □ Какие классы грамматик существуют? Что такое регулярные грамматики? □ Что такое конечный автомат? Дайте определение детерминированного и не- детерминированного конечных автоматов. □ Опишите алгоритм преобразования недетерминированного конечного автомата в детерминированный. □ Какие проблемы необходимо решить при построении сканера на основе ко- нечного автомата? □ Объясните общий алгоритм функционирования лексического анализатора. Варианты заданий 1. Входной язык содержит арифметические выражения, разделенные символом ; (точка с запятой). Арифметические выражения состоят из идентификаторов,
^aluHaustik Варианты заданий 47 десятичных чисел с плавающей точкой (в обычной и логарифмической фор- ме), знака присваивания (:=), знаков операций +, *, / и круглых скобок. 2. Входной язык содержит логические выражения, разделенные символом; (точка с запятой). Логические выражения состоят из идентификаторов, констант true и false, знака присваивания (:=), знаков операций or, xor, and, not и круглых скобок. 3. Входной язык содержит операторы условия типа if... then ... else и if... then, разделенные символом; (точка с запятой). Операторы условия содержат иден- тификаторы, знаки сравнения <, >, =, десятичные числа с плавающей точкой (в обычной и логарифмической форме), знак присваивания (:=). 4. Входной язык содержит операторы цикла типа for (...; ...; ...) do, разделенные символом ; (точка с запятой). Операторы цикла содержат идентификаторы, знаки сравнения <, >, =, десятичные числа с плавающей точкой (в обычной и логарифмической форме), знак присваивания (:=). 5. Входной язык содержит арифметические выражения, разделенные символом ; (точка с запятой). Арифметические выражения состоят из идентификаторов, римских чисел, знака присваивания (:=), знаков операций +, -, *, / и круглых скобок. 6. Входной язык содержит логические выражения, разделенные символом ; (точ- ка с запятой). Логические выражения состоят из идентификаторов, констант О и 1, знака присваивания (:=), знаков операций or, xor, and, not и круглых скобок. 7. Входной язык содержит операторы условия типа if... then ... else и if... then, разделенные символом; (точка с запятой). Операторы условия содержат иден- тификаторы, знаки сравнения <, >, =, римские числа, знак присваивания (:=). 8. Входной язык содержит операторы цикла типа for (...; ...; ...) do, разделенные символом ; (точка с запятой). Операторы цикла содержат идентификаторы, знаки сравнения <, >, =, римские числа, знак присваивания (:=). 9. Входной язык содержит арифметические выражения, разделенные символом ; (точка с запятой). Арифметические выражения состоят из идентификаторов, шестнадцатеричных чисел, знака присваивания (:=), знаков операций +, -, *, / и круглых скобок. 10. Входной язык содержит логические выражения, разделенные символом; (точка с запятой). Логические выражения состоят из идентификаторов, шестнадца- теричных чисел, знака присваивания (:=), знаков операций or, xor, and, not и круглых скобок. 11. Входной язык содержит операторы условия типа if... then ... else и if... then, разделенные символом; (точка с запятой). Операторы условия содержат иден- тификаторы, знаки сравнения <, >, =, шестнадцатеричные числа, знак присва- ивания (:=). 12. Входной язык содержит операторы цикла типа for (...; ...;...) do, разделенные символом ; (точка с запятой). Операторы цикла содержат идентификаторы, знаки сравнения <, >, =, шестнадцатеричные числа, знак присваивания (:=).
48 Лабораторная работа Ns 2 Проектирование лексического анализатора 13. Входной язык содержит арифметические выражения, разделенные символом ; (точка с запятой). Арифметические выражения состоят из идентификаторов, символьных констант (один символ в одинарных кавычках), знака присваива- ния (:=), знаков операций +, -, *, / и круглых скобок. 14. Входной язык содержит логические выражения, разделенные символом; (точка с запятой). Логические выражения состоят из идентификаторов, символьных констант 'Т и ’F’, знака присваивания (:=), знаков операций or, xor, and, not и круглых скобок. 15. Входной язык содержит операторы условия типа if... then... else и if... then, разделенные символом; (точка с запятой). Операторы условия содержат иден- тификаторы, знаки сравнения <,>,=, строковые константы (последователь- ность символов в двойных кавычках), знак присваивания (:=). 16. Входной язык содержит операторы цикла типа for (...;...;...) do, разделенные символом ; (точка с запятой). Операторы Цикла содержат идентификаторы, знаки сравнения <, >, =, строковые константы (последовательность символов в двойных кавычках), знак присваивания (:=). ПРИМЕЧАНИЕ------------------------------------------------------------ Римскими числами считать последовательности заглавных латинских букв X, V и I; шестнадцатеричными числами считать последовательность цифр и символов «а», «Ь», «с», «d, «е» и «Ь», начинающуюся с цифры (например: 89, 45ас9, 0abc4); задание по лабораторной работе № 2 взаимосвязано с заданием по лабораторной рабо- те № 3, для уточнения состава входного языка можно посмотреть грамматику, заданную в работе Ns 3 по соответствующему варианту. Пример выполнения работы Задание для примера В качестве задания для примера возьмем входной язык, который содержит набор условных операторов условия типа jf... then... else и if... then, разделенных сим- волом ; (точка с запятой). Эти операторы в качестве условия содержат логические выражения, построенные с помощью операций or, xor и and, операндами которых являются идентификаторы и целые десятичные константы без знака. В исполни- тельной части эти операторы содержат или оператор присваивания переменной логического выражения (:=), или другой условный оператор. Комментарий будет организован в виде последовательности символов, начинаю- щейся с открывающей фигурной скобки ({) и заканчивающейся закрывающей фигурной скобкой (}). Комментарий может содержать любые алфавитно-цифро- вые символы, в том числе и символы национальных алфавитов. Грамматика входного языка Описанный выше входной язык может быть построен с помощью КС-граммати- ки G({if,then,else,a,:=,or,xor,and,(,),;},{5,F,£,D,C},P,5) с правилами Р:
L^atattausilk Пример выполнения работы 49 S->F; F^> iff then T else F| if E then F| a := E T—> if E then Telse T| a := E E —> E or D | E xor D | D D-+ D and C | C C->z\(E) Описание грамматики построено в форме Бэкуса—Наура. Жирным шрифтом в грамматике и в правилах выделены терминальные символы. Выбранный в качестве примера язык и задающая его грамматика не совпадают ни с одним из предложенных выше вариантов. С другой стороны, на этом примере можно проиллюстрировать многие особенности построения лексического, а впо- следствии — и синтаксического распознавателя, присущие различным вариантам. Он содержит как условные операторы, связанные с передачей управления в то или иное место исходной программы, так и линейные операции в форме вычисления логических выражений. Поэтому данный пример выбран в качестве иллюстрации для лабораторной работы № 2, а позже будет использоваться также в лаборатор- ных работах № 3 и 4. Описание конечного автомата для распознавания лексем входного языка Задача лексического анализатора для описанного выше языка заключается в том, чтобы распознавать и выделять в исходном тексте программы все лексемы этого языка. Лексемами данного языка являются: □ шесть ключевых слов языка (if, then, else, or, xor и and); О разделители: открывающая и закрывающая круглые скобки, точка с запятой; □ знак операции присваивания; □ идентификаторы; □ целые десятичные константы без знака. Кроме перечисленных лексем распознаватель должен уметь определять и исклю- чать из входного текста комментарии, принцип построения которых описан выше. Для выделения комментариев ключевыми символами должны быть открываю- щая и закрывающая фигурные скобки. Для перечисленных типов лексем и комментария можно построить регулярную грамматику, а затем на ее основе»создать КА. Однако построенная таким образом грамматика, с одной стороны, будет элементарно простой, с другой стороны — громоздкой и малоинформативной. Поэтому можно пойти путем построения КА непосредственно по описанию лексем. Для этого не хватает только описания иден- тификаторов и целых десятичных констант без знака: □ идентификатор — это произвольная последовательность малых и прописных букв латинского алфавита (от А до Z и от а до z), цифр (от 0 до 9) и знака подчеркивания (_), начинающаяся с буквы или со знака подчеркивания;
50 Лабораторная работа № 2 • Проектирование лексического анализатора □ целое десятичное число без знака — это произвольная последовательность цифр (от 0 до 9), начинающаяся с любой цифры. Границами лексем для данного распознавателя будут служить пробел, знак табу- ляции, знаки перевода строки и возврата каретки, а также круглые скобки, откры- вающая фигурная скобка, точка с запятой и знак двоеточия. При этом следует помнить, что круглые скобки и точка с запятой сами по себе являются лексема- ми, открывающая фигурная скобка начинает комментарий, а знак двоеточия, яв- ляясь границей лексемы, в то же время является и началом другой лексемы — опе- рации присваивания. В данном языке лексический анализатор всегда может однозначно определить границы лексемы, поэтому нет необходимости в его взаимодействии с синтакси- ческим анализатором и другими элементами компилятора. Рис. 2.1. Фрагмент графа переходов КА для распознавания всех лексем, кроме ключевых слов Полный граф переходов КА будет очень громоздким и неудобным для просмот- ра, поэтому проиллюстрируем его несколькими фрагментами. На рис. 2.1 изобра-
Пример выполнения работы 51 жен фрагмент графа переходов КА, отвечающий за распознавание разделителей, комментариев, знака присваивания, переменных и констант (всех лексем входно- го языка, кроме ключевых слов). На рис. 2.2 изображен фрагмент графа переходов КА, отвечающий за распознава- ние ключевых слов if и then (этот фрагмент имеет ссылки на состояния, изобра- женные на рис. 2.1). Аналогичные фрагменты можно построить и для других клю- чевых слов. Рис. 2.2. Фрагмент графа переходов КА для ключевых слов if и then На фрагментах графа переходов КА, изображенных на рис. 2.1 и 2.2, приняты сле- дующие обозначения: □ А — любой алфавитно-цифровой символ; □ А(*) — любой алфавитно-цифровой символ, кроме перечисленных в скобках; □ П — любой незначащий символ (пробел, знак табуляции, перевод строки, воз- врат каретки); □ Б — любая буква английского алфавита (прописная или строчная) или сим- вол подчеркивания (_);
52 Лабораторная работа № 2 • Проектирование лексического анализатора □ Б(*) — любая буква английского алфавита (прописная или строчная) или сим- вол подчеркивания (_), кроме перечисленных в скобках; □ Ц — любая цифра от 0 до 9; □ F — функция обработки таблицы лексем, вызываемая при переходе КА из од- ного состояния в другое. Обозначения ее аргументов: v — переменная, запомненная при работе КА; d — константа, запомненная при работе КА; а — текущий входной символ КА. С учетом этих обозначений, полностью КА можно описать следующим образом: M(Q,Z,S,9o,F): Q- {Н, С, G, V, D, И, 12, Tl, Т2, ТЗ, Т4, El, Е2, ЕЗ, Е4, О1, 02, XI, Х2, ХЗ, Al, А2, АЗ, F} Е = А (все допустимые алфавитно-цифровые символы); < 7о = Н; F = {E}. Функция переходов (б) для этого КА приведена в приложении 2. Из начального состояния КА литеры «i», «t», «е», «о», «х» и «а» ведут в начало цепочек состояний, каждая из которых соответствует ключевому слову: □ состояния И, 12 — ключевому слову if; □ состояния Tl, Т2, ТЗ, Т4 — ключевому слову then; □ состояния El, Е2, ЕЗ, Е4 — ключевому слову else; □ состояния О1, 02 — ключевому слову or; □ состояния XI, Х2, ХЗ — ключевому слову хог; □ состояния Al, А2, АЗ — ключевому слову and. Остальные литеры ведут к состоянию, соответствующему переменной (иденти- фикатору), — V. Если в какой-то из цепочек встречается литера, не соответству- ющая ключевому слову, или цифра, то КА также переходит в состояние V, а если встречается граница лексемы — запоминает уже прочитанную часть ключевого слова как переменную (чтобы правильно выделять такие идентификаторы, как «Ь> или «els», которые совпадают с началом ключевых слов). Цифры ведут в состояние, соответствующее входной константе, — D. Открываю- щая фигурная скобка ведет в состояние С, которое соответствует обнаружению комментария — из этого состояния КА выходит, только если получит на вход зак- рывающую фигурную скобку. Еще одно состояние — G — соответствует лексеме «знак присваивания». В него КА переходит, получив на вход двоеточие, и ожида- ет в этом состоянии символа «равенство». Состояние Н — начальное состояние КА, а состояние F — его конечное состоя- ние. Поскольку КА работает с непрерывным потоком лексем, перейдя в конечное состояние, он тут же должен возвращаться в начальное, чтобы распознавать оче- редную лексему. Поэтому в моделирующей программе эти два состояния можно объединить.
Пример выполнения работы 53 На графе и йри описании функции переходов не обозначено состояние «ошиб- ка», чтобы не загромождать и без того сложный граф и функцию. В это состояние КА переходит всегда, когда получает на вход символ, по которому нет переходов из текущего состояния. Функция F, которой помечены дуги КА на графе и переходы в функции перехо- дов, соответствует выполнению записи данных в таблицу лексем. Аргументы функции зависят от текущего состояния КА. В реализации программы, моделирую- щей функционирование КА, этой функции должны соответствовать несколько функций, вызываемые в зависимости от текущего состояния и входного символа. Надо отметить, что для корректной записи переменных и констант в таблицу лек- сем КА должен запоминать соответствующие им цепочки символов. Проще всего это делать, запоминая позицию считывающей головки КА всякий раз, когда он находится в состоянии Н. Можно заметить, что функция переходов КА получилась довольно громоздкой, хотя и простой по своей сути (для всех ключевых слов она работает однотипно). В реализации функционирования КА проще было бы не выделять отдельные со- стояния для ключевых слов, а переходить всегда по обнаружению буквы на входе КА в состояние V. Тогда проверку того, является ли считанная строка ключевым словом или же идентификатором, можно было бы выполнять на момент ее записи в таблицу лексем с помощью стандартных операций сравнения строк. Граф пере- ходов КА в таком варианте был бы намного компактнее — он выглядел бы точно так же, как фрагмент, представленный на рис. 2.1. Его можно назвать «сокращен- ным» графом переходов КА (или «сокращенным КА»). Но следует отметить, что, несмотря на большую наглядность и простоту реализа- ции, сокращенный КА будет менее эффективным, поскольку в момент записи лек- семы в таблицу он должен будет выполнять ее сравнение со всеми известными ключевыми словами (в данном случае надо определять шесть ключевых слов — следовательно, будет выполняться шесть сравнений строк). То есть такой КА бу- дет повторно просматривать уже прочитанную часть входной цепочки, да еще и несколько раз! И хотя в явном виде в реализации сокращенного КА эта опера- ция не присутствует, она все равно будет выполняться в вызове библиотечной функции сравнения строк. Итак, хотя сокращенный КА меньше по количеству состояний и проще в реали- зации, он является менее эффективным, чем полный КА, построенный на анализе всех входных лексем. Тем не менее оба варианта реализации КА обеспечивают построение требуемого лексического анализатора. Какой из них выбрать, решает разработчик компилятора. Реализация лексического анализатора Разбиение на модули Модули, реализующие лексический анализатор, разделены на две группы: □ модули, программный код которых не зависит от входного языка; □ модули, программный код которых зависит от входного языка.
54 Лабораторная работа Ne 2 • Проектирование лексического анализатора В первую группу входят модули: □ LexEl em — описывает структуру данных элемента таблицы лексем; □ FormLab2 — описывает интерфейс с пользователем. Во вторую группу входят модули: □ LexType — описывает типы входных лексем, связанные с ними наименования и текстовую информацию; □ LexAuto — реализует функционирование КА. Такое разбиение на модули позволяет использовать те же самые структуры данных для организации лексического распознавателя при изменении входного языка. Кроме этих модулей для реализации лабораторной работы № 2 используются так- же программные модули (ТЫ Е1 его и FncTree), позволяющие работать с комбиниро- ванной таблицей идентификаторов, которые были созданы при выполнении ла- бораторной работы № 1. Эти два модуля, очевидно, также не зависят от входного языка. Кратко опишем содержание программных модулей, используемых для организа- ции лексического анализатора. Модуль типов лексем Модуль LexType в детальных комментариях не нуждается. В нем перечислены все допустимые типы лексем (тип данных TLexType), каждой из которых соответствует наименование и обозначение лексемы. Вывод наименований лексем обеспечивает функция LexTypeName, а вывод обозначений — функция LexTypelnfo. Следует отме- тить, что кроме перечисленных в задании лексем используется еще одна дополни- тельная информационная лексема (LEX START), обозначающая конец строки. Модуль LexEl em описывает структуры данных элемента таблицы лексем (TLexem) и самой таблицы лексем (TLexList), а также все, что с ними связано. Модуль структур данных таблицы идентификаторов Структура данных таблицы лексем содержит информацию о лексеме (поле Lexlnfo). В этом поле содержится тип лексемд (LexType), а также следующие данные: □ Varlnfo — ссылку на элемент таблицы идентификаторов для лексем типа «пе- ременная»; □ ConstVal — целочисленное значение для лексем типа «константа»; □ szlnfo — произвольная строка для информационной лексемы. Для лексем других типов не требуется никакой дополнительной информации. Следует отметить, что для лексем типа «переменная» хранится именно ссылка на таблицу идентификаторов, а не имя переменной. Именно для этого в данной ла- бораторной работе используются модули из лабораторной работы № 1. Для само- го лексического анализатора не имеет значения, что хранить в таблице лексем — ссылку на таблицу идентификаторов со всей информацией о переменной или же
Пример выполнения работы 55 только имя переменной. Но реализация лексического анализатора, при которой хранится именно ссылка на таблицу идентификаторов, чрезвычайно удобна для дальнейшей обработки данных, что будет очевидно в последующих работах (ла- бораторных работах № 3 и № 4). Поскольку лексический анализатор интересен не сам по себе, а в составе компилятора, такой подход принципиально важен. Кроме этого в структуре данных элемента таблицы лексем хранится информация о позиции лексемы в тексте входной программы: □ тStr — номер строки, где встретилась лексема; □ iPos — позиция лексемы в строке; □ тAl 1Р — позиция лексемы относительно начала входного файла. Эта информация будет полезна, в частности, при информировании пользователя об ошибках. Кроме этих данных структура содержит также: □ четыре конструктора для создания лексем четырех разных типов: CreateVar — для создания лексем типа «переменная»; CreateConst — для создания лексем, типа «константа»; Createlnfo — для создания информационных лексем; CreateKey — для создания лексем других типов; □ деструктор Destroy для освобождения памяти, занятой лексемой (важен для информационных лексем); □ свойства и функции для доступа к информации о лексеме. Хранить в структуре строку самой лексемы нет никакой необходимости (для пе- ременных строка хранится в таблице идентификаторов, для других типов лексем она просто не нужна). Сама таблица лексем (тип данных TLexList) построена на основе динамического массива TList из библиотеки VCL (модуль Classes) системы программирования Delphi 5. Динамический массив типа TList обеспечивает все функции и данные, необходи- мые для хранения в памяти произвольного количества лексем (максимальное ко- личество лексем ограничено только объемом доступной оперативной памяти). Для таблицы лексем TLexList дополнительно реализованы функции очистки таблицы, которые освобождают память, запятую лексемами, при их удалении из таблицы (функция Cl ear и деструктор Destroy), а также функция GetLexem и свойство Lexem, обеспечивающие удобный доступ к любой лексеме в таблице по ее индексу (по- рядковому номеру). Модуль моделирования работы КА Модуль LexAuto, моделирующий работу КА, на основе которого построен лекси- ческий распознаватель, — самый значительный по объему программного кода. Однако по содержанию программного кода он предельно прост. Этот модуль обес-
56 Лабораторная работа № 2 • Проектирование лексического анализатора печивает функционирование полного КА, фрагменты графа переходов которого были изображены на рис. 2.1 и 2.2, а функция переходов была построена выше. Главной составляющей этого программного модуля является функция Маке- LexList, которая непосредственно моделирует работу КА. На вход функции по- дается входная программа в виде списка строк (формальный параметр 1 i stFile) и таблица лексем, куда должны помещаться найденные лексемы (формальный параметр listLex). Результатом работы функции является 0, если лексический анализ выполнен без ошибок, а если ошибка обнаружена — номер строки в ис- ходном файле, в которой она присутствует. Для более подробной информации об обнаруженной ошибке функция создает информационную лексему и поме- щает ее в конец таблицы лексем. Сама информационная лексема кроме тексто- вой информации об ошибке содержит еще дополнительную информацию о ее местонахождении в исходной программе (смещение от начала файла и длина ошибочной лексемы). » В типе данных TAutoPos перечислены все возможные состояния КА. Перечень со- стояний полностью соответствует функции переходов КА. Реализация функции MakeLexList, несмотря на большой объем программного кода, предельно проста. Она построена на основе двух вложенных циклов (первый — по строкам входного списка; второй — по символам в текущей строке), внутри которых находятся два уровня вложенных оператора выбора типа case — типич- ный подход к моделированию функционирования КА. Внешний оператор case выполняется по всем возможным состояниям автомата, a case второго уровня — по допустимым входным символам в каждом состоянии. Можно обратить внимание на шесть вспомогательных функций: □ AddVarToList — добавление лексемы типа «переменная» в таблицу лексем; □ AddVarKeyToLi st — добавление Лексем типа «переменная» и типа «разделитель» в таблицу лексем; □ AddConstToList — добавление лексемы типа «константа» в таблицу лексем; □ AddConstKeyToLi st — добавление лексем типа «константа» и типа «разделитель» в таблицу лексем; □ AddKeyToLi st — добавление лексемы типа «ключевое слово» или «разделитель» в таблицу лексем; □ Add2KeysToList — добавление лексем типа «ключевое слово» и «разделитель» в таблицу лексем подряд. Эти функции, по сути, являются реализацией функции, которая на графе перехо- дов КА была обозначена F. Еще две вспомогательные функции служат для упрощения кода. Они выполняют часто повторяющиеся действия в состояниях автомата, которые связаны со сред- ними символами ключевых слов (в функции переходов эти состояния обозначе- ны Т2, ТЗ, Е2, ЕЗ, Х2 и А2) и завершающими символами ключевых слов (в функ- ции переходов эти состояния обозначены 12, Т4, Е4, 02, ХЗ и АЗ).
^ataHaus,^. Пример выполнения работы 57 Построенный лексический анализатор обнаруживает три типа ошибок: □ неверный символ в лексеме (например, сочетания «2а» или «:6» будут призна- ны неверными символами в лексемах); □ незакрытый комментарий (присутствует открывающая фигурная скобка, но отсутствует соответствующая ей закрывающая); □ незавершенная лексема (в данном входном языке это может быть только сим- вол «:» в конце входной программы, который будет воспринят как начало не- завершенной лексемы «:=»). Остальные ошибки входного языка должен обнаруживать синтаксический ана- лизатор. В качестве еще одной особенности реализации можно отметить, что переход с од- ной строки входного списка на другую должен восприниматься как граница теку- щей лексемы, так как одна лексема не может быть разбита на две строки — имен- но это и реализовано в конце цикла по символам текущей строки. Текст программы распознавателя Кроме перечисленных выше модулей необходим еще модуль, обеспечивающий интерфейс с пользователем. Как и в лабораторной работе № 1, этот модуль (Form- Lab2) реализует графическое окно TLab2Form на основе класса TForm библиотеки VCL и включает в себя две составляющие: □ файл программного кода (файл FormLab2. pas); □ файл описания ресурсов пользовательского интерфейса (файл FormLab2.dfm). Кроме описания интерфейсной формы и ее органов управления модуль FormLab2 содержит переменную (listLex), в которую записывается ссылка на таблицу лек- сем. Интерфейсная форма, описанная в модуле, содержит следующие основные орга- ны управления: □ многостраничную вкладку (PageControll) с двумя закладками (SheetFile и SheetLexems) под названиями «Исходный файл» и «Таблица лексем» соответ- ственно; □ на закладке SheetFile: поле ввода имени файла (EditFile), кнопка выбора имени файла из катало- гов файловой системы (BthFile), кнопка чтения файла (BtnLoad); многострочное поле для отображения прочитанного файла (Listldents); □ на закладке SheetLexems: сетка (GridLex) с тремя колонками для отображения данных о прочитанных лексемах; □ кнопка завершения работы с программой (BtnExit).
58 Лабораторная работа № 2 • Проектирование лексического анализатора Внешний вид двух закладок этой формы приведен на рис. 2.3 и 2.4. Рис. 2.3. Внешний вид первой закладки интерфейсной формы для лабораторной работы № 2 Рис. 2.4. Внешний вид второй закладки интерфейсной формы для лабораторной работы № 2
Пример выполнения работы 59 Чтение содержимого входного файла организовано точно так же, как в лабора- торной работе № 1. После чтения файла создается таблица лексем (ссылка на нее запоминается в пе- ременной listLex) и вызывается функция MakeLexList, результат работы которой помещается во временную переменную 1Егг. Если обнаружена ошибка, пользователю выдается сообщение об этом и указатель в списке строк позиционируется на место, где обнаружена ошибка. Если ошибок не обнаружено, то на основании считанной таблицы лексем listLex заполняется сетка GridLex, которая очень удобна для наглядного представления таблицы лексем: □ первая колонка — порядковый номер лексемы; □ вторая колонка — тип лексемы (ее внешний вид); □ третья колонка — информация о лексеме. Полный текст программного кода модуля интерфейса с пользователем приведен в листинге П2.4 в приложении 2, а описание ресурсов пользовательского интер- фейса — в листинге П2.5 в приложении 2. Полный текст всех программных модулей, реализующих рассмотренный пример для лабораторной работы № 2, приведен в приложении 2. Выводы по проделанной работе В результате лабораторной работы № 2 построен лексический анализатор на ос- нове конечного автомата. Построенный лексический анализатор позволяет выде- лять в тексте исходной программы лексемы следующих типов: □ ключевые слова (if, then, else, or, xor и and); □ идентификаторы (при этом в именах идентификаторов различаются строчные и прописные английские буквы); □ знак операции присваивания; □ целые десятичные константы без знака; □ разделители (круглые скобки и точка с запятой). Лексический анализатор игнорирует в тексте входной программы пробелы, зна- ки табуляции и переводы строки, а также комментарии, выделенные фигурными скобками. В случае обнаружения неверной лексемы (например числа, содержащего букву), незакрытого комментария или незавершенной лексемы (такой лексемой может быть только символ «:») лексический анализатор выдает сообщение об ошибке и прекращает дальнейший анализ. При наличии нескольких неверных лексем ана- лизатор обнаруживает только первую из них. Результатом выполнения лексического анализа является структура данных, ко- торая представляет таблицу лексем. Построенный лексический анализатор пред- назначен для подготовки данных, необходимых для выполнения следующих ла- бораторных работ, связанных с синтаксическим анализом и генерацией кода.
ЛАБОРАТОРНАЯ РАБОТА № 3 Построение простейшего дерева вывода Цель работы Цель работы: изучение основных понятий теории грамматик простого и опера- торного предшествования, ознакомление с алгоритмами синтаксического анали- за (разбора) для некоторых классов КС-грамматик, получение практических на- выков создания простейшего синтаксического анализатора для заданной грамма- тики операторного предшествования. Краткие теоретические сведения Назначение синтаксического анализатора По иерархии грамматик Хомского выделяют четыре основные группы языков (и описывающих их грамматик) [1, 3, 4, 7]. При этом наибольший интерес пред- ставляют регулярные и контекстно-свободные (КС) грамматики и языки. Они ис- пользуются при описании синтаксиса языков программирования. С помощью ре- гулярных грамматик можно описать лексемы языка — идентификаторы, констан- ты, служебные слова и прочие. На основе КС-грамматик строятся более крупные синтаксические конструкции: описания типов и переменных, арифметические и логические выражения, управляющие операторы и, наконец, полностью вся про- грамма на входном языке.
Краткие теоретические сведения 61 Входные цепочки регулярных языков распознаются с помощью конечных авто- матов (КА). Они лежат в основе сканеров, выполняющих лексический анализ и выделение слов в тексте программы на входном языке. Результатом работы ска- нера является преобразование исходной программы в список или таблицу лек- сем. Дальнейшую ее обработку выполняет другая часть компилятора — синтак- сический анализатор. Его работа основана на использовании правил КС-грамма- тики, описывающих конструкции исходного языка. Синтаксический анализатор (синтаксический разборщик) — это часть компиля- тора, которая отвечает за выявление и проверку синтаксических конструкций входного языка. В задачу синтаксического анализатора входит: □ найти и выделить синтаксические конструкции в тексте исходной программы; □ установить тип и проверить правильность каждой синтаксической конструк- ции; □ представить синтаксические конструкции в виде, удобном для дальнейшей генерации текста результирующей программы. Синтаксический анализатор — это основная часть компилятора на этапе анализа. Без выполнения синтаксического разбора работа компилятора бессмысленна, в то время как лексический разбор, в принципе, не является обязательной фазой ком- пиляции. Все задачи по проверке синтаксиса входного языка могут быть решены на этапе синтаксического разбора. Лексический анализатор только позволяет из- бавить сложный по структуре синтаксический анализатор от решения примити 15- ных задач по выявлению и запоминанию лексем исходной, программы. Выходом лексического анализатора является таблица лексем. Эта таблица обра- зует вход синтаксического анализатора, который исследует только один компо- нент каждой лексемы — ее тип. Остальная информация о лексемах используется на более поздних фазах компиляции при семантическом анализе, подготовке к ге- нерации и генерации кода результирующей программы. Синтаксический анализатор воспринимает выход лексического анализатора и раз- бирает его в соответствии с грамматикой входного языка. Однако в грамматике входного языка программирования обычно не уточняется, какие конструкции следует считать лексемами. Примерами конструкций, которые обычно распозна- ются во время лексического анализа, служат ключевые слова, константы и иден- тификаторы. Но эти же конструкции могут распознаваться и синтаксическим ана- лизатором. На практике не существует жесткого правила, определяющего, какие конструкции должны распознаваться на лексическом уровне, а какие надо остав- лять синтаксическому анализатору. Обычно это определяет разработчик компи- лятора исходя из технологических аспектов программирования, а также синтак- сиса и семантики входного языка. Принципы взаимодействия лексического и син- таксического анализаторов были рассмотрены в лабораторной работе № 2. В основе синтаксического анализатора лежит распознаватель текста исходной программы, построенный на основе грамматики входного языка. Как правило, синтаксические конструкции языков программирования могут быть описаны с по- мощью КС-грамматик; реже встречаются языки, которые могут быть описаны с по- мощью регулярных грамматик.
62 Лабораторная работа № 3 • Построение простейшего дерева вывода Главную роль в том, как функционирует синтаксический анализатор и какой ал- горитм лежит в его основе, играют принципы построения распознавателей для КС-языков. Без применения этих принципов невозможно выполнить эффектив- ный синтаксический разбор предложений входного языка. Проблема распознавания цепочек КС-языков Взаимодействие лексического и синтаксического анализаторов рассматривалось в предыдущей лабораторной работе, здесь же будут рассмотрены алгоритмы, ле- жащие в основе синтаксического анализа. Перед синтаксическим анализатором стоят две основные задачи: проверить правильность конструкций программы, ко- торая представляется в виде уже выделенных слов входного языка, и преобразо- вать ее в вид, удобный для дальнейшей семантической (смысловой) обработки и генерации кода. Одним из способов такого представления является дерево син- таксического разбора. Основой для построения распознавателей КС-языков являются автоматы с мага- зинной памятью — МП-автоматы — односторонние недетерминированные распо- знаватели с линейно-ограниченной магазинной памятью (полная классификация распознавателей приведена в [1, 4, 3, 7]). Поэтому важно рассмотреть, как функ- ционирует МП-автомат и как для КС-языков решается задача разбора — постро- ение распознавателя языка на основе заданной грамматики. Далее рассмотрены технические аспекты, связанные с реализацией синтаксических анализаторов. МП-автомат в отличие от обычного КА имеет стек (магазин), в который можно помещать специальные «магазинные» символы (обычно это терминальные и не- терминальные символы грамматики языка). Переход МП-автомата из одного со- стояния в другое зависит не только от входного символа, но и от одного или не- скольких верхних символов стека. Таким образом, конфигурация автомата опре- деляется тремя параметрами: состоянием автомата, текущим символом входной цепочки (положением указателя в цепочке) и содержимым стека. При выполнении перехода МП-автомата из одной конфигурации в другую из стека удаляются верхние символы, соответствующие условию перехода, и добавляется цепочка, соответствующая правилу перехода. Первый символ цепочки становит- ся верхушкой стека. Допускаются переходы, при которых входной символ игно- рируется (и тем самым он будет входным символом при следующем переходе). Эти переходы называются Х-переходами. Если при окончании цепочки автомат находится в одном из заданных конечных состояний, а стек пуст, цепочка счита- ется принятой (после окончания цепочки могут быть сделаны Х-переходы). Ина- че цепочка символов не принимается. МП-автомат называется недетерминированным, если при одной и той же его кон- фигурации возможен более чем один переход. В противном случае (если из любой конфигурации МП-автомата по любому входному символу возможно не более од- ного перехода в следующую конфигурацию) МП-автомат считается детерминиро- ванным (ДМП-автоматом). ДМП-автоматы задают класс детерминированных КС- языков, для которых существуют однозначные КС-грамматики. Именно этот класс языковчтежит в основе синтаксических конструкций всех языков программирова-
Краткие теоретические сведения 63 ния, так как любая синтаксическая конструкция языка программирования должна допускать только однозначную трактовку [1-4,7]. По произвольной КС-грамматике G(VN,VT,P,5), V = VToVN всегда можно по- строить недетерминированный МП-автомат, который допускает цепочки языка, заданного этой грамматикой [1-3, 7]. А на основе этого МП-автомата можно со- здать распознаватель для заданного языка. Однако при алгоритмической реализации функционирования такого распозна- вателя могут возникнуть проблемы. Дело в том, что, построенный МП-автомат будет, как правило, недетерминированным, а для МП-автоматов, в отличие от обычных КА, не существует алгоритма, который позволял бы преобразовать про- извольный МП-автомат в ДМП-автомат. Поэтому программирование функцио- нирования МП-автомата — нетривиальная задача. Если моделировать его функ- ционирование по шагам с перебором всех возможных состояний, то может ока- заться, что построенный для тривиального МП-автомата алгоритм никогда не завершится на конечной входной цепочке символов при определенных условиях. Примеры таких Mil-автоматов можно найти в [1, 3, 7]. Поэтому для построения распознавателя для языка, заданного КС-грамматикой, рекомендуется воспользоваться соответствующим математическим аппаратом и одним из существующих алгоритмов. Виды распознавателей для КС-языков Существуют несложные преобразования КС-грамматик, выполнение которых гарантирует, что построенный на основе преобразованной грамматики МП-авто- мат можно будет промоделировать за конечное время на основе конечных вычис- лительных ресурсов. Описание сути и алгоритмов этих преобразований можно найти в [1,3,7]. Эти преобразования позволяют строить два основных типа простейших распо- знавателей: □ распознаватель с подбором альтернатив; □ распознаватель на основе алгоритма «сдвиг-свертка». Работу распознавателя с подбором альтернатив можно неформально описать сле- дующим образом: если на верхушке стека МП-автомата находится нетерминаль- ный символ А, то его можно заменить на цепочку символов а при условии, что в грамматике языка есть правило А —> а, не сдвигая при этом считывающую го- ловку автомата (этот шаг работы называется «подбор альтернативы»); если же на верхушке стека находится терминальный символ а, который совпадает с текущим символом входной цепочки, то этот символ можно выбросить из стека и передви- нуть считывающую головку на одну позицию вправо (этот шаг работы называет- ся «выброс»). Данный МП-автомат может быть недетерминированным, посколь- ку при подборе альтернативы в грамматике языка может оказаться более одного правила вида А а, тогда функция 6(<7,Х Л ) будет содержать более одного следу- ющего состояния — у МП-автомата будет несколько альтернатив.
64 Лабораторная работа № 3 • Построение простейшего дерева вывода Решение о том, выполнять ли на каждом шаге работы МП-автомата выброс или подбор альтернативы, принимается однозначно. Моделирующий алгоритм дол- жен обеспечивать выбор одной из возможных альтернатив и хранение информа- ции о том, какие альтернативы на каком шаге уже были выбраны, чтобы иметь возможность вернуться к этому шагу и подобрать другие альтернативы. Распознаватель с подбором альтернатив является нисходящим распознавателем: он читает входную цепочку символов слева направо и строит левосторонний вы- вод. Название «нисходящий» дано ему потому, что дерево вывода в этом случае следует строить сверху вниз, от корня к концевым вершинам («листьям»)1. Работу распознавателя на основе алгоритма «сдвиг-свертка» можно описать так: если на верхушке стека МП-автомата находится цепочка символов у, то ее можно заменить на нетерминальный символ А при условии, что в грамматике языка су- ществует правило вида А у, не сдвигая при этом считывающую головку автома- та (этот шаг работы называется «свертка»); с другой стороны, если считывающая головка автомата обозревает некоторый символ входной цепочки с, то его можно поместить в стек, сдвинув при этом головку на одну позицию вправо (этот шаг работы называется «сдвиг» или «перенос»). Этот распознаватель потенциально имеет больше неоднозначностей, чем рассмот- ренный выше распознаватель, основанный на алгоритме подбора альтернатив. На каждом шаге работы автомата надо решать следующие вопросы: □ что необходимо выполнять: сдвиг или свертку; □ если выполнять свертку, то какую цепочку у выбрать для поиска правил (це- почка у должна встречаться в правой части правил грамматики); □ какое правило выбрать для свертки, если окажется, что существует несколько правил вида Д —> у (несколько правил с одинаковой правой частью). Для моделирования работы этого расширенного МП-автомата надо на каждом шаге запоминать все предпринятые действия, Чтобы иметь возможность вернуться к уже сделанному шагу и выполнить эти же действия по-другому. Этот процесс дол- жен повторяться до тех пор, пока не будут перебраны все возможные варианты. Распознаватель на основе алгоритма «сдвиг-свертка» является восходящим рас- познавателем: он читает входную цепочку символов слева направо и строит пра- восторонний вывод. Название «восходящий» дано ему потому, что дерево вывода в этом случае следует строить снизу вверх, от концевых вершин к корню. Функционирование обоих рассмотренных распознавателей реализуется достаточ- но простыми алгоритмами, которые можно найти в [3,7]. Однако оба они имеют один существенный недостаток — время их функционирования экспоненциаль- но зависит от длины входной цепочки п = |а|, что недопустимо для компиляторов, где длина входных программ составляет от десятков до сотен тысяч символов. Так происходит потому, что оба алгоритма выполняют разбор входной цепочки символов методом простого перебора, подбирая правила грамматики произволь- В отличие от обычных деревьев, корень у синтаксического дерева вывода находится вверху, а листья — внизу.
^aiaHaus,^. Краткие теоретические сведения 65 ным образом, а в случае неудачи возвращаются к уже прочитанной части вход- ной цепочки и пытаются подобрать другие правила. Существуют более эффективные табличные распознаватели, построенные на осно- ве алгоритмов Эрли и Кока—Янгера—Касами [1,3]. Они обеспечивают полиноми- альную зависимость времени функционирования от длины входной цепочки (и3 для произвольного МП-автомата и п~ для ДМП-автомата). Это самые эффек- тивные из универсальных распознавателей для КС-языков. Но и полиномиальную зависимость времени разбора от длины входной цепочки нельзя признать удовлет- ворительной. Лучших универсальных распознавателей не существует. Однако среди всего типа КС-языков существует множество классов и подклассов языков, для которых можно построить распознаватели, имеющие линейную зависимость времени функ- ционирования от длины входной цепочки символов. Такие распознаватели назы- вают линейными распознавателями КС-языков. В настоящее время известно множество линейных распознавателей и соответству- ющих им классов КС-языков. Каждый из них имеет свой алгоритм функциони- рования, но все известные алгоритмы являются модификацией двух базовых ал- горитмов — алгоритма с подбором альтернатив и алгоритма «сдвиг-свертка», рас- смотренных выше. Модификации заключаются в том, что алгоритмы выполняют подбор правил грамматики для разбора входной цепочки символов не произволь- ным образом, а руководствуясь установленным порядком, который создается за- ранее на основе заданной КС-грамматики. Такой подход позволяет избежать воз- вратов к уже прочитанной части цепочки и существенно сокращает время, требу- емое на ее разбор. Среди всего множества можно выделить следующие наиболее часто используе- мые распознаватели: ' □ распознаватели па основе рекурсивного спуска (модификация алгоритма с под- бором альтернатив); □ распознаватели на основе LL( 1)- и ££(£)-грамматик (модификация алгоритма с подбором альтернатив); □ распознаватели на основе £/?(0)- и LR( 1 )-грамматик (модификация алгорит- ма «сдвиг-свертка»); □ распознаватели на основе SLR( 1)- и LALR( 1 )-грамматик (модификация алго- ритма «сдвиг-свертка»); □ распознаватели на основе грамматик предшествования (модификация алго- ритма «сдвиг-свертка»). Алгоритмы функционирования всех перечисленных и ряда других линейных рас- познавателей описаны в [1-4, 7]. Построение синтаксического анализатора Синтаксический анализатор должен распознавать весь текст исходной програм- мы. Поэтому, в отличие от лексического анализатора, ему нет необходимости ис- 3 Зак. 6 IS
66 Лабораторная работа № 3 • Построение простейшего дерева вывода кать границы распознаваемой строки символов. Он должен воспринимать всю информацию, поступающую ему на вход, и либо подтвердить ее принадлежность входному языку, либо сообщить об ошибке в исходной программе. Но, как и в случае лексического анализа, задача синтаксического анализа не огра- ничивается только проверкой принадлежности цепочки заданному языку. Необ- ходимо оформить найденные синтаксические конструкции для дальнейшей гене- рации текста результирующей программы. Синтаксический анализатор должен иметь некий выходной язык, с помощью которого он передает следующим фазам компиляции информацию о найденных и разобранных синтаксических структу- рах. В таком случае он уже является не разновидностью МП-автомата, а преобра- зователем с магазинной памятью — МП-преобразователем [1, 2, 7]. Вопросы, связанные с представлением информации, являющейся результатом работы синтаксического анализатора, и с порождением на основе этой информа- ции текста результирующей программы, рассмотрены в лабораторной работе № 4, поэтому здесь на них останавливаться не будем. Построение синтаксического анализатора — это более творческий процесс, чем построение лексического анализатора. Этот процесс не всегда может быть пол- ностью формализован. Имея грамматику входного языка, разработчик синтаксического анализатора дол- жен в первую очередь выполнить ряд формальных преобразований над этой грам- матикой, облегчающих построение распознавателя. После этого он должен про- верить, относится ли полученная грамматика к одному из известных классов КС-языков, для которых существуют линейные распознаватели. Если такой класс найден, можно строить распознаватель (если найдено несколько классов, следует выбрать тот, для которого построение распознавателя проще либо построенный распознаватель будет обладать лучшими характеристиками). Если же такой класс КС-языков найти не удалось, то разработчик должен попытаться выполнить над грамматикой некоторые преобразования, чтобы привести ее к одному из извест- ных классов. Эти преобразования не могут быть описаны формально, и в каждом конкретном случае разработчик должен попытаться найти их сам (иногда преоб- разования имеет смысл искать даже в том случае, когда грамматика подпадает под один из известных классов КС-языков, с целью найти другой класс, для которого можно построить лучший по характеристикам распознаватель). Сложностей с построением синтаксических анализаторов не существовало бы, если бы для КС-грамматик были разрешимы проблемы преобразования и эквивалент- ности. Но поскольку в общем случае это не так, то одним классом КС-грамматик, для которого существуют линейные распознаватели, ограничиться не удается. По этой причине для всех классов КС-грамматик существует принципиально важное ограничение: в общем случае невозможно преобразовать произвольную КС-грамматику к виду, требуемому данным классом КС-грамматик, либо же дока- зать, что такого преобразования не существует. То, что проблема неразрешима в об- щем случае, не говорит о том, что она не решается в каждом конкретном частном случае, и зачастую удается найти такие преобразования. И чем шире набор классов КС-грамматик с линейными распознавателями, тем проще их искать.
^ataHaus^k Краткие теоретические сведения 67 Только, когда в результате всех этих действий не удалось найти соответствующий класс КС-языков, разработчик вынужден строить универсальный распознаватель. Характеристики такого распознавателя будут существенно хуже, чем у линейного распознавателя: в лучшем случае удается достичь квадратичной зависимости вре- мени работы распознавателя от длины входной цепочки. Такое бывает редко, по- этому все современные компиляторы построены на основе линейных распознава- телей (иначе время их работы было бы недопустимо велико). Часто одна и та же КС-грамматика может быть отнесена не к одному, а сразу к не- скольким классам КС-грамматик, допускающих построение линейных распозна- вателей. Тогда необходимо решить, какой из нескольких возможных распознава- телей выбрать для практической реализации. Ответить на этот вопрос не всегда легко, поскольку могут быть построены два принципиально разных распознавателя, алгоритмы работы которых несопоста- вимы. В первую очередь речь идет именно о восходящих и нисходящих распозна- вателях: в основе первых лежит алгоритм подбора альтернатив, в основе вторых — алгоритм «сдвиг-свертка». На вопрос о том, какой распознаватель — нисходящий или восходящий — выбрать для построения синтаксического анализатора, нет однозначного ответа. Эту про- блему необходимо решать, опираясь на некую дополнительную информацию о том, как будут использованы или каким образом будут обработаны результаты работы распознавателя. Более подробно обсуждение этого вопроса можно найти в [1, 7]. СОВЕТ-------------------------------------------------------------------- Следует вспомнить, что синтаксический анализатор — это один из этапов компиляции. И с этой точки зрения результаты работы распознавателя служат исходными данными для следую- щих этапов компиляции. Поэтому выбор того или иного распознавателя во многом зависит от реализации компилятора, от того, какие принципы положены в его основу. Желание использовать более простой класс грамматик для построения распозна- вателя может потребовать каких-то манипуляций с заданной грамматикой, необ- ходимых для ее преобразования к требуемому классу. При этом нередко грамма- тика становится неестественной и малопонятной, что в дальнейшем затрудняет ее использование для генерации результирующего кода. Поэтому бывает удоб- ным использовать исходную грамматику такой, какая она есть, не стремясь пре- образовать ее к более простому классу. В целом следует отметить, что, с учетом всего сказанного, интерес представляют как левосторонний, так и правосторонний анализ. Конкретный выбор зависит от реализации конкретного компилятора, а также от сложности грамматики входно- го языка программирования. В общем виде процесс построения синтаксического анализатора можно описать следующим образом: 1. Выполнить простейшие преобразования над заданной КС-грамматикой. 2. Проверить принадлежность КС-грамматики, получившейся в результате пре- образований, к одному из известных классов КС-грамматик, для которых су- ществуют линейные распознаватели.
68 Лабораторная работа № 3 • Построение простейшего дерева вывода 3. Если соответствующий класс найден, взять за основу для построения распо- знавателя алгоритм разбора входных цепочек, известный для этого класса, если найдено несколько классов линейных распознавателей — выбрать из них один по своему усмотрению. 4. Иначе, если соответствующий класс по п. 2 не был найден или же найденный класс КС-грамматик не устраивает разработчиков компилятора — попытаться выполнить над грамматикой неформальные преобразования с целью подвести ее под интересующий класс КС-грамматик для линейных распознавателей и вернуться к п. 2. 5. Если же ни в п. 3, ни в п. 4 соответствующий распознаватель найти не удалось (что для современных языков программирования практически невозможно), необходимо использовать один из универсальных распознавателей. 6. Определить, в какой форме синтаксический распознаватель будет передавать результаты своей работы другим фазам компилятора (эта форма называется внутренним представлением программы в компиляторе). Реализовать выбранный в п. 3 или 5 алгоритм с учетом структур данных, соответ- ствующих п. 6. В данной лабораторной работе в заданиях предлагаются грамматики, не требую- щие дополнительных преобразований. Кроме того, гарантировано, что все они относятся к классу КС-грамматик операторного предшествования, для которых существует известный алгоритм линейного распознавателя. Поэтому создание синтаксического распознавателя для выполнения лабораторной работы суще- ственно упрощается. Для грамматик, предложенных в заданиях, известно, что они относятся также к классам КС-грамматик LR{\) и LALR( 1), для которых также существует извес- тный алгоритм линейного распознавателя, но, по мнению автора, этот алгоритм более сложен (его описание можно найти в [1, 2, 7]). Однако желающие могут не согласиться с автором и использовать для выполнения лабораторной работы лю- бой из этих классов. После несложных преобразований эти же грамматики могут быть приведены к ви- ду, удовлетворяющему требованиям алгоритма рекурсивного спуска (или алго- ритма анализа для ЕЕ(1)-грамматик). Этот алгоритм тривиально прост, но для его реализации надо выполнить достаточно несложные неформальные преобра- зования над заданными грамматиками — автор оставляет эти преобразования для желающих попробовать свои силы. Выполняющие лабораторную работу могут пойти любым из рекомендованных путей или построить иной синтаксический анализатор по своему усмотрению — в этом направлении их ничто не ограничивает. В качестве основного пути выполнения лабораторной работы автор предлага- ет распознаватель на основе грамматик операторного предшествования, поэто- му именно этот класс КС-грамматик далее рассмотрен более подробно (описа- ния остальных известных классов и подклассов КС-грамматик можно найти в [1-3, 7]).
^latattaus^. Краткие теоретические сведения 69 Грамматики предшествования КС-языки делятся на классы в соответствии со структурой правил их грамматик. В каждом из классов налагаются дополнительные ограничения на допустимые правила грамматики. Одним из таких классов является класс грамматик предше- ствования. Они используются для синтаксического разбора цепочек с помощью модификаций алгоритма «сдвиг-свертка». Принцип организации распознавателя па основе грамматики предшествования исходит из того, что для каждой упорядоченной пары символов в грамматике уста- навливается отношение, называемое отношением предшествования. В процессе разбора МП-автомат сравнивает текущий символ входной цепочки с одним из символов, находящихся на верхушке стека автомата. В процессе сравнения про- веряется, какое из возможных отношений предшествования существует между этими двумя символами. В зависимости от найденного отношения выполняется либо сдвиг, либо свертка. При отсутствии отношения предшествования между символами алгоритм сигнализирует об ошибке. Задача заключается в том, чтобы иметь возможность непротиворечивым образом определить отношения предшествования между символами грамматики. Если это возможно, то грамматика может быть отнесена к одному из классов грамматик предшествования. Отношения предшествования будем обозначать знаками «=.», «<.» и «.>». Отно- шение предшествования единственно для каждой упорядоченной пары символов. При этом между какими-либо двумя символами может и не быть отношения пред- шествования — это значит, что они не могут находиться рядом пи в одном элементе разбора синтаксически правильной цепочки. Отношения предшествования зави- сят от порядка, в котором стоят символы, и в этом смысле их нельзя путать со зна- ками математических операций (хотя по внешнему виду они очень похожи) — они не обладают ни свойством коммутативности, ни свойством ассоциативности. На- пример, если.известно, что В( .> В, то не обязательно выполняется Я <. В( (поэтому знаки предшествования помечают специальной! точкой: «=.», «<.», «.>»). Метод предшествования основан на том факте, что отношения предшествования между двумя соседними символами распознаваемой строки соответствуют трем следующим вариантам: □ Bt <. Bt + V если символ В^ ( — крайний левый символ некоторой основы (это отношение между символами можно назвать «предшествует основе» или про- сто «предшествует»); □ В; > Bi + V если символ В — крайний правый символ некоторой основы (это отношение между символами можно назвать «следуетза основой» или просто «следует»); □ В ==. В; + р если символы В. иВ;+1 принадлежат одной основе (это отношение между символами можно назвать «составляют основу»). Исходя из этих соотношений выполняется разбор входной строки для грамматик предшествования.
70 Лабораторная работа № 3 • Построение простейшего дерева вывода Суть принципа такого разбора поясняет рис. 3.1. На нем изображена входная це- почка символов аурб в тот момент, когда выполняется свертка цепочки у. Символ а является последним символом подцепочки а, а символ b — первым символом подцепочки р. Тогда, если в грамматике удастся установить непротиворечивые отношения предшествования, то в процессе выполнения разбора по алгоритму «сдвиг-свертка» можно всегда выполнять сдвиг до тех пор, пока между символом на верхушке стека и текущим символом входной цепочки существует отношение <. или =.. А как только между этими символами будет обнаружено отношение .>, сразу надо выполнять свертку. Причем для выполнения свертки из стека надо выбирать все символы, связанные отношением =•. Все различные правила в грам- матике предшествования должны иметь различные правые части — это гаранти- рует непротиворечивость выбора правила при выполнении свертки. а а Y Ь Р 5 , Рис. 3.1. Отношения между символами входной цепочки в грамматике предшествования Таким образом, установление непротиворечивых отношений предшествования между символами грамматики в комплексе с несовпадающими правыми частями различных правил дает ответы на все вопросы, которые надо решить для органи- зации работы алгоритма «сдвиг-свертка» без возвратов. На основании отношений предшествования строят матрицу предшествования грамматики. Строки матрицы предшествования помечаются первыми (левыми) символами, столбцы — вторыми (правыми) символами отношений предшество- вания. В клетки матрицы на пересечении соответствующих столбца и строки по- мещаются знаки отношений. При этом пустые клетки матрицы говорят о том, что между данными символами нет ни одного отношения предшествования. Существует несколько видов грамматик предшествования. Они различаются по тому, какие отношения предшествования в них определены и между какими ти- пами символов (терминальными или нетерминальными) могут быть установле- ны эти отношения. Кроме того, возможны незначительные модификации функ- ционирования самого алгоритма «сдвиг-свертка» в распознавателях для таких грамматик (в основном на этапе выбора правила для выполнения свертки, когда возможны неоднозначности) [1]. Выделяют следующие виды грамматик предшествования: □ простого предшествования; □ расширенного предшествования; □ слабого предшествования; □ смешанной стратегии предшествования; □ операторного предшествования. Далее будут рассмотрены ограничения на структуру правил и алгоритмы разбора для грамматик операторного предшествования.
^aiattaus,^. Краткие теоретические сведения 71 Матрицу операторного предшествования КС-грамматики можно построить, опи- раясь непосредственно на определения отношений предшествования [1, 3, 7], но проще и удобнее воспользоваться двумя дополнительными типами множеств — множествами крайних левых и крайних правых символов, а также множествами крайних левых терминальных и крайних правых терминальных символов для всех нетерминальных символов грамматики. Если имеется КС-грамматика G(VT,VN,P,5), V = VT u VN, то множества край- них левых и крайних правых символов определяются следующим образом: □ L(t/)-{T|3 U => *Tz} — множество крайних левых символов относительно нетерминального символа U; □ R(l0-{r|3 U => *zT} — множество крайних правых символов относительно нетерминального символа U, где U — заданный нетерминальный символ ([/ е VN), Т — любой символ грамма- тики (Те V),az — произвольная цепочка символов (zg V*, цепочкаz может быть и пустой цепочкой). Множества крайних левых и крайних правых терминальных символов определя- ются следующим образом: □ L,(f/)-{f|3 U =э *tz или 3 U=$ *Ctz} — множество крайних левых терминаль- ных символов относительно нетерминального символа U; □ U => *zt или 3 U => *ztC} — множество крайних правых терминаль- ных символов относительно нетерминального символа U, где t — терминальный символ (£е VT), U и С — нетерминальные символы (U, С 6 VN), az — произвольная цепочка символов (z е V*, цепочка z может быть и пу- стой цепочкой). Множества L(U) и R(t/) могут быть построены для каждого нетерминального символа U е VN по очень простому алгоритму: 1. Для каждого нетерминального символа U ищем все правила, содержащие U в левой части. Во множество L(U) включаем самый левый символ из правой части правил, а во множество R( СТ) — самый правый символ из правой части (то есть во множество L(U) записываем все символы, с которых начинаются правила для символа U, а во множество R(t/) — символы, которыми эти пра- вила заканчиваются). Если в правой части правила для символа U имеется только один символ, то он должен быть записан в оба множества — L(l/) и R([7). 2. Для каждого нетерминального символа U выполняем следующее преобразо- вание: если множество L([/) содержит нетерминальные символы грамматики U', U", ..., то его надо дополнить символами, входящими в соответствующие множества HU'), L([/”)... и не входящими вЬ(17). Ту же операцию надо вы- полнить для R(t/). Фактически, если какой-то символ U' входит в одно из мно- жеств для символа U, то надо объединить множества для U' и U, а результат записать во множество для символа U.
72 Лабораторная работа № 3 • Построение простейшего дерева вывода 3. Если на предыдущем шаге хотя бы одно множество L(U) или R( U) для некото- рого символа грамматики изменилось, то надо вернуться к шагу 2, иначе — построение закончено. Для нахождения множеств L,([/) и R,(U) используется следующий алгоритм: 1. Для каждого нетерминального символа грамматики Uстроятся множества L( U) и R(£/). 2. Для каждого нетерминального символа грамматики U ищутся правила вида U^tzn U Ctz, где t g VT, С е VN, z е V*; терминальные символы t включа- ются во множество L,([/). Аналогично для множества Rr( 17) ищутся правила вида U-+ztnU-+ ztC (то есть во множество ЬДГ/) записываются все крайние слева терминальные символы из правых частей правил для символа U, а во множество Rz(^ — все крайние справа терминальные символы этих правил). Не исключено, что один и тот же терминальный символ будет записан в оба множества — L/^HR/tO. 3. Просматривается множество L(C7)> в которое входят символы U\ U"... Мно- жество LXtT) дополняется терминальными символами, входящими bL/IO» L/t/")- и не входящими bL/L)- Аналогичная операция выполняется и для множества R,(7) на основе множества ВД. Для практического использования матрицу предшествования дополняют терми- нальными символами ±|( и ±к (начало и конец цепочки). Для них определены сле- дующие отношения предшествования: JL(I <• а, если a g L/5); 1к •> а, если а е R/5). Имея построенные множества L;(C) и R/b), заполнение матрицы операторного предшествования для КС-грамматики G(VT,VN,P,5) можно выполнить по сле- дующему алгоритму: 1. Берем первый символ из множества терминальных символов грамматики VT: я, g VT, i = 1. Будем считать этот символ текущим терминальным символом. 2. Во всем множестве правил Р ищем правила вида С —> xaby или С —> xa JJbij, где а(. — текущий терминальный символ, tx — произвольный терминальный символ {bj g VT), Uи С — произвольные нетерминальные символы ( U, С е VN), а х и у — произвольные цепочки символов, возможно пустые (х, у е V*). Фак- тически производится поиск таких правил, в которых в правой части символы а, и Ь. стоят рядом или же между ними есть не более одного нетерминального символа (причем символ о, обязательно стоит слева от /а). 3. Для всех символов найденных на шаге 2, выполняем следующее: ставим знак «=•» («составляет основу») в клетки матрицы операторного предшествования на пересечении строки, помеченной символом а, и столбца, помеченного сим- волом Ьг 4. Во всем множестве правил Р ищем правила вида С —> хо(17г/, где — текущий терминальный символ, Ц. и С — произвольные нетерминальные символы (I/,
'balaUausTiik Краткие теоретические сведения 73 Се VN), ах му — произвольные цепочки символов, возможно пустые (х,у е V*). Фактически ищем правила, в которых в правой части символ а; стоит слева от нетерминального символа Ц. 5. Для всех символов LL, найденных на шаге 4, берем множество символов L,(t/). Для всех терминальных символов с/(, входящих в это множество, выполняем следующее: ставим знак «<•» («предшествует») в клетки матрицы оператор- ного предшествования на пересечении строки, помеченной символом а., и столб- ца, помеченного символом ск. 6. Во всем множестве правил Р ищем правила вида С—> хЦсгу, где ак — текущий терминальный символ, Ц и С — произвольные нетерминальные символы (Ц, Се VN), ах \\у— произвольные цепочки символов, возможно пустые (х,у е V*). Фактически ищем правила, в которых в правой части символ «стоит справа от нетерминального символа U-. 7. Для всех символов Ц, найденных на шаге 6, берем множество символов Rf([7). Для всех терминальных символов ск, входящих в это множество, выполняем следующее: ставим знак «•>» («следует») в клетки матрицы операторного пред- шествования на пересечении строки, помеченной символом ск, и столбца, по- меченного символом а. 8. Если рассмотрены все терминальные символы из множества VT, то переходим к шагу 9, иначе — берем очередной символ ai е VT из множества VT, i:== i + 1, делаем его текущим терминальным символом и возвращаемся к шагу 2. 9. Берем множество L/5) для целевого символа грамматики 5. Для всех терми- нальных символов ср входящих в это множество, выполняем следующее: ста- вим знак «<» («предшествует») в клетки матрицы операторного предшество- вания на пересечении строки, помеченной символом JL(| («начало строки»), и столбца, помеченного символом ск. 10. Берем множество R,(5) для целевого символа грамматики 5. Для всех терми- нальных символов Ср входящих в это множество, выполняем следующее: ста- вим знак «•>» («следует») в клетки матрицы операторного предшествования на пересечении строки, помеченной символом ск, и столбца, помеченного сим- волом ±к («конец строки»). Построение матрицы закончено. Если на всех шагах алгоритма построения матрицы операторного предшествова- ния не возникло противоречий, когда в одну и ту же клетку матрицы надо запи- сать два или три различных символа предшествования, то матрица построена пра- вильно (в каждой клетке такой матрицы присутствует один из символов предше- ствования — «=•», «<•» или «>» — или же клетка пуста). Если на каком-то шаге возникло противоречие, значит, исходная КС-грамматика G(VT,VN,P,5) не яв- ляется грамматикой операторного предшествования. В этом случае можно попро- бовать преобразовать грамматику так, что она станет удовлетворять требованиям операторного предшествования (что не всегда возможно), либо необходимо ис- пользовать другой тип распознавателя. Более подробно работа с грамматиками предшествования и другими типами рас- познавателей описана в [1-4, 7].
74 Лабораторная работа № 3 • Построение простейшего дерева вывода Алгоритм «сдвиг-свертка» для грамматик операторного предшествования Алгоритм «сдвиг-свертка» для грамматики операторного предшествования вы- полняется МП-автоматом с одним состоянием. Для моделирования его работы необходима входная цепочка символов и стек символов, в котором автомат мо- жет обращаться не только к самому верхнему символу, но и к некоторой цепочке символов на вершине стека. Этот алгоритм для заданной КС-грамматики G(VT,VN,P,S) при наличии постро- енной матрицы предшествования можно описать следующим образом: 1. Поместить в верхушку стека символ ±п («начало строки»), считывающую го- ловку МП-автомата поместить в начало входной цепочки (текущим входным символом становится первый символ входной цепочки). В конец входной це- почки надо дописать символ ±к («конец строки»). 2. В стеке ищется самый верхний! терминальный символ 5 (если на вершине сте- ка лежат нетерминальные символы, они игнорируются и берется первый тер- минальный символ, находящийся под ними), при этом сам символ v остается в стеке. Из входной цепочки берется текущий символ at (справа от считываю- щей головки МП-автомата). 3. Если символ s- — это символ начала строки (±н), а символ at — символ конца строки (±к), то алгоритм завершен, входная цепочка символов разобрана. 4. В матрице предшествования ищется клетка на пересечении строки, помечен- ной символом s., и столбца, помеченного символом ai (выполняется сравнение текущего входного символа и терминального символа на верхушке стека). 5. Если клетка, найденная на шаге 3, пустая, то значит, входная строка символов не принимается МП-автоматом, алгоритм прерывается и выдает сообщение об ошибке. 6. Если клетка, найденная на шаге 3, содержит символ «=•» («составляет основу») или «<•» («предшествует»), то необходимо выполнить перенос (сдвиг). При выполнении переноса текущий входной символ at помещается на верхушку сте- ка, считывающая головка МП-автомата во входной цепочке символов сдвигает- ся на одну позицию вправо (после чего текущим входным символом становится следующий символ at + р i := i + 1). После этого надо вернуться к шагу 2. 7. Если клетка, найденная па шаге 3, содержит символ «•>» («следует»), то необ- ходимо произвести свертку. Для выполнения свертки из стека выбираются все терминальные символы; связанные отношением «=•» («составляет основу»), начиная от вершины стека, а также все нетерминальные символы, лежащие в стеке рядом с ними. Эти символы вынимаются йз стека и собираются в це- почку у (если в стеке нет символов, связанных отношением «=•», то из него вынимается один самый верхний терминальный символ и лежащие рядом с ним нетерминальные символы). 8. Во всем множестве правил Р грамматики G(VT,VN,P,5) ищется правило, у ко- торого правая часть совпадает с цепочкой у (по условиям грамматик предше-
Краткие теоретические сведения 75 ствования все правые части правил должны быть различны, поэтому может быть найдено или одно такое правило, или ни одного). Если правило найдено, то в стек помещается нетерминальный символ из левой части правила, иначе, если правило не найдено, это значит, что входная строка символов не прини- мается МП-автоматом, алгоритм прерывается и выдает сообщение об ошибке. Следует отметить, что при выполнении свертки считывающая головка авто- мата не сдвигается и текущий входной символ а остается неизменным. После выполнения свертки необходимо вернуться к шагу 2. После завершения алгоритма решение о принятии цепочки зависит от содержи- мого стека Автомат принимает цепочку, если в результате завершения алгорит- ма он находится в состоянии, когда в стеке находятся начальный символ грамма- тики S и символ JLn. Выполнение алгоритма может быть прервано, если на одном из его шагов возникнет ошибка. Тогда входная цепочка не принимается. Алгоритм «сдвиг-свертка» для грамматики операторного предшествования игно- рирует нетерминальные символы. Поэтому имеет смысл преобразовать исходную грамматику таким образом, чтобы оставить в ней только один нетерминальный символ. Это преобразование заключается в том, что все нетерминальные симво- лы в правилах грамматики заменяются на один нетерминальный символ (чаще всего — целевой символ грамматики). Построенная в результате такого преобразования грамматика называется остов- ной грамматикой, а само преобразование — остовным преобразованием [1,7]. Остовное преобразование не ведет к созданию эквивалентной грамматики и вы- полняется только для упрощения работы алгоритма (который при выборе правил все равно игнорирует нетерминальные символы) после построения матрицы пред- шествования. Полученная в результате остовного преобразования грамматика может не являться однозначной, но все необходимые данные о порядке примене- ния правил содержатся в матрице предшествования и распознаватель остается детерминированным. Поэтому остовное преобразование может выполняться без потерь информации только после построения матрицы предшествования. При этом также необходимо следить, чтобы в грамматике не возникло неоднозначно- стей из-за одинаковых правых частей правил, которые могут появиться в остов- ной грамматике. Вывод, полученный при разборе на основе остовной граммати- ки, называют результатом остовного разбора, или остовным выводом. По результатам остовного разбора можно построить соответствующий ему вывод на основе правил исходной грамматики. Однако эта задача не представляет прак- тического интереса, поскольку остовной вывод отличается от вывода на основе исходной грамматики только тем, что в нем отсутствуют шаги, связанные с при- менением цепных правил, и не учитываются типы нетерминальных символов. Для компиляторов же распознавание цепочек входного языка заключается не в на- хождении того или иного вывода, а в выявлении основных синтаксических кон- струкций исходной программы с целью построения на их основе цепочек языка результирующей программы. В этом смысле типы нетерминальных символов и цепные правила не несут никакой полезной информации, а напротив, только усложняют обработку цепочки вывода. Поэтому для реального компилятора
76 Лабораторная работа № 3 • Построение простейшего дерева вывода нахождение остовного вывода является даже более полезным, чем нахождение вывода на основе исходной грамматики. Найденный остовной вывод в дальней- ших преобразованиях уже не нуждается*. В общем виде последовательность построения распознавателя для КС-граммати- ки операторного предшествования G( VT,VN,P,5) можно описать следующим об- разом: 1. На основе множества правил грамматики Р построить множества крайних ле- вых и крайних правых символов для всех нетерминальных символов грамма- тики Ue VN: L([/) и R(U)- 2. На основе множества правил грамматики Р и построенных па шаге 1 множеств L(U) и R([/) построить множества крайних левых и крайних правых терми- нальных символов для всех нетерминальных символов грамматики U е VN: L,(t/)nR,([7). 3. На основе построенных на шаге 2 множеств Lz(U) и R,(t/) для всех терминаль- ных символов грамматики а е VTзаполняется матрица операторного предше- ствования. 4. Исходная грамматика G(VT,VN,P,5) преобразуется в остовную грамматику G’(VT,{5},P,5) с одним нетерминальным символом. 5. На основе построенной матрицы предшествования и остовной грамматики строится распознаватель на базе алгоритма «сдвиг-свертка» для грамматик операторного предшествования. Важно, что алгоритм распознавателя может быть реализован вне зависимости от матрицы предшествования и правил исходной грамматики. Тогда, меняя матри- цу и правила, один и тот же алгоритм можно использовать для распознавания входных цепочек любой грамматики операторного предшествования. Далее в примере выполнения работы проиллюстрирован именно такой подход к построению распознавателя. Требования к выполнению работы Порядок выполнения работы Для выполнения лабораторной работы требуется написать программу, которая выполняет лексический анализ входного текста в соответствии с заданием, по- рождает таблицу лексем и выполняет синтаксический разбор текста по заданной грамматике с построением дерева разбора. Текст на входном языке задается в ви- де символьного (текстового) файла. Синтаксис входного языка и перечень допу- стимых лексем указаны в задании. Допускается исходить из условия, что текст содержит не более одного предложения входного языка. ’ Из цепочки (и дерева) вывода удаляются ценные правила, которые все равно не несут никакой по- лезной семантической (смысловой) нагрузки, а потому для компилятора являются бесполезными. Это положительное свойство распознавателя.
Требования к выполнению работы 77 При наличии во входном файле текста, соответствующего заданному языку, про- грамма должна строить и отображать дерево синтаксического разбора. Если же текст во входном файле содержит ошибки (лексические пли синтаксические), программа должна выдавать сообщения о наличии ошибок во входном тексте и корректно завершать свое выполнение. Рекомендуется разбить программу на три составные части: лексический анализ, построение цепочки вывода и построение дерева вывода. Лексический анализа- тор должен выделять в тексте лексемы языка и заменять их на терминальный сим- вол грамматики (который в задании обозначен как а). Полученная после лекси- ческого анализа цепочка должна рассматриваться во второй части программы в со- ответствии с алгоритмом разбора. При неудачном завершении алгоритма выдается сообщение об ошибке, при удачном — строится цепочка вывода. После построе- ния цепочки вывода на ее основе строится дерево разбора, в котором символы а последовательно заменяются на лексемы из таблицы лексем. Для выполнения лексического анализа рекомендуется использовать программ- ные модули, созданные в результате выполнения лабораторной работы № 2. Длину идентификаторов и строковых констант можно считать ограниченной 32 символами. Программа должна допускать наличие комментариев неограничен- ной длины во входном файле. Форму организации комментариев предлагается выбрать самостоятельно. 1. Получить вариант задания у преподавателя. 2. Построить множества крайних левых и крайних правых символов, множества крайних правых и крайних левых терминальных символов и матрицу опера- торного предшествования для заданной грамматики (если для построения син- таксического распознавателя предполагается использовать другой механизм, отличный от грамматик операторного предшествования, то форму его надо предварительно согласовать с преподавателем). 3. Выполнить разбор простейшего примера вручную по правилам заданной грам- матики, убедиться, что разбор выполняется корректно. 4. Подготовить и защитить отчет. 5. Написать и отладить программу на ЭВМ. 6. Сдать работающую программу преподавателю. Требования к оформлению отчета Отчет должен содержать следующие разделы: □ Задание по лабораторной работе. □ Краткое изложение цели работы. □ Запись заданной грамматики входного языка в форме Бэкуса—Наура (если для построения синтаксического распознавателя используется механизм, требую- щий преобразования исходной грамматики входного языка, то эти преобразо-
78 Лабораторная работа № 3 • Построение простейшего дерева вывода вания и полученная в результате их грамматика должны быть отражены в от- чете). □ Множества крайних правых и крайних левых символов с указанием шагов построения. □ Множества крайних правых и крайних левых терминальных символов. □ Заполненную матрицу предшествования для грамматики (если для построе- ния синтаксического распознавателя используется другой механизм, отлич- ный от грамматик операторного предшествования, то форму его отображения в отчете надо согласовать с преподавателем). □ Пример выполнения разбора простейшего предложения входного языка. □ Текст программы (оформляется после выполнения программы на ЭВМ). Основные контрольные вопросы □ Какую роль выполняет синтаксический анализ в процессе компиляции? □ Какие проблемы возникают при построении синтаксического анализатора и как они могут быть решены? □ Какие типы грамматик существуют? Что такое КС-грамматики? Расскажите об их использовании в компиляторе. □ Какие типы распознавателей для КС-грамматик существуют? Расскажите о не- достатках и преимуществах различных типов распознавателей. □ Поясните правила построения дерева вывода грамматики. □ Что такое грамматики простого предшествования? □ Как вычисляются отношения предшествования для грамматик простого пред- шествования ? □ Что такое грамматика операторного предшествования ? □ Как вычисляются отношения для грамматик операторного предшествования ? □ Расскажите о задаче разбора. Что такое распознаватель языка? □ Расскажите об общих принципах работы распознавателя языка. □ Что такое перенос, свертка? Для чего необходим алгоритм «перенос-свертка»? □ Расскажите, как работает алгоритм «перенос-свертка» в общем случае (с воз- вратами). □ Как работает алгоритм «перенос-свертка» без возвратов (объясните на своем примере)? Варианты заданий Варианты исходных грамматик Д алее приведены варианты грамматик. Во всех вариантах символ 5 является на- чальным символом грамматики; 5, F, Т и Е обозначают нетерминальные символы.
Варианты заданий 79 Терминальные символы выделены жирным шрифтом. Вместо символа а должны подставляться лексемы. 1. 5->a:=F; F^>F+T\Т Т-+Т-Е\ Т/Е\Е E^(F)\~(F)\a 2. 5—>a:=F; F—>For T\Fxor Г| T T^> Тап&Е\Е Е —» (F) | not (F) | а 3. 5-»F; F —> if Е then Т else F | if Е then F | a := a T —» if E then T else T | a := a E —> a<a | a>a | a=a 4. 5->F; F —> for (T) do F | a := a F->F;E;F| ;E;F|F;E; | ;E; E —» a<a [ a>a | a=a Исходные грамматики и типы допустимых лексем Ниже в табл. 3.1 приведены номера заданий. Для каждого задания указана соот- ветствующая ему грамматика и типы допустимых лексем. Таблица 3.1. Номера заданий для выполнения лабораторной работы № № варианта грамматики Допустимые лексемы входного языка 1 1 Идентификаторы, десятичные числа с плавающей точкой 2 2 Идентификаторы, константы true и false 3 3 Идентификаторы, десятичные числа с плавающей точкой 4 4 Идентификаторы, десятичные числа с плавающей точкой 5 1 Идентификаторы, римские числа 6 2 Идентификаторы, константы 0 и 1 7 3 Идентификаторы, римские числа продолжение
80 Лабораторная работа № 3 • Построение простейшего дерева вывода Таблица 3.1. (продолжение) № № варианта Допустимые лексемы входного языка грамматики 8 4 Идентификаторы, римские числа 9 1 Идентификаторы, шестнадцатеричные числа 10 2 Идентификаторы, шестнадцатеричные числа 11 3 Идентификаторы, шестнадцатеричные числа 12 4 Идентификаторы, шестнадцатеричные числа 13 1 Идентификаторы, символьные константы (в одинарных кавычках) 14 2 Идентификаторы, символьные константы 'Г и ’F’ 15 3 Идентификаторы, строковые константы (в двойных кавычках) 16 4 Идентификаторы, строковые константы (в двойных кавычках) ПРИМЕЧАНИЕ----------------------------------------------------------------- Римскими числами считать последовательности больших латинских букв X, V и I. Шестнадцатеричными числами считать последовательность цифр и символов «а», «Ь», «с», «d», «е» и «fr>, начинающуюся с цифры (например: 89, 45ас9, ОаЬс4). Для выполнения работы рекомендуется использовать лексический анализатор, построен- ный в ходе выполнения лабораторной работы № 2. Пример выполнения работы Задание для примера Для выполнения лабораторной работы возьмем тот же самый язык, который был использован для выполнения лабораторной работы № 2. Этот язык может быть задан, например, с помощью следующей КС-грамматики G({if,then,else,а,:=,or,xor,and,{5,F,E,D,С},P,5) с правилами P: S-+F; F —> if E then Telse F | if E then F | a := E T —> if £ then T else T\ a := E E E or D | £ xor D | D D-+ D and C | C C-+a\(E) Жирным шрифтом в грамматике и в правилах выделены терминальные символы. Как было уже сказано ранее, выбранный в качестве примера язык не совпадает ни с одним из предложенных выше вариантов и, кроме этого, служит хорошей иллю- страцией основных особенностей построения синтаксического распознавателя, присущих различным вариантам.
^ataHausiik Пример выполнения работы 81 Построение матрицы операторного предшествования Построение множеств крайних правых и крайних левых символов Построение множеств крайних левых и крайних правых символов выполним со- гласно описанному ранее алгоритму. На первом шаге возьмем все крайние левые и крайние правые символы из правил грамматики G. Получим множества, представленные в табл. 3.2. Таблица 3.2. Множества крайних левых и крайних правых символов. Шаг 1 Символ U L(U) R(U) S F F if, а F, Е Т if, а Т, Е Е Е, D D D D, С С С а, ( а, ) Из табл. 3.2 видно, что множества L( L7) для символов 5, Е, D, а также множества R(t/) для символов F, Т, Е, D содержат другие нетерминальные символы, а потому должны быть дополнены. Например, L(5) должно быть дополнено L(F), так как символ F входит в L(5): Fe L(5), a R(F) должно быть дополнено R(E), так как символ Е входит в R(F): Е g R(F). Выполним необходимые дополнения и получим множества, представленные в табл. 3.3. Таблица 3.3. Множества крайних левых и крайних правых символов. Шаг 2 Символ U L(U) R(U) S F Т Е D С F, if, a ; if, a F, E, D if, a T, E, D E, D, C D, C D, С, a, ( C, a, ) a, ( a, ) Практически все множества в табл. 3.3 изменились по сравнению с табл. 3.2 (кро- ме множеств для символа С), а значит, построение не закончено. Продолжим до- полнять множества. Получим множества, представленные в табл. 3.4. В табл. 3.4 по сравнению с табл. 3.3 изменились множества для символов F,T\iE — построение не закончено. Продолжим дополнять множества. Получим множества, представленные в табл. 3.5.
82 Лабораторная работа № 3 • Построение простейшего дерева вывода Таблица 3.4. Множества крайних левых и крайних правых символов. Шаг 3 Символ U UU) R(U) S F, if, а I F if, а F, Е, D, С Т if, а Т, Е, D, С Е Е, D, С, а, ( D, С, а, ) D D, С, а,( С, а, ) С а, ( а, ) Таблица 3.5. Множества крайних левых и крайних правых символов. Шаг 4 (результат) Символ U UU) R(U) S F, if, а « F if, а F, Е, D, С, а, ) Т if, а Т, Е, D, С, а, ) Е Е, D, С, а, ( D, С, а, ) D D, С, а, ( С, а, ) С а, ( а, ) В табл. 3.5 по сравнению с табл. 3.4 изменились только множества R(t7) для сим- волов F и Т— построение не закончено. Продолжим дополнять множества. Но если выполнить еще один шаг (шаг 5), то можно убедиться, что множества уже больше не изменятся (чтобы не создавать еще одну лишнюю таблицу, этот шаг здесь выполнять не будем). Таким образом, множества, представленные в табл. 3.5, являются результатом построения множеств крайних левых и крайних правых символов грамматики G. Построение множеств крайних правых и крайних левых терминальных символов Построение множеств крайних левых и крайних правых терминальных символов также выполним согласно описанному выше алгоритму. На первом шаге возьмем все крайние левые и крайние правые терминальные сим- волы из правил грамматики G. Получим множества, представленные в табл. 3.6. Таблица 3.6. Множества крайних левых и крайних правых терминальных символов. Шаг 1 Символ U Lt(U) Rt(U) S > F if, а else, then, := Т if, а else, := Е or, xor or, xor D and and С a, ( a, )
Пример выполнения работы 83 Дополним множества, представленные в табл. 3.6, на основании ранее построенных множеств крайних левых и крайних правых символов, представленных в табл. 3.5. Например, L,(E) должно быть дополнено L,(£>) и L/C), так как символы D и С вхо- дят в L(E): D, Се L(E), a R/F) должно быть дополнено RZ(E), R,(£)) и R/0, так как символы E,DnC входят в R(F): Е, D, С е R(F). Получим итоговые множества крайних левых и крайних правых терминальных символов, которые представлены в табл. 3.7. Таблица 3.7. Множества крайних левых и крайних правых терминальных симво- лов. Результат Символ U Lt(U) Rt(U) S if, а, ; I F if, а else, then, :=, or, xor, and, a, ) Т if, а else, := , or, xor, and, a, ) Е or, xor, and, a, ( or, xor, and, a.) D and, a, ( and, a, ) С a, ( a, ) Теперь все готово для заполнения матрицы операторного предшествования. Заполнение матрицы предшествования Для заполнения матрицы операторного предшествования необходимы множе- ства крайних левых и крайних правых терминальных символов, представлен- ные в табл. 3.7, и правила исходной грамматики G. Заполнение таблицы рассмотрим на примере лексем or и (. Символ огне стоит рядом с другими терминальными символами в правилах грам- матики. Поэтому знак «=•» («составляет основу*) для него не используется. Символ or стоит слева от нетерминального символа D в правиле Е —> Е or D. В мно- жество 4D) входят символы and, а и (. Поэтому в строке матрицы, помеченной символом or, ставим знак «<-» («предшествует») в клетках на пересечении со стол- бцами, помеченными символами and, а и (. Кроме того, символ or стоит справа от нетерминального символа Е в том же пра- виле Е Еor D. В множество R/E) входят символы or, xor, and, а и ). Поэтому в столбце матрицы, помеченном символом or, ставим знак «•>» («следует») в клет- ках на пересечении со строками, помеченными символами or, xor, and, а и ). Больше ни в каких правилах символ or не встречается, поэтому заполнение мат- рицы для него закончено. Символ ( стоит рядом с терминальным символом ) в правиле С (Е) (между ними должно быть не более одного нетерминального символа — в данном случае один символ Е). Поэтому в строке матрицы, помеченной символом (, ставим знак «=•» («составляет основу») на пересечении со столбцом, помеченным символом ). Символ ( также стоит слева от нетерминального символа Е в том же правиле С —> (Е). В множество L/Е) входят символы or, xor, and, а и (. Поэтому в строке
84 Лабораторная работа № 3 • Построение простейшего дерева вывода матрицы, помеченной символом (, ставим знак «<•» («предшествует») в клетках на пересечении со столбцами, помеченными символами or, xor, and, а и (. Больше ни в каких правилах символ ( не встречается, поэтому заполнение матри- цы для него закончено. Повторяя описанные выше действия по заполнению матрицы для всех терминаль- ных символов грамматики G, получим матрицу операторного предшествования. Останется только заполнить строку, соответствующую символу ±н («начало стро- ки»), и столбец, соответствующий символу ±к («конец строки»). Начальным символом грамматики G является символ S, поэтому для заполнения строки, помеченной ±и, возьмем множество L,(5). В это множество входят симво- лы if, а и ;. Поэтому в строке матрицы, помеченной символом ±и, ставим знак «<•» («предшествует») в клетках на пересечении со столбцами, помеченными сим- волами if, а и;. Аналогично, для заполнения столбца, помеченного ±к, возьмем множество R,(5). В это множество входит только один символ — ;. Поэтому в столбце матрицы, помеченном символом ±к, ставим знак «•>» («следует») в клетке на пересечении со строкой, помеченной символом ;. В итоге получим заполненную матрицу операторного предшествования, которая представлена в табл. 3.8. Таблица 3.8. Матрица операторного предшествования Символы ; if then else А := or xor and ( ) if then else xor and Теперь на основе исходной грамматики G можно построить остовную граммати- ку G’({if,then,else,a,:=,or,xor,and,(,),;},{E},P',E) с правилами Р': Е —> Е; — правило 1; Е —» if Е then Е else Е | if Е then Е | а := Е — правила 2, 3 и 4; Е —» if Е then Е else Е | а := Е — правила 5 и 6; Е —> Е or Е.| £ xor Е | Е — правила 7, 8 и 9;
Пример выполнения работы 85 Е —> Е and Е | Е — правила 10 и 11; Е —> а | (Е) — правила 12 и 13. Жирным шрифтом в грамматике и в правилах выделены терминальные символы. Всего имеем 13 правил грамматики. Причем правила 2 и 5, а также правила 4 и 6 в остовной грамматике неразличимы, а правила 9 и 11 не имеют смысла (как было уже сказано, цепные правила в остовных грамматиках теряют смысл). То, что две пары правил стали неразличимы, не имеет значения, так как по смыслу (семантике входного языка) эти две пары правил обозначают одно и то же (правила 2 и 5 соот- ветствуют полному условному оператору, а правила 9 и 11 — оператору присваи- вания). Поэтому в дереве синтаксического разбора нет необходимости их разли- чать. Следовательно, синтаксический распознаватель может пользоваться остов- ной грамматикой G’. Примеры выполнения разбора предложений входного языка Рассмотрим примеры разбора цепочек входного языка в виде последовательно- сти конфигураций МП-автомата, выполняющего разбор. Результат разбора бу- дем представлять в виде последовательности номеров правил грамматики. На ос- нове найденной последовательности правил после выполнения разбора при от- сутствии ошибок (когда входная цепочка принята МП-автоматом) можно построить дерево синтаксического разбора. Рассматриваемый МП-автомат имеет только одно состояние. Тогда для иллюст- рации работы МП-автомата будем записывать каждую его конфигурацию в виде трех составляющих {а|Р|у}> где: □ а — непрочитанная часть входной цепочки; □ Р — содержимое стека МП-автомата; □ у — последовательность номеров примененных правил. В начальном состоянии вся входная цепочка не прочитана, стек автомата содер- жит только лексему типа «начало строки», последовательность номеров правил пуста. Для удобства чтения стек МП-автомата будем заполнять в порядке справа нале- во, тогда находящимся на верхушке стека будет считаться крайний правый сим- вол в цепочке р. Пример 1 Возьмем входную цепочку «if a or b and с then а := 1 хог с;». После выполнения лексического анализа, если все лексемы типа «идентификатор» и «константа» обозначить как «а», получим цепочку: «if a or a and a then а := а хог а;». Рассмотрим процесс синтаксического анализа этой входной цепочки. Шаги функ- ционирования МП-автомата будем обозначать символом « + ». Символом « + » будем обозначать шаги, на которых выполняется сдвиг (перенос), символом « + (.» — шаги, на которых выполняется свертка.
86 Лабораторная работа № 3 • Построение простейшего дерева вывода {if a or a and a then а := а хот а;±к|±н|Х} + п {a or a and a then а := a xor a;±K|±uif]X} + и {or a and a then а := a xor a;_LK|_L|iif а|Х} + с {or a and a then а := a xor a;±K|±uif Е\ 12} -г- н {a and a then а := a xor a;_LK|_Li(if £ог|12} + и {and a then а := a xor a;±J±uif Е or а| 12} + с {and a then а := a xor a;_LK|_Luif Е or £]12 12} + и {a then а := а хог aj-LjlJf Eor Е and| 12 12} + и {then а := a xor a;_LK|_Liiif Eor Еand а\ 12 12} + с {then а := а хог а;J_K|_Liiif Е or Е and Е\ 12 12 12} + с {then а := a xor a;±K|±nif Е or £]12 12 12 10} ч- с {then а := a xor a;_LK|_L(iif £| 12 12 12 10 7} + н {а := a xor a;±K|±uif Е then| 12 12 12 10 7} + н {:= a xor а\-Ljl„if Е then а| 12 12 12 10 7} -г- и {a xor a;_LK|_Liiif Е then а :=|12 12 12 10 7} + п {xor a;-LjlHif Е then а := а| 12 12 12 10 7} - (. {xor Е then а := £}12 12 12 10 7 12} + и {ajljljf £then а := £хог|12 12 12 10 7 12} ч- н {;J_J_L11if £then а := £хог а|12 12 12 10 7 12} + с {;±K|±Hif £ then a = £xor £]12 12 12 10 7 12} + c {;lK|lHif£thena:“E|12 12 12 10 7 12 12 8} +c {;1К|1И £ then £112 12 12 10 7 12 12 8 4} - c {;_LK|JL„£112 12 12 10 7 12 12 8 4 3} - n {JLj_L„£;|12 12 12 10 7 12 12 8 4 3} +1 {1К|£1Н|12 12 12 10 7 12 12 8 4 3 1} — разбор закончен, МП-автомат перешел в ко- нечную конфигурацию, цепочка принята. В результате получим последовательность правил: 12121210712128431. Этой последовательности правил будет соответствовать цепочка вывода на основе ос- товной грамматики G’: £=>, £; =>3 if £ then £; =>4 if £ then а := £; =>g if £ then a=E xor £; =>12 if £ then a ~E xor a; =>12 if £ then a := a xor a; =>7 if £ or £ then а := a xor a; =>10 if £ or £ and £ then a := a xor a; =>12 if £ or £ and a then a := a xor a; =>12 if £ or a and a then a := a xor a; =>12 if a or a and a then a := a xor a; Стоит обратить внимание, что, так как данный МП-автомат строит правосторон- ний вывод, в цепочке вывода на каждом шаге правило всегда применяется к край- нему правому нетерминальному символу в цепочке. Дерево синтаксического разбора, соответствующее данной входной цепочке, при- ведено на рис. 3.2.
'yatatlaus^k Пример выполнения работы 87 Рис. 3.2. Дерево синтаксического разбора входной цепочки «if a or a and a then а := а хог а;» Пример 2 Возьмем входную цепочку «if (a or b then а := 25;». После выполнения лексического анализа, если все лексемы типа «идентифика- тор» и «константа» обозначить как «а», получим цепочку: «if {a or a then а := «». Рассмотрим процесс синтаксического анализа этой входной цепочки: {if (a or a then а := а;±J-L.JX} + п {(a or a then а := a;lJlnif|X} + н {a or a then а := a;lK|±iiif(|X} + „ {ora then а := a;±K|±iiif(a|X} + с {or a then а := a;±JlHif(E|12} + н {a then а := a;lK|±uif(E or| 12} + и {then а := a;±K|±uif(E or a|12} + c {then a := a;lK|±uif(E or E]12 12} + c {then a := a;±K|±i)if(E]12 12 7} — нет отношения предшествования между лексема- ми «(» и «then», разбор закончен, МП-автомат не перешел в конечную конфигу- рацию, цепочка не принята (выдается сообщение об ошибке).
88 Лабораторная работа № 3 • Построение простейшего дерева вывода Реализация синтаксического распознавателя Разбиение на модули В лабораторной работе № 3, так же, как и в лабораторной работе № 2, модули, реализующие синтаксический анализатор разделены на две группы: □ модули, программный код которых не зависит от входного языка; □ модули, программный код которых зависит от входного языка. В первую группу входят модули: □ SyntSymb — описывает структуры данных для синтаксического анализа и реа- лизует алгоритм «сдвиг-свертка» для грамматик операторного предшествова- ния; □ FormLab3 — описывает интерфейс с пользователем. Во вторую группу входит один модуль: □ SyntRule — содержит описания матрицы операторного предшествования и пра- вил исходной грамматики. Такое разбиение на модули позволяет использовать те же самые структуры дан- ных для организации синтаксического распознавателя при изменении входного языка. Кроме этих модулей для реализации лабораторной работы № 3 используются программные модули TblElem и FncTree, позволяющие работать с комбинирован- ной таблицей идентификаторов, которые были созданы при выполнении лабора- торной работы № 1, а также модули LexType, LexEl em, и LexAuto, которые обеспечи- вают работу лексического распознавателя (эти модули были созданы при выпол- нении лабораторной работы № 2). Кратко опишем содержание программных модулей, используемых для организа- ции синтаксического анализатора. Модуль описания матрицы предшествования и правил грамматики Модуль SyntRule содержит структуры данных, которые описывают матрицу опе- раторного предшествования и правила остовной грамматики. Матрица операторного предшествования (GramMatrix) описана как двумерный массив, каждой строке и каждому столбцу которого соответствует лексема (тип TLexType). Важно, чтобы данные в строках и столбцах матрицы были заполнены в том же порядке, в каком перечислены типы лексем в описании TLexType в моду- ле LexType. В каждой клетке матрицы находится символ, обозначающий тип отно- шения предшествования: □ — для отношения «<•» («предшествует»); □ ’>' — для отношения «•>» («следует»); □ '=' — для отношения «=•» («составляет основу»);
Пример выполнения работы 89 □ ' ’ — для пустых клеток матрицы (когда отношениеоператориого предшество- вания между двумя символами отсутствует). Кроме матрицы операторного предшествования и правил грамматики в модуле SyntRuleописана функция корректировки отношений предшествования CorrectRule, которая позволяет расширять возможности грамматики операторного предшество- вания. В данной лабораторной работе эта функция не используется (о технике ее использования можно узнать далее из описания примера выполнения курсовой работы). В целом описанная в модуле SyntRule матрица операторного предшествования GramMatrix полностью соответствует построенной матрице операторного предше- ствования (см. табл. 3.8). Отличие заключается в том, что, поскольку терминаль- ному символу а в грамматике G могут соответствовать два типа лексем входного языка (переменные и константы), в матрице GramMatrix строка и столбец, соответ- ствующие символу а в табл. 3.8, продублированы. Таким образом, построенный на основе матрицы предшествования из табл. 3.8 синтаксический анализатор не различает константы и переменные. Это соот- ветствует синтаксису заданного входного языка. Для этого языка проводить различие между переменными и константами необходимо только в одном слу- чае: при анализе оператора присваивания (присваивать значение константе нельзя). Для того чтобы компилятор находил такого рода ошибки, возможны два варианта: 1. Изменить синтаксис входного языка (грамматику G) так, чтобы константы и пе- ременные различались в правилах грамматики, и перестроить синтаксический анализатор. 2. Обрабатывать присваивание значений константам на этапе семантического анализа. В данном случае выбран второй вариант, который реализован в лабораторной ра- боте № 4 (где рассматриваются генерация кода и подготовка к генерации кода). Позже, при разработке компилятора для выполнения курсовой работы, рассмот- рен первый вариант (см. главу, посвященную выполнению курсовой работы). Каждый из рассмотренных вариантов имеет свои преимущества и недостатки. В общем случае выбор того, на каком этапе компиляции будет обнаружена га или иная ошибка, зависит от разработчика компилятора. Правила остовной грамматики G' описаны в виде массива строк GramRules. Каж- дому правилу в этом массиве соответствует строка, по написанию совпадающая с правой частью правила (пробелы игнорируются). Правила пронумерованы в по- рядке слева направо и сверху вниз — так, как они были пронумерованы в остовной грамматике G’. Для поиска подходящего правила используется метод простого перебора — так как правил мало (всего 13), в данном случае этот метод вполне удовлетворителен. Кроме двух упомянутых структур данных (GramMatrix и GramRules) в модуле SyntRule описана также функция MakeSymbol Str, возвращающая наименование нетерминаль- ного символа в правилах остовной грамматики. В грамматике G’ во всех правилах
90 Лабораторная работа № 3 • Построение простейшего дерева вывода символ обозначен Е, поэтому функция MakeSymbol Str всегда возвращает ' Е' как результат своего выполнения. Но тем не менее эта функция не бессмысленна, так как могут быть другие варианты остовных грамматик. Модуль структур данных для синтаксического анализа и реализации алгоритма «сдвиг-свертка» Модуль SyntSymb содержит реализацию алгоритма «сдвиг-свертка» и описания всех структур данных, необходимых для этой реализации. Поскольку сам алгоритм «сдвиг-свертка» не зависит от входного языка, реализующий его модуль также не зависит от входного языка и правил исходной грамматики (они специально вы- несены в отдельный модуль). Основу модуля составляют следующие структуры данных: □ TSymblnfo — описание двух типов символов грамматики: терминальных и не- терминальных; □ TSymbol — описание всех данных, связанных с понятием «символ грамматики»; □ TSymbStack — описание синтаксического стека. Структура TSymblnfo содержит информацию о типе символа грамматики — поле SymbType, которое может принимать два значения: SYMB LEX (терминальный сим- вол) или SYMB SYNT (нетерминальный символ), и дополнительные данные: □ ссылку на лексему (LexOne) — для терминального символа; □ перечень всех составляющих (LexList) — для нетерминального символа. Перечень всех составляющих нетерминального символа LexList построен на ос- нове динамического массива (тип TList из библиотеки VCL системы программи- рования Delphi 5). В него вносятся ссылки на символы, на основании которых создан данный символ, в том порядке, в котором они следуют в правиле грамма- тики. СтруктураTSymbol содержит информацию о символе (поле Symblnfo типа TSymblnfo), а также номер правила грамматики, на основании которого создан символ (поле данных iRuleNum). Для терминальных символов номер правила равен 0, для нетер- минальных символов он может быть от 1 до 13. Кроме этих данных структура содержит методы, необходимые для работы с сим- волами грамматики: □ конструктор CreateLex для создания терминального символа на основе лек- семы; □ конструктор CreateSymb для создания нетерминального символа на основе пра- вила грамматики и массива исходных символов; □ деструктор Destroy для освобождения занятой памяти при удалении символа (при удалении нетерминального символа удаляются все ссылки на его состав- ляющие и динамический массив для их хранения); □ функции, процедуры и свойства для работы с информацией, хранящейся в струк- туре данных.
Пример выполнения работы 91 Поскольку в поле данных Symblnfo структуры TSymbol хранятся все ссылки на со- ставляющие символы, внутри которых, в свою очередь, могут храниться ссылки на их составляющие и т. д., то на основе структуры TSymbol можно построить пол- ное синтаксическое дерево разбора. Третья структура данных TSymbStack построена на основе динамического.массива типа TList из библиотеки VCL системы программирования Delphi 5. Она пред- назначена для того, чтобы моделировать синтаксический стек МП-автомата. В этой структуре нет никаких данных (используются только данные, унаследо- ванные от класса TList), но с ней связаны методы, необходимые для работы син- таксического стека: □ функция очистки стека (Clear) и деструктор для освобождения памяти при удалении стека (Destroy); □ функция доступа к символам в стеке начиная от ето вершины (GetSymbol); □ функция для помещения в стек очередной входящей лексемы (Push), при этом лексема преобразуется в терминальный символ; □ функция, возвращающая самую верхнюю лексему в стеке (TopLexem), при этом нетерминальные символы игнорируются; □ функция, выполняющая свертку (MakeTopSymb); новый символ, полученный в ре- зультате свертки, помещается на вершину стека. Кроме трех перечисленных ранее структур данных в модуле SyntSymb описана также функция BuildSyntList, моделирующая работу алгоритма «сдвиг-свертка» для грам- матик операторного предшествования. Входными данными для функции явля- ются список лексем (1 istLex), который должен быть заполнен в результате лекси- ческого анализа, и синтаксический стек (symbStack), который в начале выполне- ния функции должен быть пуст. Результатом функции является: □ нетерминальный символ (ссылающийся на корень синтаксического дерева), если разбор был выполнен успешно; □ терминальный символ, ссылающийся на лексему, где была обнаружена ошиб- ка, если разбор выполнен с ошибками. Функция BuildSyntList моделирует алгоритм «сдвиг-свертка» для грамматик опе- раторного предшествования так, как он был описан в разделе «Краткие теорети- ческие сведения». Текст программы распознавателя Кроме перечисленных выше модулей необходим еще модуль, обеспечивающий интерфейс с пользователем. Этот модуль (FormLab3) реализует графическое окно TLab3Form на основе класса TForm библиотеки VCL и включает в себя две составля- ющие: □ файл программного кода (файл FormLab3.pas); □ файл описания ресурсов пользовательского интерфейса (файл FormLab3.dfm).
92 Лабораторная работа № 3 • Построение простейшего дерева вывода Модуль FormLab3 построен на основе модуля FormLab2, который использовался для реализации интерфейса с пользователем в лабораторной работе № 2. Он содер- жит все данные, управляющие и интерфейсные элементы, которые были исполь- зованы в лабораторной работе № 2, поскольку первым этапом лабораторной ра- боты № 3 является лексический анализ, который выполняется модулями, создан- ными для лабораторной работы № 2. Кроме данных, используемых для выполнения лексического анализа так, как это было описано в лабораторной работе № 2, модуль содержит поле symbStack, кото- рое представляет собой синтаксический стек, используемый для выполнения син- таксического анализа. Этот стек инициализируется при создании интерфейсной формы и уничтожается при ее закрытии. Он также очищается всякий раз, когда запускаются процедуры лексического и синтаксического анализа. Кроме органов управления, использованных в лабораторной работе № 2, интер- фейсная форма, описанная в модуле FormLab3, содержит органы управления для синтаксического анализатора лабораторной работы № 3: □ в многостраничной вкладке (PageControl 1) появилась новая закладка (SheetSynt) под названием «Синтаксис»; □ на закладке SheetSynt расположен интерфейсный элемент для просмотра иерар- хических структур (TreeSynt типа TTreeView). Внешний вид новой закладки интерфейсной формы TLab3Form приведен на рис. 3.3. Чтение содержимого входного файла организовано точно так же, как в лабора- торной работе № 2. После чтения файла выполняется лексический анализ, как это было описано в ла- бораторной работе № 2. Если лексический анализ выполнен успешно, то в список лексем listLex добавля- ется информационная лексема, обозначающая конец строки, после чего вызыва- ется функция выполнения синтаксического анализа Bui 1 dSyntList, на вход кото- рой подаются список лексем (listLex) и синтаксический стек (symbStack). Резуль- тат выполнения функции запоминается во временной Переменной symbRes. Если переменная symbRes содержит ссылку на лексему, это значит, что синтакси- ческий анализ выполнен с ошибками и эта лексема как раз указывает на то место, где была обнаружена ошибка. Тогда список строк входного файла позициониру- ется на указанное место ошибки, а пользователю выдается сообщение об ошибке. Иначе, если ошибок не обнаружено, переменная symbRes указывает на корень по- строенного синтаксического дерева. Тогда в интерфейсный элемент TreeSynt за- писывается ссылка на корень синтаксического дерева, после чего все дерево ото- бражается на экране с помощью функции MakeTree. Функция MakeTree обеспечивает рекурсивное отображение синтаксического дере- ва в интерфейсном элементе типаTTreeView. Элемент типа TTreeView является стан- дартным интерфейсным элементом в ОС типа Windows для отображения иерар- хических структур (например он используется для отображения файловой струк- туры).
Пример выполнения работы 93 Рис. 3.3. Внешний вид третьей закладки интерфейсной формы для лабораторной работы № 3 Полный текст программного кода модуля интерфейса с пользователем и описа- ние ресурсов пользовательского интерфейса находятся в архиве, находящемся на веб-сайте издательства, в файлах FormL.ab3.pas и FormLab3.dfm соответственно. Полный текст всех программных модулей, реализующих рассмотренный пример для лабораторной работы N_> 3, можно найти в архиве, находящемся на веб-сайте издательства, в подкаталогах LABS и COMMON (в подкаталог COMMON вынесены те программные модули, исходный текст которых не зависит от входного языка и задания по лабораторной работе). Главным файлом проекта является файл LAB3.DPR в подкаталоге LABS. Кроме того, текст модуля SyntSymb приведен в лис- тинге П3.7 в приложении 3. Выводы по проделанной работе В результате лабораторной работы № 3 построен синтаксический анализатор на основе грамматики операторного предшествования. Синтаксический анализ по- зволяет проверять соответствие структуры исходного текста заданной граммати- ке входного языка. Синтаксический анализ позволяет обнаруживать любые син- таксические ошибки во входной программе. При наличии одной ошибки пользо- вателю выдается сообщение с указанием местоположения ошибки в исходном
94 Лабораторная работа № 3 * Построение простейшего дерева вывода тексте. Анализ типа обнаруженной ошибки не производится. При наличии не- скольких ошибок в исходном тексте обнаруживается только первая из них, после чего дальнейший анализ не выполняется. Результатом работы синтаксического анализатора является структура данных, представляющая синтаксическое дерево. В комплексе с лексическим анализато- ром, созданным при выполнении лабораторной работы № 2, построенный син- таксический анализатор позволяет выполнять подготовку данных, необходимых для выполнения следующей лабораторной работы, связанной с генерацией кода.
ЛАБОРАТОРНАЯ РАБОТА № 4 Генерация и оптимизация объектного кода Цель работы Цель работы-, изучение основных принципов генерации компилятором объект- ного кода, ознакомление с методами оптимизации результирующего объектного кода для линейного участка программы с помощью свертки и исключения лиш- них операций. Краткие теоретические сведения Общие принципы генерации кода Генерация объектного кода — это перевод компилятором внутреннего представ- ления исходной программы в цепочку символов выходного языка. Поскольку выходным языком компилятора (в отличие от транслятора) может быть только либо язык ассемблера, либо язык машинных кодов, то генерация кода порождает результирующую объектную программу на языке ассемблера или непосредствен- но на машинном языке (в машинных кодах). Генерация объектного кода выполняется после того, как выполнены лексический и синтаксический анализ программы и все необходимые действия по подготовке к генерации кода: проверены семантические соглашения входного языка (семан-
96 Лабораторная работа № 4 • Генерация и оптимизация объектного кода тический анализ), выполнена идентификация имен переменных и функций, рас- пределено адресное пространство под функции и переменные и т. д. В данной лабораторной работе используется предельно простой входной язык, поэтому нет необходимости выполнять все перечисленные преобразования. Бу- дем считать, что все они уже выполнены. Более подробно все эти фазы компиля- ции описаны в [1-4,7], а здесь речь будет идти только о самых примитивных при- емах семантического анализа, которые будут проиллюстрированы на примере выполнения лабораторной работы. Внутреннее представление программы может иметь любую структуру в зависи- мости от реализации компилятора, в то время как результирующая программа всегда представляет собой линейную последовательность команд. Поэтому гене- рация объектного кода (объектной программы) в любом случае должна выпол- нять действия, связанные с преобразованием сложных синтаксических структур в линейные цепочки. Генерацию кода можно считать функцией, определенной на синтаксическом де- реве, построенном в результате синтаксического анализа, и на информации, со- держащейся в таблице идентификаторов. Характер отображения входной програм- мы в последовательность команд, выполняемого генерацией, зависит от входного ^зыка, архитектуры целевой вычислительной системы, на которую ориентирова- на результирующая программа, а также от качества желаемого объектного кода. В идеале компилятор должен выполнить синтаксический анализ всей входной про- граммы, затем провести ее семантический анализ, после чего приступать к подго- товке генерации и непосредственно генерации кода. Однако такая схема работы компилятора практически почти никогда не применяется. Дело в том, что в общем случае ни один семантический анализатор и ни один компилятор не способны про- анализировать и оценить смысл всей исходной программы в целом. Формальные методы анализа семантики применимы только к очень незначительной части воз- можных исходных программ. Поэтому у компилятора нет практической возмож- ности порождать эквивалентную результирующую программу на основе всей ис- ходной программы. Как правило, компилятор выполняет генерацию результирующего кода поэтапно, на основе законченных синтаксических конструкций входной программы. Компи- лятор выделяет законченную синтаксическую конструкцию из текста исходной программы, порождает для нее фрагмент результирующего кода и помещает его в текст результирующей программы. Затем он переходит к следующей синтакси- ческой конструкции. Так продолжается до тех пор, пока не будет разобрана вся ис- ходная программа. В качестве анализируемых законченных синтаксических кон- струкций выступают блоки операторов, описания процедур и функций. Их конк- ретный состав зависит от входного языка и реализации компилятора. Смысл (семантику) каждой такой синтаксической конструкции входного язы- ка можно определить, исходя из ее типа, а тип определяется синтаксическим анализатором на основе грамматики входного языка. Примерами типов синтак- сических конструкций могут -служить операторы цикла, условные операторы, операторы выбора и т. д. Одни и те же типы синтаксических конструкций ха-
Natatfauslik Краткие теоретические сведения 97 рактерны для различных языков программирования, при этом они различаются синтаксисом (который задается грамматикой языка), но имеют схожий смысл (который определяется семантикой). В зависимости от типа синтаксической конструкции выполняется генерация кода результирующей программы, соот- ветствующего данной синтаксической конструкции. Для семантически схожих конструкций различных входных языков программирования может порождать- ся типовой результирующий код. Синтаксически управляемый перевод Чтобы компилятор мог построить код результирующей программы для синтак- сической конструкции входного языка, часто используется метод, называемый синтаксически управляемым переводом — СУ-переводом. Идея СУ-перевода основана на том, что синтаксис и семантика языка взаимосвя- заны. Это значит, что смысл предложения языка зависит от синтаксической струк- туры этого предложения. Теория синтаксически управляемого перевода была предложена американским лингвистом Ноамом Хомским. Она справедлива как для формальных языков, так и для языков естественного общения: например, смысл предложения русского языка зависит от входящих в пего частей речи (под- лежащего, сказуемого, дополнений и др.) и от взаимосвязи между ними. Однако естественные языки допускают неоднозначности в грамматиках — отсюда проис- ходят различные двусмысленные фразы, значение которых человек обычно по- нимает из того контекста, в котором эти фразы встречаются (и то он не всегда может это сделать). В языках программирования неоднозначности в граммати- ках исключены, поэтому любое предложение языка имеет четко определенную структуру и однозначный смысл, напрямую связанный с этой структурой. Входной язык компилятора имеет бесконечное множество допустимых предло- жений, поэтому невозможно задать смысл каждого предложения. Но все входные предложения строятся на основе конечного множества правил грамматики, кото- рые всегда можно найти. Так как этих правил конечное число, то для каждого правила можно определить его семантику (значение). Но абсолютно то же самое можно утверждать и для выходного языка компилято- ра. Выходной язык содержит бесконечное множество допустимых предложений, но все они строятся на основе конечного множества известных правил, каждое из которых имеет определенную семантику (смысл). Если по отношению к исход- ной программе компилятор выступает в роли распознавателя, то для результиру- ющей программы он является генератором предложений выходного языка. Зада- ча заключается в том, чтобы найти порядок правил выходного языка, по которым необходимо выполнить генерацию. Грубо говоря, идея СУ-перевода заключается в том, что каждому правилу вход- ного языка компилятора сопоставляется одно или несколько (или ни одного) пра- вил выходного языка в соответствии с семантикой входных и выходных правил. То есть при сопоставлении надо выбирать правила выходного языка, которые не- сут тот же смысл, что и правила входного языка. 4 Зак. 68
98 Лабораторная работа № 4 • Генерация и оптимизация объектного кода СУ-перевод — это основной метод порождения кода результирующей программы на основании результатов синтаксического анализа. Для удобства понимания сути метода можно считать, что результат синтаксического анализа представлен в виде дерева синтаксического анализа, хотя в реальных компиляторах это не всегда так. Суть принципа СУ-перевода заключается в следующем: с каждой вершиной дерева синтаксического разбора Nсвязывается цепочка некоторого промежуточ- ного кода C(N). Код для вершины Достроится путем сцепления (конкатенации) в фиксированном порядке последовательности кода С(№) и последовательностей кодов, связанных со всеми вершинами, являющимися прямыми потомками N. В свою очередь, для построения последовательностей кода прямых потомков вер- шины N потребуется найти последовательности кода для их потомков — потом- ков второго уровня вершины N — и т. д. Процесс перевода идет, таким образом, снизу вверх в строго установленном порядке, определяемом структурой дерева. Для того чтобы построить СУ-перевод по заданному дереву синтаксического раз- бора, необходимо найти последовательность кода для корня дерева. Поэтому для каждой вершины дерева порождаемую цепочку кода надо выбирать таким обра- зом, чтобы код, приписываемый корню дерева, оказался искомым кодом для все- го оператора, представленного этим деревом. В общем случае необходимо иметь единообразную интерпретацию кода C(N), которая бы встречалась во всех ситуа- циях, где присутствует вершина N. В принципе, эта задача может оказаться не- тривиальной, так как требует оценки смысла (семантики) каждой вершины дере- ва. При применении СУ-перевода задача оценки смысловой нагрузки для каждой вершины дерева решается разработчиком компилятора. Возможна модель компилятора, в которой синтаксический анализ исходной про- граммы и генерация кода результирующей программы объединены в одну фазу. Такую модель можно представить в виде компилятора, у которого операции ге- нерации кода совмещены с операциями выполнения синтаксического разбора. Для описания компиляторов такого типа часто используется термин СУ-компиляция (синтаксически управляемая компиляция). Схему СУ-компиляции можно реализовать не для всякого входного языка про- граммирования. Если принцип СУ-перевода применим ко всем входным КС-язы- кам, то применить СУ-компиляцию оказывается не всегда возможным [1, 2, 7]. В процессе СУ-перевода и СУ-компиляции не только вырабатываются цепочки текста выходного языка, но и совершаются некоторые дополнительные действия, выполняемые самим компилятором. В общем случае схемы СУ-неревода могут, предусматривать выполнение следующих действий: □ помещение в выходной поток данных машинных кодов или команд ассембле- ра, представляющих собой результат работы (выход) компилятора; □ выдача пользователю сообщений об обнаруженных ошибках и предупрежде- ниях (которые должны помещаться в выходной поток, отличный от потока, используемого для команд результирующей программы); □ порождение и выполнение команд, указывающих, что некоторые действия должны быть произведены самим компилятором (например операции, выпол- няемые над данными, размещенными в таблице идентификаторов).
ftataHausI^!. Краткие теоретические сведения 99 Ниже рассмотрены некоторые основные технические вопросы, позволяющие ре- ализовать схемы СУ-перевода для данной лабораторной работы. Более подробно с механизмами СУ-перевода и СУ-компиляции можно ознакомиться в [1, 2, 7]. Способы внутреннего представления программ Результатом работы синтаксического анализатора на основе КС-грамматики вход- ного языка является последовательность правил грамматики, примененных для построения входной цепочки. По найденной последовательности, зная тип рас- познавателя, можно построить цепочку вывода или дерево вывода. В этом случае дерево вывода выступает в качестве дерева синтаксического разбора и представ- ляет собой результат работы синтаксического анализатора в компиляторе. Однако ни цепочка вывода, ни дерево синтаксического разбора не являются целью работы компилятора. Для полного представления о структуре разобранной синтак- сической конструкции входного языка в принципе достаточно знать последователь- ность номеров правил грамматики, примененных для ее построения. Однако фор- ма представления этой информации может быть различной в зависимости как от реализации самого компилятора, так и от фазы компиляции. Эта форма называет- ся внутренним представлением программы (иногда используются также термины промежуточное, представление или промежуточная программа). Все внутренние представления программы обычно содержат в себе два принци- пиально различных элемента — операторы и операнды. Различия между форма- ми внутреннего представления заключаются лишь в том, как операторы и опе- ранды соединяются между собой. Также операторы и операнды должны отличать- ся друг от друга, если они встречаются в любом порядке. За различение операндов и операторов, как уже было сказано выше, отвечает разработчик компилятора, ко- торый руководствуется семантикой входного языка. Известны следующие формы внутреннего представления программ1: □ структуры связных списков, представляющие синтаксические деревья; □ многоадресный код с явно именуемым результатом (тетрады); □ многоадресный код с неявно именуемым результатом (триады); □ обратная (постфиксная) польская запись операций; □ ассемблерный код или машинные команды. В каждом конкретном компиляторе может использоваться одна из этих форм, выбранная разработчиками. Но чаще всего компилятор не ограничивается исполь- зованием только одной формы для внутреннего представления программы. 1 Существуют три формы записи выражений — префиксная, инфиксная и постфиксная. При префикс- ной записи операция записывается перед своими операндами, при инфиксной — между операнда- ми, а при постфиксной — после операндов. Общепринятая запись арифметических выражений яв- ляется примером инфиксной записи. Запись математических функций и функций в языках про- граммирования является префиксной (другие примеры префиксной записи — команды ассемблера и триады в том виде, как они рассмотрены далее). Постфиксная запись в повседневной жизни встре- чается редко. С нею сталкиваются разве что пользователи стековых калькуляторов и программи- сты на языке Forth.
100 Лабораторная работа № 4 • Генерация и оптимизация объектного кода На различных фазах компиляции могут использоваться различные формы, кото- рые по мере выполнения проходов компилятора преобразуются одна в другую. Некоторые компиляторы, незначительно оптимизирующие результирующий код, генерируют объектный код по мере разбора исходной программы. В этом случае применяется схема СУ-компиляции, когда фазы синтаксического разбора, семан- тического анализа, подготовки и генерации объектного кода совмещены в одном проходе компилятора. Тогда внутреннее представление программы существует только условно в виде последовательности шагов алгоритма разбора. Алгоритмы, предложенные, для выполнения данной лабораторной работы, по- строены на основе использования формы внутреннего представления программы в виде триад. Поэтому далее будет рассмотрена именно эта форма внутреннего представления программы. С остальными формами можно более подробно позна- комиться в [1-3,7]. Многоадресный код с неявно именуемым результатом (триады) Триады представляют собой запись операций в форме из трех составляющих: операция и два операнда. Например, в строковой записи триады могут иметь вид: <операция>(<операнд1>,<операнд2>). Особенностью триад является то, что один или оба операнда могут быть ссылками на другую триаду в том случае, если в каче- стве операнда данной триады выступает результат выполнения другой триады. Поэтому триады при записи последовательно нумеруют для удобства указания ссылок одних триад на другие (в реализации компилятора в качестве ссылок можно использовать не номера триад, а непосредственно ссылки в виде указа- телей — тогда при изменении нумерации и порядка следования триад менять ссылки не требуется). Например, выражение A:=B«C+D-B-10, записанное в виде триад, будет иметь вид: 1: * ( В. С ) 2: + ( *1. D ) 3: * ( В. 10 ) 4: - ( *2. ~3 ) 5: := ( А. *4 ) Здесь операции обозначены соответствующими знаками (при этом присваивание также является операцией), а знак " означает ссылку операнда одной триады на результат другой. Триады представляют собой линейную последовательность команд. При вычис- лении выражения, записанного в форме триад, они вычисляются одна за другой последовательно. Каждая триада в последовательности вычисляется так: опера- ция, заданная триадой, выполняется над операндами, а если в качестве одного из операндов (или обоих операндов) выступает ссылка на другую триаду, то берется результат вычисления той триады. Результат вычисления триады нужно сохра- нять во временной памяти, так как он может быть затребован последующими три-
^aiattaus,^. Краткие теоретические сведения 101 адами. Если какой-то из операндов в триале отсутствует (например, если триада представляет собой унарную операцию), то он может быть опущен или заменен пустым операндом (в зависимости от принятой формы записи и ее реализации). Порядок вычисления триад может быть изменен, но только если допустить нали- чие триад, целенаправленно изменяющих этот порядок (например, триады, вы- зывающие безусловный переход на другую триаду с заданным номером или пере- ход на несколько шагов вперед или назад при каком-то условии). Триады представляют собой линейную последовательность, а потому для них не- сложно написать тривиальный алгоритм, который будет преобразовывать после- довательность триад в последовательность команд результирующей программы (либо последовательность команд ассемблера). В этом их преимущество перед синтаксическими деревьями. Однако для триад требуется также и алгоритм, от- вечающий за распределение памяти, необходимой для хранения промежуточных результатов вычисления, так как временные переменные для этой цели не исполь- зуются (в этом отличие триад от тетрад). Триады не зависят от архитектуры вычислительной системы, на которую ориен- тирована результирующая программа. Поэтому они представляют собой машин- но-независимую форму внутреннего представления программы. Триады обладают следующими преимуществами: □ являются линейной последовательностью операций, в отличие от синтакси- ческого дерева, и потому проще преобразуются в результирующий код; □ занимают меньше памяти, чем тетрады, дают больше возможностей по опти- мизации программы, чем обратная польская запись; □ явно отражают взаимосвязь операций между собой, что делает их применение удобным, особенно при оптимизации внутреннего представления программы; □ промежуточные результаты вычисления триад могут храниться в регистрах процессора, что удобно при распределении регистров и выполнении машин- но-зависимой оптимизации; □ по форме представления находятся ближе к двухадресным машинным коман- дам, чем другие формы внутреннего представления программ, а именно эти команды более всего распространены в наборах команд большинства совре- менных компьютеров. Необходимость создания алгоритма, отвечающего за распределение памяти для хранения промежуточных результатов, является главным недостатком триад. Но при грамотном распределении памяти и регистров процессора этот недостаток может быть обращен на пользу разработчиками компилятора. Схемы СУ-перевода Ранее был описан принцип СУ-перевода, позволяющий получить линейную по- следовательность команд результирующей программы или внутреннего представ- ления программы в компиляторе на основе результатов синтаксического анали- за. Теперь построим вариант алгоритма генерации кода, который получает на входе
102 Лабораторная работа № 4 • Генерация и оптимизация объектного кода дерево синтаксического разбора и создает по нему последовательность триад (да- лее — просто «триады») для линейного участка результирующей программы. Рас- смотрим примеры схем СУ-перевода для бинарных арифметических операций. Эти схемы достаточно просты, и на их основе можно проиллюстрировать, как выполняется СУ-перевод в компиляторе при генерации кода. Для построения триад по синтаксическому дереву может использоваться простей- шая рекурсивная процедура обхода дерева. Можно использовать и другие мето- ды обхода дерева — важно, чтобы соблюдался принцип, согласно которому ниже- лежащие операции в дереве всегда выполняются перед вышележащими операци- ями (порядок выполнения операций одного уровня не важен, он нс влияет на результат и зависит от порядка обхода вершин дерева). Процедура генерации триад по синтаксическому дереву прежде всего должна опре- делить тип узла дерева. Для бинарных арифметических операций каждый узел дерева имеет три нижележащие вершины (левая вершина — первый операнд, сред- няя вершина — операция и правая вершина — второй операнд). При этом тип узла дерева соответствует типу операции, символом которой помечена средняя из ни- жележащих вершин. После определения типа узла процедура строит триады для узла дерева в соответствии с типом операции. Фактически процедура генерации триад должна для каждого узла дерева выполнить конкатенацию триады, связанной с текущим узлом, и цепочек триад, связанных с ни- жележащими узлами. Конкатенация цепочек триад должна выполняться таким об- разом, чтобы триады, связанные с нижележащими узлами, выполнялись до выпол- нения операции, связанной с текущим узлом. Причем для арифметических опера- ций важно, чтобы триады, связанные с первым операндом, выполнялись раньше, чем триады, связанные со вторым операндом (так как все арифметические операции при отсутствии скобок и приоритетов выполняются в порядке слева направо). При этом возможны четыре ситуации: □ левая и правая вершины указывают на непосредственный операнд (это можно определить, если у каждой из них есть только один нижележащий узел, поме- ченный символом какой-то лексемы — константы или идентификатора); □ левая вершина является непосредственным операндом, а правая указывает на другую операцию; □ левая вершина указывает на другую операцию, а правая является непосред- ственным операндом; □ обе вершины указывают на другую операцию. Считаем, что на вход процедуры порождения триад по синтаксическому дереву подается список, в который нужно добавлять триады, и ссылка на узел дерева, который надо обработать. Тогда процедура порождения триад для узла синтакси- ческого дерева, связанного с бинарной арифметической операцией, может выпол- няться по следующему алгоритму: 1. Проверяется тип левой вершины узла. Если она — простой операнд, запомина- ется имя первого операнда, иначе для этой вершины рекурсивно вызывается процедура порождения триад, построенные ею триады добавляются в конец
^alatiaus^!, Краткие теоретические сведения 103 общего списка и запоминается номер последней триады из этого списка как первый операнд. 2. Проверяется тип правой вершины узла. Если она — простой операнд, запомина- ется имя второго операнда, иначе для этой вершины рекурсивно вызывается процедура порождения триад, построенные ею триады добавляются в конец об- щего списка и запоминается номер последней триады как второй операнд. 3. В соответствии с типом средней вершины в конец общего списка добавляется триада, соответствующая арифметической операции. Ее первым операндом становится операнд, запомненный на шаге 1, а вторым операндом — операнд, запомненный на шаге 2. 4. Процедура закончена. Процедуры такого рода должен создавать разработчик компилятора, так как только он может сопоставить по смыслу узлы синтаксического дерева и соответствую- щие им последовательности триад. Для разных типов узлов синтаксического де- рева могут быть построены разные варианты процедур, которые будут вызывать друг друга в зависимости от принятого порядка обхода синтаксического дерева (в описанном выше варианте — рекурсивно). В рассмотренном примере при порождении кода преднамеренно не были приня- ты во внимание многие вопросы, возникающие при построении реальных компи- ляторов. Это было сделано для упрощения примера. Например, фрагменты кода, соответствующие различным узлам дерева, принимают во внимание тип опера- ции, но никак не учитывают тип операндов. Все эти требования ведут к тому, что в реальном компиляторе при генерации кода надо принимать во внимание очень многие особенности, зависящие от семантики входного языка и от используемой формы внутреннего представления программы. В данной лабораторной работе эти вопросы не рассматриваются. Кроме того, в случае арифметических операций код, порождаемый для узлов син- таксического дерева, зависит только от типа операции, то есть только от текущего узла дерева. Такие схемы можно построить для многих операций, но не для всех. Иногда код, порождаемый для узла дерева, может зависеть от типа вышестоящего узла: например, код, порождаемый для операторов типа Break и Continue (которые есть в языках С, C++ и Object Pascal), зависит от того, внутри какого цикла они находятся. Тогда при рекурсивном построении кода по дереву вышестоящий узел, вызывая функцию для нижестоящего узла, должен передать ей необходимые пара- метры. Но код, порождаемый для вышестоящего узла, никогда не должен зависеть от нижестоящих узлов, в противном случае принцип СУ-перевода неприменим. Далее в примере выполнения работы даются варианты схем СУ-перевода для раз- личных конструкций входного языка, которые могут служить хорошей иллюст- рацией механизма применения этого метода. Общие принципы оптимизации кода Как уже говорилось, в подавляющем большинстве случаев генерация кода выпол- няется компилятором не для всей исходной программы в целом, а последовательно
104 Лабораторная работа № 4 • Генерация и оптимизация объектного кода для отдельных ее конструкций. Для построения результирующего кода различ- ных синтаксических конструкций входного языка используется метод СУ-перевода. Он объединяет цепочки построенного кода по структуре дерева без учета их взаимосвязей. Построенный таким образом код результирующей программы может содержать лищние команды и данные. Это снижает эффективность выполнения результиру- ющей программы. В принципе, компилятор может завершить на этом генерацию кода, поскольку результирующая программа построена и является эквивалентной по смыслу (семантике) программе на входном языке. Однако эффективность ре- зультирующей программы важна для ее разработчика, поэтому большинство со- временных компиляторов выполняют еще один этап компиляции — оптимизацию результирующей программы (или просто «оптимизацию»), чтобы повысить ее эф- фективность, насколько это возможно. Важно отметить два момента: во-первых, выделение оптимизации в отдельный этап генерации кода — это вынужденный шаг. Компилятор вынужден произво- дить оптимизацию построенного кода, поскольку он не может выполнить семан- тический анализ всей входной программы в целом, оценить ее смысл и исходя из него построить результирующую программу. Во-вторых, оптимизация — это не- обязательный этап компиляции. Компилятор может вообще не выполнять опти- мизацию, и при этом результирующая программа будет правильной, а сам ком- пилятор будет полностью выполнять свои функции. Однако практически все со- временные компиляторы так или иначе выполняют оптимизацию, поскольку их разработчики стремятся завоевать хорошие позиции на рынке средств разработ- ки программного обеспечения. Теперь дадим определение понятию «оптимизация». Оптимизация программы — это обработка, связанная с переупорядочиванием и из- менением операций в компилируемой программе с целью получения более эф- фективной результирующей объектной программы. Оптимизация выполняется на этапах подготовки к генерации и непосредственно при генерации объектного кода. В качестве показателей эффективности результирующей программы можно ис- пользовать два критерия: объем памяти, необходимый для выполнения результи- рующей программы, и скорость выполнения (быстродействие) программы. Дале- ко не всегда удается выполнить оптимизацию так, чтобы она удовлетворяла обо- им этим критериям. Зачастую сокращение необходимого программе объема данных ведет к уменьшению ее быстродействия, и наоборот. Поэтому для опти- мизации обычно выбирается один из упомянутых критериев. Выбор критерия оп- тимизации обычно выполняется в настройках компилятора. Но даже выбрав критерий оптимизации, в общем случае практически невозмож- но построить код результирующей программы, который бы являлся самым ко- ротким или самым быстрым кодом, соответствующим входной программе. Дело в том, что нет алгоритмического способа нахождения самой короткой или самой быстрой результирующей программы, эквивалентной заданной исходной програм- ме. Эта задача в принципе неразрешима. Существуют алгоритмы, которые можно
Краткие теоретические сведения 105 ускорять сколь угодно много раз для большого числа возможных входных дан- ных, и при этом для других наборов входных данных они окажутся неоптималь- ными [1, 2]. К тому же компилятор обладает весьма ограниченными средствами анализа семантики всей входной программы в целом. Все, что можно сделать на этапе оптимизации, — это выполнить над заданной программой последователь- ность преобразований в надежде сделать ее более эффективной. Чтобы оценить эффективность результирующей программы, полученной с помо- щью того или иного компилятора, часто прибегают к сравнению ее с эквивалент- ной программой (программой, реализующей тот же алгоритм), полученной из исходной программы, написанной на языке ассемблера. Лучшие оптимизирую- щие компиляторы могут получать результирующие объектные программы из сложных исходных программ, написанных на языках высокого уровня, почти не уступающие по качеству программам на языке ассемблера. Обычно соотношение эффективности программ, построенных с помощью компиляторов с языков вы- сокого уровня, и программ, построенных с помощью ассемблера, составляет 1,1- 1,3. То есть объектная программа, построенная с помощью компилятора с языка высокого уровня, обычно содержит па 10-30% больше команд, чем эквивалент- ная ей объектная программа, построенная с помощью ассемблера, а также выпол- няется на 10-30% медленнее*. Это очень неплохие результаты, достигнутые компиляторами с языков высокого уровня, если сравнить трудозатраты на разработку программ на языке ассембле- ра и языке высокого уровня. Далеко не каждую программу можно реализовать на языке ассемблера в приемлемые сроки (а значит и выполнить напрямую приве- денное выше сравнение можно только для узкого круга программ). Оптимизацию можно выполнять на любой стадии генерации кода, начиная от за- вершения синтаксического разбора и вплоть до последнего этапа, когда порожда- ется код результирующей программы. Если компилятор использует несколько различных форм внутреннего представления программы, то каждая из них может быть подвергнута оптимизации, причем различные формы внутреннего представ- ления ориентированы на различные методы оптимизации [1-3, 7]. Таким обра- зом, оптимизация в компиляторе может выполняться несколько раз на этапе ге- нерации кода. Принципиально различаются два основных вида оптимизирующих преобразова- ний: □ преобразования исходной программы (в форме ее внутреннего представления в компиляторе), не зависящие от результирующего объектного языка; □ преобразования результирующей объектной программы. Первый вид преобразований не зависит от архитектуры целевой вычислитель- ной системы, на которой будет выполняться результирующая программа. Обыч- но он основан на выполнении хорошо известных и обоснованных математических ' Обычно такое сравнение выполняют для специальных тестовых программ, для которых код на язы- ке ассемблера уже заранее известен. Полученные результаты распространяют на все множество входных программ, поэтому их можно считать очень усредненными.
106 Лабораторная работа № 4 • Генерация и оптимизация объектного кода и логических преобразований, производимых над внутренним представлением программы (некоторые из них будут рассмотрены ниже). Второй вид преобразований может зависеть не только от свойств объектного язы- ка (что очевидно), но и от архитектуры вычислительной системы, на которой бу- дет выполняться результирующая программа. Так, например, при оптимизации может учитываться объем кэш-памяти и методы организации конвейерных опе- раций центрального процессора. В большинстве случаев эти преобразования силь- но зависят от реализации компилятора и являются «ноу-хау» производителей компилятора. Именно этот тип оптимизирующих преобразований позволяет су- щественно повысить эффективность результирующего кода. Используемые методы оптимизации ни при каких условиях не должны приво- дить к изменению «смысла» исходной программы (то есть к таким ситуациям, когда результат выполнения программы изменяется после ее оптимизации). Для преобразований первого вида проблем обычно не возникает. Преобразования вто- рого вида могут вызывать сложности, поскольку не все методы оптимизации, ис- пользуемые создателями компиляторов, могут быть теоретически обоснованы и доказаны для всех возможных видов исходных программ. Именно эти преобра- зования могут повлиять на смысл исходной программы. Поэтому у современных компиляторов существуют возможности выбора не только общего критерия оп- тимизации, но и отдельных методов, которые будут использоваться при выпол- нении оптимизации. Нередко оптимизация ведет к тому, что смысл программы оказывается не совсем таким, каким его ожидал увидеть разработчик программы, но не по причине нали- чия ошибки в оптимизирующей части компилятора, а потому, что пользователь не принимал во внимание некоторые аспекты программы, связанные с оптимизацией. Например, компилятор может исключить из программы вызов некоторой функции с заранее известным результатом, но если эта функция имела «побочный эффект» — изменяла некоторые значения в глобальной памяти — смысл программы может измениться. Чаще всего это говорит о плохом стиле программирования исходной программы. Такие ошибки трудноуловимы, для их нахождения разработчику про- граммы следует обратить внимание на предупреждения, выдаваемые семантиче- ским анализатором, или отключить оптимизацию. Применение оптимизации так- же нецелесообразно в процессе отладки исходной программы. Методы преобразования программы зависят от типов синтаксических конструк- ций исходного языка. Теоретически разработаны методы оптимизации для мно- гих типовых конструкций языков программирования. Оптимизация может выполняться для следующих типовых синтаксических кон- струкций: □ линейных участков программы; □ логических выражений; □ циклов; □ вызовов процедур и функций; □ других конструкций входного языка.
yaiallaus^k Краткие теоретические сведения 107 Во всех случаях могут использоваться как машинно-зависимые, так и машинно- независимые методы оптимизации. В лабораторной работе используются два машинно-независимых метода оптими- зации линейных участков программы. Поэтому только эти два метода будут рас- смотрены далее. С другими машинно-независимыми методами оптимизации мож- но более подробно ознакомиться в [ 1,2,7]. Что касается машинно-зависимых ме- тодов, то они, как правило, редко упоминаются в литературе. Некоторые из них рассматриваются в технических описаниях компиляторов. Принципы оптимизации линейных участков Линейный участок программы — это выполняемая по порядку последовательность операций, имеющая один вход и один выход. Чаще всего линейный участок со- держит последовательность вычислений, состоящих из арифметических опера- ций и операторов присваивания значений переменным. Любая программа предусматривает выполнение вычислений и присваивания зна- чений, поэтому линейные участки встречаются в любой программе. В реальных программах они составляют существенную часть программного кода. Поэтому для линейных участков разработан широкий спектр методов оптимизации кода. Кроме того, характерной особенностью любого линейного участка является по- следовательный порядок выполнения операций, входящих в его состав. Ни одна операция в составе линейного участка программы не может быть пропущена, ни одна операция не может быть выполнена большее число раз, чем соседние с нею операции (иначе Этот фрагмент программы просто не будет линейным участком). Это существенно упрощает задачу оптимизации линейных участков программ. Поскольку все операции линейного участка выполняются последовательно, их можно пронумеровать в порядке их выполнения. Для операций, составляющих линейный участок программы, могут применяться следующие виды оптимизирующих преобразований: □ удаление бесполезных присваиваний; □ исключение избыточных вычислений (лишних операций); □ свертка операций объектного кода; □ перестановка операций; □ арифметические преобразования. Далее рассмотрены два метода оптимизации линейных участков: исключение лишних операций и свертка объектного кода. Свертка объектного кода Свертка объектного кода — это выполнение во время компиляции тех операций исходной программы, для которых значения операндов уже известны. Нет необ- ходимости многократно выполнять эти операции в результирующей программе — вполне достаточно один раз выполнить их при компиляции.
108 Лабораторная работа № 4 • Генерация и оптимизация объектного кода ВНИМАНИЕ------------------------------------------------ Не следует путать оптимизацию по методу свертки объектного кода с рассмотренным в лабора- торной работе № 3 алгоритмом «сдвиг-свертка». Свертка объектного кода и свертка по правилам грамматики при выполнении синтаксического разбора — это принципиально разные операции! Простейший вариант свертки — выполнение в компиляторе операций, операнда- ми которых являются константы. Несколько более сложен процесс определения тех операций, значения которых могут быть известны в результате выполнения других операций. Для этой цели при оптимизации линейных участков програм- мы используется специальный алгоритм свертки объектного кода. Алгоритм свертки для линейного участка программы работает со специальной таблицей Т, которая содержит пары (<переменная>.<константа>) для всех перемен- ных, значения которых уже известны. Кроме того, алгоритм свертки помечает те операции во внутреннем представлении программы, для которых в результате свертки уже не требуется генерация кода. Так как при выполнении алгоритма свертки учитывается взаимосвязь операций, то удобной формой представления для него являются триады, поскольку в других формах представления операций (таких как тетрады или команды ассемблера) требуются дополнительные струк- туры, чтобы отразить связь результатов одних операций с операндами других. Рассмотрим выполнение алгоритма свертки объектного кода для триад. Для по- метки операций, не требующих порождения объектного кода, будем использовать триады специального вида С(К,0). Алгоритм свертки триад последовательно просматривает триады линейного уча- стка и для каждой триады делает следующее: 1. Если операнд есть переменная, которая содержится в таблице Т, то операнд заменяется на соответствующее значение константы. 2. Если операнд есть ссылка на особую триаду типа С(К, 0), то операнд заменяет- ся на значение константы К. 3. Если все операнды триады являются константами, то триада может быть свер- нута. Тогда данная триада выполняется и вместо нее помещается особая триа- да вида С(К,О), где К — константа, являющаяся результатом выполнения свер- нутой триады. (При генерации кода для особой триады объектный код не по- рождается, а потому она в дальнейшем может быть просто исключена.) 4. Если триада является присваиванием типа А:=В, тогда: если В — константа, то Асо значением константы заносится в таблицу Т (если там уже было старое значение для А, то это старое значение исключается); если В — нс константа, то А вообще исключается из таблицы Т, если оно там есть. Рассмотрим пример выполнения алгоритма. Пусть фрагмент исходной программы (записанной на языке типа Pascal) имеет вид: I := 1 + 1: I := 3: J := 6*1 + I:
^ialallaus^ Краткие теоретические сведения 109 Ее внутреннее представление в форме триад будет иметь вид: 1. + (1.1) 2: := (I. Л1) 3: := (I. 3) 4: * (6. I) 5: + Г4. I) 6: := (J. я5) Процесс выполнения алгоритма свертки показан в табл. 4.1. Таблица 4.1. Пример работы алгоритма свертки Триада Шаг 1 Шаг 2 Шаг 3 Шаг 4 Шаг 5 Шаг 6 1 С (2, 0) С (2, 0) С (2, 0) С (2, 0) С (2, 0) С (2, 0) 2 := (I, я1) :=(1. 2) :=(1, 2) := (I. 2) :=(1. 2) :=d. 2) 3 :=«. 3) :=(!. 3) := (I. 3) := (I. 3) :=«, 3) := (I. 3) 4 * (6. I) * (6. I) * (6. I) С (18, 0) С (18, 0) С (18, 0) 5 + Г4, I) + (Л4, I) + (л4, I) + (л4, 1) С (21, 0) С (21, 0) 6 := (J. "5) := (J, *5) := (J. л5) := 0. *5) := (J, я5) := (J. 21) Т ( , ) (I. 2) (I. 3) (1.3) 0. 3) (I, 3)(J. 21) Если исключить особые триады типа С(К. 0) (которые не порождают объектного кода), то в результате выполнения свертки получим следующую последователь- ность триад: 1. := (I. 2) 2: := (I. 3) 3: := (J. 21) Видно, что результирующая последовательность триад может быть подвергнута дальнейшей оптимизации — в ней присутствуют лишние присваивания, но дру- гие методы оптимизации выходят за рамки данной лабораторной работы (с ними можно познакомиться в 11, 2, 7]). Алгоритм свертки объектного кода позволяет исключить из линейного участка программы операции, для которых на этапе компиляции уже известен результат. За счет этого сокращается время выполнения1, а также объем кода результирую- щей программы. Свертка объектного кода, в принципе, может выполняться не только для линей- ных участков программы. Когда операндами являются константы, логика выполне- ния программы значения не имеет — свертка может быть выполнена в любом слу- чае. Если же необходимо учитывать известные значения переменных, то нужно 1 Даже если принять по внимание, что время на выполнение операции будет потрачено компилято- ром при порождении результирующей программы, то все равно мы имеем выигрыш во времени. Во- первых, любая результирующая программа создается (а значит и компилируется окончательно) только один раз, а выполняется многократно; во-вторых, оптимизируемый линейный участок мо- жет входить в состав оператора цикла или вызова функции, и тогда выигрыш очевиден.
110 Лабораторная работа № 4 • Генерация и оптимизация объектного кода принимать во внимание и логику выполнения результирующей программы. По- этому для нелинейных участков программы (ветвлений и циклов) алгоритм бу- дет более сложным, чем последовательный просмотр линейного списка триад. Исключение лишних операций Исключение избыточных вычислений (лишних операций) заключается в нахож- дении и удалении из объектного кода операций, которые повторно обрабатывают одни и те же операнды. Операция линейного участка с порядковым номером i считается лишней опера- цией, если существует идентичная ей операция с порядковым номером j, j < i и ни- какой операнд, обрабатываемый операцией с порядковым номером 1, не изменял- ся никакой другой операцией, имеющей порядковый номер между i и j. Алгоритм исключения лишних операций просматривает операции в порядке их следования. Так же как и алгоритму свертки, алгоритму исключения лишних опе- раций проще всего работать с триадами, потому что они полностью отражают вза- имосвязь операций. Рассмотрим алгоритм исключения лишних операций для триад. Чтобы следить за внутренней зависимостью переменных и триад, алгоритм при- сваивает им некоторые значения, называемые числами зависимости, по следую- щим правилам: □ изначально для каждой переменной ее число зависимости равно 0, так как в на- чале работы программы значение переменной не зависит ни от какой триады; □ после обработки i-й триады, в которой переменной А присваивается некоторое значение, число зависимости A (dep(A)) получает значение i, так как значение А теперь зависит от данной i-й триады; □ при обработке i-й триады ее число зависимости (dep(i)) принимается равным значению 1 + (наксимальное_из_чисел_зависимости_операндов). Таким образом, при использовании чисел зависимости триад и переменных мож- но утверждать, что если i-я триада идентична j-й триаде (j < i), то i-я триада счи- тается лишней в том и только в том случае, когда dep(i) = dep( j). Алгоритм исключения лишних операций использует в своей работе триады осо- бого вида SAME(j.O). Если такая триада встречается в позиции с номером i, то это означает, что в исходной последовательности триад некоторая триада i идентич- на триаде j. Алгоритм исключения лишних операций последовательно просматривает триа- ды линейного участка. Он состоит из следующих шагов, выполняемых для каж- дой триады: 1. Если какой-то операнд триады ссылается на особую триаду вида SAME( j.O), то он заменяется на ссылку на триаду с номером j (*j). 2. Вычисляется число зависимости текущей триады с номером i, исходя из чи- сел зависимости ее операндов.
Краткие теоретические сведения 111 3. Если в просмотренной части списка триад существует идентичная j-я триада, причем j < 1 и dep(i) = dep( j), то текущая триада 1 заменяется на триаду особо- го вида SAME(j.O). 4. Если текущая триада есть присваивание, то вычисляется число зависимости соответствующей переменной. Рассмотрим работу алгоритма на примере: D := D + С*В: А := D + С*В: С := D + С*В; Этому фрагменту программы будет соответствовать следующая последователь- ность триад: 1: * (С. В) 2: + (D. ж1) 3: := (D. ж2) 4: * (С. В) 5: + (D. Л4) 6: := (А. ж5) 7: * (С. В) 8: + (D. Л7) 9: (С. ж8) Видно, что в данном примере некоторые операции вычисляются дважды над од- ними и теми же операндами, а значит, они являются лишними и могут быть ис- ключены. Работа алгоритма исключения лишних операций отражена в табл. 4.2. Таблица 4.2. Пример работы алгоритма исключения лишних операций Обрабатываемая триада i Числа зависимости переменных Числа зависимости триад dep(i) Триады, полученные после выполнения алгоритма А В С D 1)*(С. В) 0 0 0 0 1 1) * (С, В) 2) + (D, Л1) 0 0 0 0 2 2) + (D, я1) 3) :=(D, л2) 0 0 0 3' 3 3) := (D, ~2) 4) * (С, В) 0 0 0 3 1 4) SAME (1,0) 5) + (D, л4) 0 0 0 3 4 5) + (D, Л1) 6) := (А, л5) 6 0 0 3 5 6) := (А, ~5) 7) * (С, В) 6 0 0 3 1 7) SAME (1,0) 8) + (D, л7) 6 0 0 3 4 8) SAME (5, 0) 9) := (С, А8) 6 0 9 3 5 9) := (С, я5) Теперь, если исключить триады особого вида8АМЕ( j.O), то в результате выполне- ния алгоритма получим следующую последовательность триад: 1: * (С. В) 2: + (D. А1) 3: := (D. ж2)
112 Лабораторная работа № 4 • Генерация и оптимизация объектного кода 4: + (D. л1) 5: := (А. л4) 6: := (С. л4) Обратите внимание, что в итоговой последовательности изменилась нумерация триад и номера в ссылках одних триад на другие. Если в компиляторе в качестве ссылок использовать не номера триад, а непосредственно указатели на них, то изменения ссылок в таком варианте не потребуется. Алгоритм исключения лишних операций позволяет избежать повторного выпол- нения одних и тех же операций над одними и теми же операндами. В результате оптимизации по этому алгоритму сокращается и время выполнения, и объем кода результирующей программы. Общий алгоритм генерации и оптимизации объектного кода Теперь рассмотрим общий вариант алгоритма генерации кода, который получает на входе дерево вывода (построенное в результате синтаксического разбора) и соз- дает по нему фрагмент объектного кода результирующей программы. Алгоритм должен выполнить следующую последовательность действий: □ построить последовательность триад на основе дерева вывода; □ выполнить оптимизацию кода методом свертки для линейных участков резуль- тирующей программы; □ выполнить оптимизацию кода методом исключения лишних операций для линейных участков результирующей программы; □ преобразовать последовательность триад в последовательность команд на язы- ке ассемблера (полученная последовательность команд и будет результатом выполнения алгоритма). Алгоритм преобразования триад в команды языка ассемблера — это единствен- ная машинно-зависимая часть общего алгоритма. При преобразовании компиля- тора для работы с другим результирующим объектным кодом потребуется изме- нить только эту часть, при этом все алгоритмы оптимизации и внутреннее пред- ставление программы останутся неизменными. В данной работе алгоритм преобразования триад в команды языка ассемблера предлагается разработать самостоятельно. В тривиальном виде такой алгоритм заменяет каждую триаду на последовательность соответствующих команд, а ре- зультат ее выполнения запоминается во временной переменной с некоторым име- нем (например ТМРт, где 1 — номер триады). Тогда вместо ссылки на эту триаду в другой триаде будет подставлено значение этой переменной. Однако алгоритм может предусматривать и оптимизацию временных переменных'. 1 Детально алгоритм порождения ассемблерного кода на основе последовательности триад рассмот- рен в примере курсовой работы.
^lataHaus,^ Требования к выполнению работы 113 Требования к выполнению работы Порядок выполнения работы Для выполнения лабораторной работы требуется написать программу, которая на основании дерева синтаксического разбора порождает объектный код и выпол- няет затем его оптимизацию методом свертки объектного кода и методом исклю- чения лишних операций. В качестве исходного дерева синтаксического разбора рекомендуется взять дерево, которое порождает программа, построенная по зада- нию лабораторной работы № 3. Программу рекомендуется построить из трех основных частей: первая часть — порождение дерева синтаксического разбора (по результатам лабораторной рабо- ты № 3), вторая часть — реализация алгоритма порождения объектного кода по дереву разбора и третья часть — оптимизация порожденного объектного кода (если в результирующей программе присутствуют линейные участки кода). Результа- том работы должна быть построенная на основе заданного предложения грамма- тики программа на объектном языке или построенная последовательность триад (по согласованию с преподавателем выбирается форма представления конечного результата). В качестве объектного языка предлагается взять язык ассемблера для про- цессоров типа Intel 80x86 в реальном режиме (возможен выбор другого объект- ного языка по согласованию с преподавателем). Все встречающиеся в исход- ной программе идентификаторы считать простыми скалярными переменны- ми, не требующими выполнения преобразования типов. Ограничения на длину идентификаторов и констант соответствуют требованиям лаборатор- ной работы № 3. 1. Получить вариант задания у преподавателя. 2. Изучить алгоритм генерации объектного кода по дереву синтаксического раз- бора. 3. Разработать схемы СУ-перевода для операций исходного языка в соответствии с заданной грамматикой. 4. Выполнить генерацию последовательности триад вручную для выбранного простейшего примера. Проверить корректность результата. 5. Изучить и реализовать (если требуется) для заданного входного языка алго: ритмы оптимизации результирующего кода методом свертки и методом исклю- чения лишних операций. 6. Разработать алгоритм преобразования последовательности триад в заданный объектный код (по согласованию с преподавателем). 7. Подготовить и защитить отчет. 8. Написать и отладить программу на ЭВМ. 9. Сдать работающую программу преподавателю.
114 Лабораторная работа № 4 • Генерация и оптимизация объектного кода Требования к оформлению отчета Отчет должен содержать следующие разделы: □ Задание по лабораторной работе. □ Краткое изложение цели работы. □ Запись заданной грамматики входного языка в форме Бэкуса—Наура. □ Описание схем СУ-перевода для операций исходного языка в соответствии с заданной грамматикой. □ Пример генерации и оптимизации последовательности триад на основе про- стейшей исходной программы. □ Текст программы (оформляется после выполнения программы на ЭВМ). Основные контрольные вопросы □ Что такое транслятор, компилятор и интерпретатор? Расскажите об общей структуре компилятора. □ Как строится дерево вывода (синтаксического разбора)? Какие исходные дан- ные необходимы для его построения? □ Какую роль выполняет генерация объектного кода? Какие данные необходи- мы компилятору для генерации объектного кода? Какие действия выполняет компилятор перед генерацией? □ Объясните, почему генерация объектного кода выполняется компилятором по отдельным синтаксическим конструкциям, а не для всей исходной программы в целом. □ Расскажите, что такое синтаксически управляемый перевод. □ Объясните работу алгоритма генерации последовательности триад по дереву синтаксического разбора на своем примере. □ За счет чего обеспечивается возможность генерации кода на разных объект- ных языках по одному и тому же дереву? □ Дайте определение понятию оптимизации программы. Для чего используется оптимизация? Каким условиям должна удовлетворять оптимизация? □ Объясните, почему генерацию программы приходится проводить в два этапа: генерация и оптимизация. □ Какие существуют методы оптимизации объектного кода? □ Что такое триады и для чего они используются? Какие еще существуют мето- ды для представления объектных команд? □ Объясните работу алгоритма свертки. Приведите пример выполнения сверт- ки объектного кода. □ Что такое лишняя операция? Что такое число зависимости?
Пример выполнения работы 115 □ Объясните работу алгоритма исключения лишних операций. Приведите при- мер исключения лишних операций. Варианты заданий Варианты заданий соответствуют вариантам заданий для лабораторной работы № 3. Для выполнения работы рекомендуется использовать результаты, получен- ные в ходе выполнения лабораторных работ № 2 и 3. Пример выполнения работы Задание для примера В качестве задания для примера возьмем язык, заданный КС-грамматикой G({if,then,else,а,:=,or,xor,and,(,),;},{5,F,E,D,С},P,S) с правилами P: S-+F; F—> if E then Telse F| if E then F| a := E T » if E then Telse T\a := E E-+ E orD | E xor D | D DD and C | C C->a\(E) Жирным шрифтом в грамматике и в правилах выделены терминальные символы. Этот язык уже был использован для иллюстрации выполнения лабораторных ра- бот № 2 и № 3. Результатом примера выполнения лабораторной работы № 4 будет генератор спис- ка триад. Преобразование списка триад в ассемблерный код рассмотрено далее в примере выполнения курсовой работы (см. главу «Курсовая работа»). Построение схем СУ-перевода Все операции, которые могут присутствовать во входной программе на языке, за- данном грамматикой G, по смыслу (семантике) можно разделить на следующие группы: □ логические операции (or, xor и and); □ оператор присваивания; □ полный условный оператор (if... then ... else ...) и неполный условный опе- ратор (if... then...); □ операции, не несущие смысловой нагрузки, а служащие только для создания синтаксических конструкций исходной программы (в данном языке таких опе- раций две: круглые скобки и точка с запятой). Рассмотрим схемы СУ-перевода для всех перечисленных групп операций.
116 Лабораторная работа № 4 • Генерация и оптимизация объектного кода СУ-перевод для линейных операций Линейной операцией будем называть такую операцию, для которой порождается код, представляющий собой линейный участок результирующей программы. На- пример, рассмотренные ранее бинарные арифметические операции (см. раздел «Краткие теоретические сведения») являются линейными. В заданном входном языке логические операции выполняются над целыми деся- тичными числами как побитовые операции, то есть они также являются бинар- ными линейными операциями. Поэтому для них могут быть использованы те же самые схемы СУ-перевода, что были рассмотрены ранее. ПРИМЕЧАНИЕ-------------------------------------------------------- На самом деле возможен другой вариант вычисления логических операций в том случае, когда они являются операциями булевой логики и их операндами могут быть только значения «Исти- на» (1) и «Ложь» (0). Здесь этот вариант не рассматривается. Более подробно о нем сказано в разделе «Курсовая работа», когда строятся схемы СУ-перевода для логических операций, а также можно обратиться к литературе [2]. СУ-перевод для оператора присваивания Оператор присваивания также является бинарной логической операцией, поэто- му для него может быть использована соответствующая схема СУ-перевода. Отличие оператора присваивания от прочих бинарных линейных операций за- ключается в том, что первым операндом у него всегда должна быть переменная. Поэтому функция, строящая код для оператора присваивания, должна проверять тип первого операнда. Эта проверка представляет собой реализацию простейше- го семантического анализа и в данном случае необходима, так как присваивание значений константам не отслеживается на этапе синтаксического анализа (об этом было сказано в лабораторной работе № 3). СУ-перевод для условных операторов Для условных операторов генерация кода должна выполняться в следующем по- рядке: 1. Порождается блок кода№ 1, вычисляющий! логическое выражение, находя- щееся между лексемами if (первая нижележащая вершина) и then (третья ни- жележащая вершина), — для этого должна быть рекурсивно вызвана функция порождения кода для второй нижележащей вершины. 2. Порождается команда условного перехода, которая передает управление в за- висимости от результата вычисления логического выражения: в начало блока кода № 2, если логическое выражение имеет ненулевое зна- чение; в начало блока кода № 3 (для полного условного оператора) или в конец оператора (для неполного условного оператора), если логическое выраже- ние имеет нулевое значение. 3. Порождается блок кода № 2, соответствующий операциям после лексемы then (третья нижележащая вершина), — для этого должна быть рекурсивно вызва- на функция порождения кода для четвертой нижележащей вершины.
Пример выполнения работы 117 4. Для полного условного оператора порождается команда безусловного перехо- да в конец оператора. 5. Для полного условного оператора порождается блок кода № 3, соответствую- щий операциям после лексемы else (пятая нижележащая вершина), — для этого должна быть рекурсивно вызвана функция порождения кода для шестой ни- жележащей вершины. Схемы СУ-перевода для полного и неполного условных операторов представле- ны на рис. 4.1. Полный условный оператор Неполный условный оператор Рис. 4.1. Схемы СУ-перевода для условных операторов Для того чтобы реализовать эти схемы, необходимы два типа триад: триада услов- ного перехода и триада безусловного перехода. Эти два типа триад реализуются следующим образом: □ 1Г(<операнд1>,<операнд2>) — триада условного перехода; □ ЭМР(1,<операнд2>) — триада безусловного перехода. У триады IF первый операнд может быть переменной, константой или ссылкой на другую триаду, второй операнд — всегда ссылка на другую триаду. Триада IF пе- редает управление на триаду, указанную вторым операндом, если первый операнд равен нулю, иначе управление передается на следующую триаду. У триады JMP первый операнд не имеет значения (для определенности он всегда будет равен 1), второй операнд — всегда ссылка на другую триаду. Триада JMP все- гда передает управление на триаду, указанную вторым операндом. СУ-перевод для семантически ненагруженных конструкций Операции, которые не несут никакой смысловой нагрузки, не требуют построе- ния результирующего кода. Для них не требуется строить схемы СУ-перевода.
118 Лабораторная работа № 4 • Генерация и оптимизация объектного кода Тем не менее функция генерации списка триад должна обрабатывать и эти опе- рации. Они должны обрабатываться следующим образом: □ для вершины, у которой первая нижележащая вершина — открывающая скоб- ка, вторая нижележащая вершина — узел дерева (не концевая вершина) и третья нижележащая вершина — закрывающая скобка, должна рекурсивно вызываться функция порождения кода для второй нижележащей вершины; □ для вершины, у которой первая нижележащая вершина — узел дерева (не кон- цевая вершина) и вторая нижележащая вершина — точка с запятой, должна рекурсивно вызываться функция порождения кода для первой нижележащей вершины. Пример генерации списка триад Возьмем в качестве примера входную цепочку: if a and b or a and b and 345 then а := 5 or 4 and 7; В результате лексического и синтаксического разбора этой входной цепочки бу- дет построено дерево синтаксического разбора, приведенное на рис. 4.2. Этому дереву будет соответствовать следующая последовательность триад: 1: and (а. Ь) 2: and (а. Ь) 3: and (*2. 345) 4: ог Г1. *3) 5: if Г4. *9) 6: and (4. 7) 7: or (5. ж6) 8: := (а. ж7) 9: ... В этой последовательности два линейных участка: от триады 1 до триады 5 и от триады 6 до триады 9. После оптимизации методом свертки объектного кода получим последователь- ность триад: 1: and (а. Ь) 2: and (а. Ь) 3: and (*2. 345) 4: ог Г1, *3) 5: if Г4. *9) 6: С (4. 0) 7: С (5. 0) 8: := (а. 5) 9: ... Если удалить триады типа С, то эта последовательность примет следующий вид: 1: and (а. Ь) 2: and (а. Ь)
^alattausiii!. Рис. 4.2. Дерево синтаксического разбора цепочки «if a and b or a and b and 345 then а := 5 or 4 and 7;» 3: and (*2. 345) 4: or (Al. ж3) 5: if Г4. ж7) 6: := (a. 5) 7: ... После оптимизации методом исключения лишних операций получим последова- тельность триад: 1: and (а. Ь) 2: same (*1. 0) 3: and Г1. 345) 4: or (*1. ж3) 5: if Г4. *7) 6: := (а. 5) 7: ... Если удалить триады типа same, то эта последовательность примет следующий вид: 1: and (а. Ь) 2: and (Л1. 345) 3: or Г1. ж2)
120 Лабораторная работа № 4 • Генерация и оптимизация объектного кода 4: If (А3. А6) 5: := (а. 5) 6: ... После применения оптимизации получаем последовательность из пяти триад. Это на 37,5% меньше, чем в исходной без применения оптимизации последовательно- сти, состоявшей из восьми триад. Следовательно, объем результирующего кода и время его выполнения в данном случае сократятся примерно на 37,5% (слово «при- мерно» указано здесь потому, что разные триады могут порождать различное коли- чество команд в результирующем коде, а потому соотношения между количеством триад и между количеством команд объектного кода могут немного различаться). Можно еще обратить внимание на то, что алгоритм оптимизации методом исклю- чения лишних операций не учитывает особенности выполнения логических и арифметических операций. Методами булевой алгебры последовательность опе- раций «а and b or a and b and 345» можно преобразовать в «а and /ъ> точно так же, как последовательность операций «а-b + с-6-345» — в «« />346», что было бы эф- фективней, чем варианты, которые строит алгоритм оптимизации методом исклю- чения лишних операций. Но для таких преобразований нужны алгоритмы, ори- ентированные на особенности выполнения логических и арифметических опера- ций [1,2, 7]. Реализация генератора списка триад Разбиение на модули Так же, как и для лабораторных работ № 2 и 3, модули, реализующие генератор списка триад, в лабораторной работе № 4 разделены на две группы: □ модули, программный код которых не зависит от входного языка; □ модули, программный код которых зависит от входного языка. В первую группу входят модули: □ Triads — описывает структуры данных для представления триад; □ TrdOpt — реализует два алгоритма оптимизации: методом свертки объектного кода и методом исключения лишних операций; □ FormLab4 — описывает интерфейс с пользователем. Во вторую группу входят модули: □ TrdType — описывает допустимые типы триад и их текстовое представление; □ TrdMake — строит список триад па основе дерева синтаксического разбора; □ TrdCal с — обеспечивает вычисление значений для триад разных типов при свер- тке объектного кода. Такое разбиение на модули позволяет использовать тс же самые структуры дан- ных для организации нового генератора списка триад при изменении входного языка.
Пример выполнения работы 121 Кроме этих модулей для реализации лабораторной работы № 4 используются сле- дующие программные модули: □ TblElem и FncTree — позволяют работать с комбинированной таблицей иденти- фикаторов (созданы при выполнении лабораторной работы № 1); □ LexType, LexElem, и LexAuto — обеспечивают работу лексического распознавате- ля (созданы при выполнении лабораторной работы К? 2); □ SyntRule и SyntSymb — обеспечивают работу синтаксического распознавателя (созданы при выполнении лабораторной работы № 3). Кратко опишем содержание программных модулей, используемых для организа- ции генератора списка триад. Модуль описания допустимых типов триад Модуль TrdType содержит структуры данных, которые описывают допустимые типы триад. Он содержит следующие важные типы данных и переменные: □ TTriadType — перечисление всех возможных типов триад; □ TriadStr — массив строковых обозначений для всех типов триад; □ TriadLineSet — множество тех триад, которые являются линейными операция- ми (оно важно для оптимизации и для порождения кода). Модуль описания структур данных для триад Модуль Triads содержит структуры данных, которые описывают триады и список триад. Эти структуры зависят от реализации компилятора, но не зависят от вход- ного языка. Он содержит следующие важные структуры данных: □ TOperand — описывает операнд триады; □ TTriad — описывает триаду и все связанные с нею данные; □ TTriadList — описывает список триад. Структура TOperand описывает операнд триады. Она соде ржпт следующие данные: □ ОрТуре — тип операнда, который может принимать три значения: OPCONST — константа; OP VAR — переменная (идентификатор); OPLINK — ссылка на другую триаду; □ и дополнительную информацию по операнду: ConstVal — значение (для константы); VarLInk — ссылка на таблицу идентификаторов (для переменной); TriadNum — номер триады (для ссылки на триаду). Один из вопросов, который необходимо было решить при реализации операн- дов триад, состоял в следующем: что использовать для описания ссылки на
122 Лабораторная работа № 4 • Генерация и оптимизация объектного кода триаду — непосредственно ссылку на тип данных (указатель) или номер триады в списке? Оба варианта имеют свои преимущества и недостатки: □ при использовании указателя легче осуществлять доступ к триаде (не надо выбирать ее из списка), не надо менять указатели при перемещении триад в списке, но при удалении любой триады из списка нужно корректно менять все указатели на эту триаду, какие только есть; □ при использовании номера триады легче порождать список триад по дереву разбора, но при любом перемещении и удалении триад из списка нужно пере- считывать все номера. Какой вариант выбрать, решает разработчик компилятора. В данном случае автор выбрал второй вариант (номер триады, а не указатель на нее), поскольку наглядная иллюстрация алгоритмов оптимизации требует удаления триад, а перестановка указателей при каждом удалении намного сложнее, чем изменение номеров (этот недостаток оказался решающим). Но поскольку в реальном компиляторе не нужно иллюстрировать работу алгоритмов оптимизации выводом списка триад (достаточно просто не порождать код для триад с типами С и same), в этом случае указатели, по мнению автора, были бы предпочтительнее. Структура TTriad описывает триаду и все связанные с пей данные. Она содержит следующие поля данных: □ TriadType — тип триады (один из перечисленных в типе TTriadType в модуле TrdType); □ Operands — массив операндов триады (из двух операндов типа TOperand); □ Info — дополнительная информация о триаде для алгоритмов оптимизации; □ I sLi nked — флаг, сигнализирующий о том, что на триаду имеется ссылка из дру- гой триады, обеспечивающей передачу управления (типа IF или JMP). Для хранения дополнительной информации можно было использовать один из двух подходов: хранить ее непосредственно в самой триаде или хранить внутри триады только ссылку (указатель), а саму дополнительную информацию разме- щать во внешней структуре данных. Этот вопрос уже возникал при выборе метода хранения информации при органи- зации таблиц идентификаторов в лабораторной работе № 1. Тогда было отдано предпочтение второму варианту, поскольку характер и размер хранимой инфор- мации для каждого идентификатора был неизвестен. В данном случае известно, что для каждой триады потребуется хранить информа- цию, обрабатываемую двумя алгоритмами оптимизации — алгоритмом свертки объектного кода и алгоритмом исключения лишних операций. Оба эти алгоритма работают со значениями, которые могут принимать триады — для заданного вход- ного языка это целые десятичные числа. Для их хранения достаточно одного це- лочисленного поля (два алгоритма никогда не выполняются одновременно, а по- тому могут использовать одно и то же поле данных). Поэтому тут выбран первый
Пример выполнения работы 123 вариант и хранимая информация включена непосредственно в структуру данных триады в виде поля Info. Флаг наличия ссылки важен для определения границ линейных участков програм- мы при оптимизации: если на какую-то триаду есть ссылка из триад типа IF или JMP, значит, на нее может быть передано управление. Такая триада является возможной точкой входа участка программы, а потому — границей линейного участка. Кроме перечисленных данных структура TTriad содержит следующие процедуры и функции: □ конструктор Create для создания триады; □ функцию проверки совпадения двух триад IsEqual; □ функцию MakeStri ng, формирующую строковое представление триады для ото- бражения триад на экране; □ функции, процедуры и свойства для доступа к данным триады. Нужно обратить внимание, что функция проверки совпадения двух триад IsEqual считает триады эквивалентными, если они имеют один тип и одинаковые операн- ды. Эта функция нужна для выполнения алгоритма исключения лишних опера- ций — она проверяет первое условие того, что операция является лишней, то есть имеется ли совпадающая с ней операция. Второе условие (что ни один из операн- дов не изменялся между двумя операциями) проверяется с помощью чисел зави- симости. Структура данных TTri adLi st описывает список триад и методы работы с ним. Как и некоторые списки, рассмотренные ранее (в лабораторных работах № 2 и 3), она построена на основе динамического массива типа TList из библиотеки VCL сис- темы программирования Delphi 5. В этой структуре нет никаких данных (исполь- зуются только данные, унаследованные от класса TList), но с ней связаны мето- ды, необходимые для работы со списком триад: □ функция очистки списка триад (Clear) и деструктор для освобождения памя- ти при удалении списка триад (Destroy); □ функция записи списка триад в текстовом представлении в список строк для отображения списка триад на экране (WriteToList); □ функция удаления триады из списка (Del Tri ad); □ функция GetTriad и свойство Triads для доступа к триадам в списке по их по- рядковому номеру. Следует отметить, что функция записи списка триад в список строк (WriteToList) последовательно вызывает функцию MakeStri ng для записи в список строк каж- дой триады из списка триад. Функция удаления триады из списка (Del Тri ad) осво- бождает память, занятую удаляемой триадой, а кроме того, следит за тем, чтобы при удалении триады флаг метки (IsLinked) от удаляемой триады был корректно переставлен на следующую по списку триаду. Кроме трех перечисленных структур данных в модуле Tri ads описана также функ- ция Del Тri adTypes, которая выполняет удаление из списка триад всех триад задан-
124 Лабораторная работа № 4 • Генерация и оптимизация объектного кода ного типа. Эта функция необходима только для наглядной иллюстрации работы алгоритмов оптимизации. Для этого надо удалять из списка триад триады с типа- ми С и same, которые не порождают результирующего кода. Удаление триад из списка можно выполнить в виде двух вложенных циклов: □ первый обеспечивает просмотр всего списка триад; □ второй обеспечивает изменение номеров всех ссылок и всех последующих три- ад в списке при удалении какой-либо триады. Тогда среднее количество просмотров списка триад можно оценить как W + K-N-N, где N— количество триад в списке, К — средний процент удаляемых триад. При хорошей оптимизации, когда К велико, время работы функции удаления триад из списка будет квадратично зависеть от количества триад. При увеличении объема результирующей программы (при росте N) это время будет существенно возрас- тать. Поэтому функция удаления триад из списка реализована другим путем. Она вы- полняет два просмотра списка триад: 1. На первом просмотре подсчитывается количество удаляемых триад и для каж- дой триады запоминается, на какую величину изменится ее номер при удале- нии. 2. На втором просмотре удаляются те триады, которые должны быть удалены, а для остальных номера и ссылки меняются на величину, запомненную при первом просмотре. При такой реализации функции количество просмотров списка триад всегда бу- дет равно 2N и обеспечит линейную зависимость времени выполнения функции от количества триад. Правда, в таком случае функция потребует еще дополни- тельно Nячеек памяти для хранения изменений индексов каждой триады, но это оправдано существенным выигрышем во времени ее выполнения. Модуль построения списка триад по дереву синтаксического разбора Модуль TrdMake содержит функцию, которая строит список триад на основе дере- ва синтаксического разбора. Эта функция работает с типами триад, описанными в модуле ТгсГГуре, и со структурами данных, описанными в модуле Triads. Дерево синтаксического разбора описано структурами данных из модуля SyntSymb, кото- рый был создан при выполнении лабораторной работы № 3. Функция построе- ния списка триад на основе синтаксического дерева зависит от входного языка, а потому вынесена в отдельный модуль. Модуль содержит одну функцию, доступную извне, — MakeTriadList. Входными данными этой функции являются: □ symbTop — ссылка на корень синтаксического дерева, по которому строится спи- сок триад; □ HstTriad — список, в который должны быть записаны построенные триады.
Пример выполнения работы 125 Результатом выполнения функции является пустая ссылка, если при построении списка триад не было обнаружено семантических ошибок, или же ссылка на лек- сему, возле которой обнаружена семантическая ошибка, если такая ошибка обна- ружена. Генератор списка триад обнаруживает один вид семантических ошибок — присваивание значения константе. Функция MakeTriadList выполняет построение списка триад, добавляет вконец списка триад завершающую триаду типа NOP (No Operation — Нет операции), что- бы корректно обрабатывать ссылки на конец списка триад, а также обеспечивает расстановку флагов IsLinked для всех триад в списке. Функция MakeTriadList построена на основе внутренней функции модуля TrdMake — MakeTriadListNOP, которая и выполняет главные действия по порождению списка триад. Эта функция обрабатывает те же входные данные 11 имеет такой же резуль- тат выполнения, что и функция MakeTriadList. Функция MakeTriadListNOP реализует схемы СУ-перевода, которые были рассмот- рены выше. Выбор схемы СУ-перевода происходит по номеру правила остовной грамматики G’, взятого из текущего нетерминального символа дерева: □ для правил 2 и 5 — схема полного условного оператора; □ для правила 3 — схема неполного условного оператора; □ для правил 4 и 6 — схема оператора присваивания; □ для правил 7, 8 и 10 — схема для бинарных линейных операций; □ для правила 13 — схема для скобок; □ в остальных случаях — схема для точки с запятой. Функция MakeTriadListNOP содержит две вспомогательные функции: □ функцию MakeOperand для порождения кода, связанного с дочерним узлом де- рева (одним из операндов); □ функцию MakeOperation, реализующую схему СУ-перевода для бинарных ли- нейных операций в зависимости от типа операции. Для построения кода для нижележащих нетерминальных символов по дереву функция MakeTriadListNOP рекурсивно вызывает сама себя. Этот вызов реализован в функции MakeOperand, если нижележащий узел является нетерминальным сим- волом, а также напрямую для узлов, связанных со скобками и с точкой с запятой (как было рассмотрено ранее при построении схем СУ-перевода). Модуль вычисления значений триад на этапе компиляции Модуль TrdCalc содержит функцию, которая вызывается, когда необходимо вы- числить значение триады на этапе компиляции. Эта функция нужна для алгорит- ма оптимизации методом свертки объектного кода. Она зависит от типов триад, которые зависят от входного языка, поэтому вынесена в отдельный модуль. Модуль содержит одну-единственную функцию,Са1сТнас1, которая предельно про- ста и в комментариях не нуждается.
126 Лабораторная работа № 4 • Генерация и оптимизация объектного кода Модуль, реализующий алгоритмы оптимизации Модуль TrdOpt реализует два алгоритма оптимизации списка триад: □ методом свертки объектного кода; □ методом исключения лишних операций. Алгоритмы, реализованные в модуле TrdOpt, в общем случае не зависят от вход- ного языка, однако они обрабатывают триады типа «присваивание» (в данной ре- ализации — TRDASSIGN). Кроме того, границы линейных участков, на которых ра- ботают эти алгоритмы, зависят от триад условного и безусловного перехода (в данной реализации — TRDIF и TRDJMP). Сами алгоритмы требуют для себя три- ад специального типа, которые в данном случае реализованы как TRD C и TRD SAME. В итоге реализация алгоритмов оптимизации зависит от следующих типов триад: □ триад присваивания; □ триад условного и безусловного перехода; □ триад специальных типов. В общем случае эти типы триад и их реализация зависят от входного языка (кро- ме триад специальных типов, которые разработчик компилятора может реализо- вать по своему усмотрению). Но поскольку сложно представить себе язык про- граммирования, в котором не было бы операций присваивания, условных и без- условных переходов, можно считать, что в такой реализации модуль TrdOpt от входного языка нс зависит. Функция вычисления значений триад при свертке объектного кода, которая име- ет явную зависимость от входного языка, вынесена в отдельный модуль (модуль TrdCalc, функция CalcTriad). Кроме функций, реализующих алгоритмы оптимизации, модуль TrdOpt содержит две структуры данных: □ TConstlnfo — для хранения информации о значениях переменных; □ TDepInfo — для хранения информации о числах зависимости переменных. Обе эти структуры построены на основе структуры TAddVarlnfo, описанной в мо- дуле TblElem (этот модуль был создан при выполнении лабораторной работы № 1), и предназначены для хранения информации, связанной с переменной в таб- лице идентификаторов. Структура TConstlnfo хранит информацию о значении переменной, если оно из- вестно. Она используется в алгоритме оптимизации методом свертки объектного кода. Структура TDepInfo хранит информацию о числе зависимости переменной. Она используется в алгоритме оптимизации методом исключения лишних операций. Каждая из этих структур имеет одно поле, которое и предназначено для хранения информации. Для доступа к этому полю используются виртуальные функции и связанные с ними свойства, которые переопределяют функции и свойства типа данных TAddVarlnfo.
'yalatiaus^k Пример выполнения работы 127 Эти структуры данных создаются по мере выполнения соответствующих алго- ритмов и уничтожаются после завершения их выполнения. Теперь можно сравнить два подхода к хранению дополнительной информации: 1. Хранение информации внутри структур данных (реализовано для триад). 2. Хранение внутри структур данных только ссылок (указателей), а самой ин- формации — во внешних структурах. Первый подход имеет следующие преимущества: □ доступ к хранимой информации осуществлять проще и быстрее; □ нет необходимости работать с динамической памятью, выделять и освобождать ее по мере надобности. В то же время первый подход имеет ряд недостатков: □ при хранении разнородной информации оперативная память расходуется не- эффективно, будут появляться неиспользуемые поля данных на разных ста- диях компиляции; □ обеспечивается меньшая гибкость в обработке информации. Второй подход имеет следующие преимущества: □ можно хранить разнородную информацию в зависим ости от потребностей на каждой стадии компиляции; □ оперативная память расходуется только на хранение необходимой информа- ции и только тогда, когда она действительно используется; □ обеспечивается более гибкая обработка информации (например, легко реали-‘ зуется понятие «отсутствие данных» в алгоритме оптимизации методом сверт- ки объектного кода через пустую ссылку nil). Но и он имеет ряд недостатков: □ использование ссылок увеличивает время доступа к хранимой информации, что может быть важно при обработке компилятором больших объемов данных; □ использование ссылок требует работы с динамической памятью, выделения и освобождения памяти по мере использования информации, что расходует время и ресурсы ОС. Какой подход выбрать в каждом конкретном случае, решает разработчик компи- лятора, принимая во внимание их достоинства и недостатки. Здесь проиллюст- рирована реализация обоих подходов: первого — для идентификаторов (перемен- ных) в лабораторных работах № 1 и 4, второго — для триад в лабораторной рабо- те № 4. Почему были выбраны именно эти подходы, было описано ранее и для переменных, и для триад. Алгоритмы оптимизации реализованы в модуле TrdOpt в виде двух процедур: □ OptimizeConst — для оптимизации методом свертки объектного кода; □ OptimizeSame — для оптимизации методом исключения лишних операций.
128 Лабораторная работа № 4 • Генерация и оптимизация объектного кода Обе процедуры принимают на вход один параметр — список триад. Все необходи- мые операции выполняются над этим списком, поэтому результатом их работы будет тот же самый список, в котором некоторые триады изменены, а другие за- менены на триады специального вида: □ С (TRD C) — при оптимизации методом свертки объектного кода; □ Same (TRD SAME) — при оптимизации методом исключения лишних операций. Триады специального вида можно удалить из общего списка триад с помощью функции удаления триад заданного типа (DelTriadTypes), которая была описана в модуле Triads. В принципе, нет необходимости выполнять это, так как на по- рождаемый объектный код эта операция никак не влияет — триады специального вида не порождают никакого кода, но для иллюстрации работы алгоритмов опти- мизации такая операция полезна. Процедуры OptimizeConst и OptimizeSame реализуют алгоритмы оптимизации, ко- торые были описаны в разделе «Краткие теоретические сведения», поэтому в до- полнительных пояснениях не нуждаются. Можно отметить только, что для хранения информации, связанной с переменны- ми (значения переменных и числа зависимости переменных), эти процедуры ис- пользуют непосредственно таблицу идентификаторов. И в этом случае проявля- ются преимущества того, что в триадах в качестве ссылки на переменную исполь- зуется именно ссылка на таблицу идентификаторов, а не на имя переменной. Эффективность прямого обращения в таблицу за требуемым значением намного выше, чем поиск переменной по ее имени. Это справедливо для любых операций, выполняемых компилятором на этапах подготовки к генерации кода, генерации кода и оптимизации. Текст программы генератора списка триад Кроме перечисленных модулей необходим еще модуль, обеспечивающий интер- фейс с пользователем. Этот модуль (FormLab4) реализует графическое окно TLab4Form на основе класса TForm библиотеки VCL и включает в себя две состав- ляющие: □ файл программного кода (файл FormLab4. pas); □ файл описания ресурсов пользовательского интерфейса (файл FormLab4.dfm). Модуль FormLab4 построен на основе модуля FormLab3, который использовался для реализации интерфейса с пользователем в лабораторной работе № 3. Он содер- жит все данные, управляющие и интерфейсные элементы, которые были исполь- зованы в лабораторных работах № 2 и 3. Такой подход оправдан, поскольку пер- вым этапом лабораторной работы № 4 является лексический анализ, который выполняется модулями, созданными для лабораторной работы № 2, а вторым эта- пом — синтаксический анализ, который выполняется модулями, созданными для лабораторной работы № 3. Кроме данных, используемых для выполнения лексического и синтаксического анализа так, как это было описано в лабораторных работах № 2 и 3, модуль содер-
^alaHaus,^. Пример выполнения работы 129 жит поле listTriad, которое представляет собой список триад. Этот список ини- циализируется при создании интерфейсной формы и уничтожается при ее зак- рытии. Он также очищается всякий раз, когда запускаются процедуры лексичес- кого и синтаксического анализа. Кроме органов управления, использованных в лабораторной работе № 3, интер- фейсная форма, описанная в модуле FormLab4, содержит органы управления для генератора списка триад лабораторной работы № 4: □ в многостраничной вкладке (PageControll) появилась новая закладка (Sheet - Triad) под названием «Триады»; □ на закладке SheetTriad расположены интерфейсные элементы для вывода и просмотра списков триад (группа с заголовком и список строк для отобра- жения каждого списка триад): GroupTriadAll и ListTriadAl 1 — для отображения полного списка триад, по- строенного до применения алгоритмов оптимизации; GroupTriadConst и ListTriadConst — для отображения списка триад, постро- енного после оптимизации методом свертки объектного кода; GroupTriadSame и ListTri adSame — для отображения списка триад, построен- ного после оптимизации методом исключения лишних операций. □ па той же закладке SheetTri ad расположены два сплиттера для управления раз- мерами списков триад; □ на первой закладке SheetFile («Исходный файл») появились два дополнитель- ных органа управления — флажки с двумя состояниями («пусто» или «отме- чено»): CheckDelC — при установке этого флажка триады типа С удаляются из спис- ка триад после выполнения оптимизации методом свертки объектного кода; CheckDel Same — при установке этого флажка триады типа same удаляются из списка триад после выполнения оптимизации методом исключения лиш- них операций. Внешний вид новой закладки интерфейсной формы TLab4Form приведен на рис. 4.3. Чтение содержимого входного файла организовано точно так же, как в лабора- торной работе № 2. После чтения файла выполняется лексический анализ, как это было описано в ла- бораторной работе № 2, а затем, при успешном выполнении лексического анали- за, синтаксический анализ, как это было описано в лабораторной работе № 3. Если синтаксический анализ выполнен успешно, полученная в результате его выполнения переменная symbRes указывает на корень построенного синтаксиче- ского дерева. Тогда, после того как синтаксическое дерево отобразится на экране с помощью функции MakeTree, вызывается функция построения списка триад по синтаксическому дереву MakeTriadList (из модуля TrdMake). Список триад запоми- нается в список listTriad, а результат выполнения функции — во временную пе- ременную 1 ехТтр. 5 Зак 68
130 Лабораторная работа № 4 • Генерация и оптимизация объектного кода ЗВР Лабораторная работа №4 Исходный Файл | Таблща лексем | Синтаксис Трижды | > Облции сгаисок После свёртки ? Без лишних операций 1 2: 3: 4: 5: 6: 7 8: 9: 10: 11 12 13: 14: 15: 16. 17: 18: 19 20 21 22: 23 or (а, Ь) or(л1. с) or (а, Ь) and (6.7) or ГЗ. ~4) and (л2. л5) if Гб.л14) or (а, Ь) or(3.4) or (л9,1) огГ8. ~10) :=(а.л11) imp (1. л23) or (а. Ь) or (~14, с) or (л15. 4) if П6. л23) or (а, Ь) or(а, Ь) or Г19. с) and (л18, л20) :=(Ь. л21) пор (0,0) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11. 12. 13 14 15: 16: 17: 18: 19 20 ог (а, Ь) or (л1, с) or (а. Ь) от (л3,6) and (л2. л4) if Г5, л11) or(а. Ь) огГ7.7) = (а.~8) |тр(1.л20) or (а. Ь) or Г11.с) or (л12. 4) if Г13. л20) or(а. Ь) ог(а. Ь) or(л16, с) andri5. л17) :=(Ь. л18) пор (0.0) 1 2: 3: 4: 5: 6 7 8: 9 10 11 12 13 14 15 or (а, Ь) or(л1. с) or П,6) and С2. л3) if (л4. л9) or Г 1.7) :=(а.%) imp (1. л15) or(а. Ь) or (л9. с) or (л10,4) if (л11.л15) and ("9. "10) :=(Ь.л13) пор (0.0) —J Oj Завершить работу с программой Рис. 4.3. Внешний вид четвертой закладки интерфейсной формы для лабораторной работы № 4 Если переменная lexTmp после построения списка триад содержит непустую ссылку на лексему, это значит, что исходная программа содержит семантическую ошиб- ку. Лексема, на которую указывает lexTmp, определяет место, где обнаружена ошиб- ка. В этом случае список строк позиционируется на место ошибки и пользовате- лю выдается соответствующее сообщение. Иначе, если переменная lexTmp после построения списка триад содержит пустую ссылку (nil), это значит, что построение списка триад выполнено без ошибок, и список 11 stTriad содержит все построенные триады в порядке их следования. Список триад отображается на экране в списке строк ListTriadAl 1, после чего выполняется оптимизация методом свертки объектного кода — вызывается про- цедура OptimizeConst. Если установлен флажок CheckDelC, то после оптимизации методом свертки объектного кода из списка триад удаляются триады типа С (вы- зывается функция DelTriadTypes с параметром TRD C0NST), после чего список триад отображается в списке строк Li stTriadConst. Затем выполняется оптимизация ме- тодом исключения лишних операций — вызывается процедура Opti mi zeSame. Если установлен флажок CheckDelSame, то после оптимизации методом исключения лиш- них операций из списка триад удаляются триады типа same (вызывается функция Del Tri adTypes с параметром TRD SAME), после чего список триад отображается в спис- ке строк ListTriadSame.
ftalaHaus^k Пример выполнения работы 131 Полный текст программного кода модуля интерфейса с пользователем и описа- ние ресурсов пользовательского интерфейса можно найти в архиве, который рас- полагается на веб-сайте издательства, в файлах FormLab4.pas и FormLab4.dfm со- ответственно. Полный текст всех программных модулей, реализующих рассмотренный пример для лабораторной работы № 4, можно найти в архиве, располагающемся на веб- сайте издательства, в подкаталогах LABS и COMMON (в подкаталог COMMON вы- несены те программные модули, исходный текст которых не зависит от входного языка и задания по лабораторной работе). Главным файлом проекта является файл LAB4.DPR в подкаталоге LABS. Кроме того, текст модуля Triads приведен в лис- тинге П3.10, а текст модуля TrdOpt — в листинге П3.11 в приложении 3. Выводы по проделанной работе В результате лабораторной работы № 4 построен генератор списка триад, порож- дающий триады для логических операций, оператора присваивания и условного оператора. Генератор списка триад обнаруживает семантические ошибки, связан- ные с присваиванием значений константам (когда первый операнд оператора при- сваивания — константа).' При наличии одной ошибки пользователю выдается сообщение с указанием местоположения ошибки. При наличии нескольких оши- бок обнаруживается только первая из них, и дальнейший анализ исходного тек- ста прекращается. Построенный генератор также выполняет оптимизацию списка триад методом свертки объектного кода и исключения лишних операций, что позволяет сокра- тить объем результирующего списка триад и время выполнения объектного кода, который может быть построен на его основе. После выполнения оптимизации ге- нератор списка триад может удалять из списка триады специального вида С и same в зависимости от настроек, сделанных пользователем. Построенный при выполнении данной лабораторной работы генератор списка триад входит в состав компилятора, в который также входят: лексический анали- затор, построенный при выполнении лабораторной работы № 2, и синтаксический анализатор, построенный при выполнении лабораторной работы № 3. Этот ком- пилятор получает на вход исходную программу в соответствии с заданной грам- матикой и порождает результирующую программу в виде списка триад. Компилятор позволяет обнаруживать следующие однократные ошибки: □ любые лексические ошибки (неправильные лексемы); □ любые синтаксические ошибки (несоответствие исходной программы синтак- сису заданного входного языка); □ семантические ошибки типа «присваивание значения константе». При обнаружении ошибки пользователю выдается сообщение о типе ошибки (лек- сическая, синтаксическая или семантическая) и о местонахождении ошибки в тек- сте исходной программы. Дальнейший анализ типа обнаруженной ошибки не производится. При наличии нескольких ошибок в исходной программе обнару- живается только первая из них.
132 Лабораторная работа № 4 • Генерация и оптимизация объектного кода В результате выполнения лабораторных работ № 1-4 построен компилятор, вы- полняющий обработку исходной программы за пять проходов: 1. Лексический анализ исходного текста и построение таблицы лексем. 2. Синтаксический анализ по таблице лексем и построение дерева синтаксиче- ского разбора. 3. Построение списка триад по дереву синтаксического разбора. 4. Оптимизация списка триад методом свертки объектного кода. 5. Оптимизация списка триад методом исключения лишних операций. На каждом проходе компилятора исходными данными являются результаты, по- лученные при выполнении предыдущего прохода. Количество проходов построенного компилятора может быть существенно сокра- щено, поскольку все операции выполняются последовательно, независимо друг от друга, однако это не входит в задачу выполненных лабораторных работ.
^aiattaus,^. Курсовая работа Цель работы Цель работы: изучение составных частей, основных принципов построения и функ- ционирования компиляторов, практическое освоение методов построения про- стейших компиляторов для заданного входного языка. Курсовая работа заключается в создании компилятора с заданного подмножества языка Паскаль с незначительными модификациями и упрощениями (полное опи- сание входного и выходного языков дано далее в задании для каждого варианта). Результатами курсовой работы являются программная реадизация заданного ком- пилятора и пояснительная записка, оформленная в соответствии с требования- ми стандартов и задания на курсовую работу. Для программной реализации компилятора рекомендуется использовать язык программирования Object Pascal и систему программирования Borland Delphi. Возможно использовать другие языки и системы программирования по согласо- ванию с преподавателем. Компилятор рекомендуется построить из следующих составных частей: 1. Лексический анализатор. 2. Синтаксический анализатор. 3. Оптимизатор. 4. Генератор результирующего кода. Для построения компилятора рекомендуется использовать методы, освоенные в ходе выполнения лабораторных работ по курсу «Системное программное обес- печение».
134 Курсовая работа Порядок выполнения работы Рекомендуемый порядок выполнения работы представлен в табл. 5.1. Таблица 5.1. Рекомендуемые этапы и время выполнения курсовой работы № Этап выполнения работы Время выполнения (недели) Результат 1 2 Получение задания Выбор одной из трех форм грамматики, 1 Грамматика входного запись грамматики входного языка языка 3 в выбранной форме грамматики Определение границы между 0,25 Описание лексического 4 лексическим и синтаксическим анализаторами, выбор метода взаимодействия между ними Выбор способа организации 0,25 анализатора Описание выбранного 5 таблицы идентификаторов Построение лексического анализатора 0,5 способа организации таблицы идентификаторов Граф переходов автомата 6 Программная реализация 2 лексического анализатора Программный код 7 лексического анализатора Выбор класса КС-грамматик 0,5 лексического анализатора Описание синтаксического 8 для построения синтаксического анализатора Программная реализация 3,5 анализатора, обоснование выбора Программный код 9 синтаксического анализатора Выбор используемых форм 0,5 синтаксического анализатора Описание выбранных 10 внутреннего представления программы Описание используемого 0,5 форм внутреннего представления программы, обоснование выбора Алгоритм работы 11 алгоритма оптимизации Программная реализация 2 оптимизатора Программный код 12 оптимизатора Реализация генератора 2 оптимизатора Программный код 13 результирующего кода Отладка компилятора в целом 1 генератора результирующего кода Программный код 14 Оформление пояснительной записки 1,5 разработанного компилятора Пояснительная записка 15 Подготовка курсовой работы к защите 0,5 к курсовой работе 16 Защита курсовой работы Итого 16
ftalatiausiisk Требования к содержанию пояснительной записки 135 Требования к содержанию пояснительной записки Пояснительная записка к курсовой работе должна содержать следующие разделы: 1. Краткое изложение цели работы. 2. Задание по лабораторной работе (номер варианта и полное описание своего варианта). 3. Грамматика входного языка в одном из трех возможных видов: форма Бэкуса—Наура; форма с метасимволами; графическая форма. 4. Описание выбранного способа организации таблицы идентификаторов с обос- нованием сделанного выбора. 5. Описание лексического анализатора и выбранного метода его взаимодействия с синтаксическим анализатором. 6. Граф переходов или иное описание конечного автомата лексического анализа- тора. 7. Обоснование выбора класса КС-грамматик для построения синтаксического анализатора. 8. Описание синтаксического анализатора в зависимости от выбранного класса КС-грамматик (включая все необходимые управляющие таблицы и множе- ства). 9. Выбор форм внутреннего представления программы, используемых в компи- ляторе с обоснованием сделанного выбора. 10. Описание используемого метода порождения результирующего кода. 11. Описание используемого метода оптимизации. 12. Информация об организации построенного компилятора, его разбиении на проходы, количество проходов в компиляторе. 13. Выводы по проделанной работе. 14. Пример входной программы и результирующей программы, построенной ком- пилятором. 15. Текст программы компилятора. Примеры входной и результирующей программ, а также текст программы ком- пилятора рекомендуется оформлять в виде приложений к тексту пояснительной записки. В качестве основы построения синтаксического анализатора допускается выбрать любой класс КС-грамматик. Описание синтаксического анализатора должно быть полным, содержать все управляющие таблицы и множества, необходимые для построения алгоритма функционирования анализатора ( распознавателя).
136 Курсовая работа Допускается для построения лексического и (или) синтаксического анализато- ров использовать автоматизированные методы построения распознавателей (на- пример на основе программ LEX и YACC) [2,3,7,27,35]. В этом случае не требу- ется приводить граф переходов конечного автомата (для лексического анализа- тора) и описание синтаксического анализатора. В таком варианте соответствующие разделы пояснительной записки должны со- держать следующую информацию: обоснование выбора программы, используе- мой в качестве средства автоматизированного построения распознавателя, и текст входного файла, созданного для выполнения автоматизированного построения лексического либо синтаксического анализатора. Задание на курсовую работу Компилятор должен запускаться командной строкой с несколькими входными параметрами. Первым и главным входным параметром должно быть имя входно- го файла, вторым параметром может быть имя результирующего файла. Требова- ния к остальным параметрам командной строки и управляющим ключам (если они необходимы) устанавливаются исполнителем самостоятельно. Командная строка должна быть достаточной для функционирования компилято- ра. Помимо интерфейса командной строки возможно наличие дополнительного интерактивного интерфейса пользователя у компилятора (в том числе и графи- ческого) по усмотрению исполнителя работы. Входной язык компилятора должен удовлетворять следующим требованиям: □ входная программа начинается ключевым словом prog (program)'и заканчива- ется ключевым словом end.; □ входная программа может быть разбита на строки произвольным образом, все пробелы и переводы строки должны игнорироваться компилятором; □ текст входной программы может содержать комментарии любой длины, кото- рые должны игнорироваться компилятором (вид комментария задан в вари- анте задания); □ входная программа должна представлять собой единый модуль, содержащий линейную последовательность операторов, вызовы процедур и функций не предусматриваются; □ должны быть предусмотрены следующие варианты операторов входной про- граммы: оператор присваивания вида <переменная>:=<выражение>; условный оператор вида if <выражение> then <оператор> либо if <выраже- ние> then <оператор> else <оператор>; составной оператор вида begin... end; оператор цикла, предусмотренный вариантом задания;
’ftatallausf'k Задание на курсовую работу 137 □ выражения в операторах могут содержать следующие операции (минимум): арифметические операции сложения (+) и вычитания (-); операции сравнения «меньше» (<), «больше» (>), «равно» (=); логические операции И (and), ИЛИ (or), НЕ (not); дополнительные арифметические операции, предусмотренные вариантом задания; □ операндами в выражениях могут выступать идентификаторы (переменные) и константы (тип допустимых констант указан в варианте задания); □ все идентификаторы, встречающиеся в исходной программе, должны воспри- ниматься как переменные, имеющие тип, заданный в варианте задания (предва- рительного описания идентификаторов в исходной программе не требуется); □ должны учитываться два предопределенных идентификатора InpVar и Compi I eTest, смысл которых будет ясен из приводимого далее описания выходного языка. Приоритет операций исполнитель работы должен выбрать самостоятельно (при- оритет операций учитывается в грамматике входного языка). Для изменения при- оритета операций должны использоваться круглые скобки. Полное описание входного языка должно быть задано в грамматике входного язы- ка, которая строится исполнителем на первом этапе рабопд. Грамматика входного языка должна предусматривать любые входные цепочки, удовлетворяющие изло- женным требованиям. Допускаются любые модификации входного языка по выбо- ру исполнителя, если они не выходят за рамки указанных требований. Допускается расширять набор разрешенных операций и операторов вводного языка при усло- вии удовлетворения заданным минимальным требованиям, но при этом не разре- шается использовать операции и операторы из других вариантов задания — все та- кие операторы обязательно должны трактоваться как ошибочные. Компилятор должен проверять следующие семантические ограничения входного языка: □ не допускается присвоение значений константам; □ не допускается присвоение значения идентификатору InpVar; □ не допускается использовать идентификатор ConpileTest, иначе как для при- своения ему значений. В качестве выходного (результирующего) языка должен использоваться язык ас- семблера процессоров типа Intel 80x86 в модификации встроенного языка ассем- блера компилятора Pascal производства фирмы Borland. Результирующая программа должна иметь следующий вид: Prog <Имя_программы>; [Имя программы выбирается исполнителем самостоятельно} Var InpVar: <Тип_данных>; {Тип данных указан в варианте задания) Var <Список_переменных>: <Тип_данных>: {Описок переменных должен содержать перечень всех переменных из исходной программы}
138 Курсовая работа Function CompileTest!InpVar: <Тип_данных>): <Тип_данных>: {Переменные CompileTest и InpVar являются предопределенными в тексте исходной программы} Begin Asm {Сюда должен быть включен текст результирующей программы. порожденный компилятором} end; end: begin readln(InpVar); writeln(CompileTestdnpVar)); end. Всю неизменную часть результирующей программы компилятор должен порож- дать самостоятельно вне зависимости от поданной па вход исходной программы. Имя результирующей программы исполнитель выбирает самостоятельно. Иден- тификаторы InpVar и CompileTest являются предопределенными переменными, которые используются для подачи значений на вход результирующей программы и получения результата от нее при тестировании работоспособности результиру- ющей программы. Тпп данных, используемый для всех переменных, задается в варианте задания. Все встречающиеся в исходной программе идентификаторы следует считать про- стыми скалярными переменными, не требующими выполнения преобразования ти- пов. Ограничения на длину идентификаторов и констант во входной программе ис- полнитель выбирает самостоятельно, но выбранная длина не должна быть меньше 32.В случае если на вход компилятора подается входная программа, содержащая семантические или синтаксические ошибки, компилятор должен корректно за- вершать свое выполнение и выдавать сообщение о найденной ошибке во входной программе с указанием строки, в которой найдена ошибка. По возможности ком- пилятор должен указывать тип найденной ошибки. Компилятор может указать несколько ошибок во входной программе, если они были им обнаружены. Варианты заданий Предлагаемые варианты заданий приведены в табл. 5.2. Таблица 5.2. Варианты заданий на выполнение курсовой работы № Тип констант Дополнительные операции Оператор цикла Оптимизация Тип данных Тип комментария 1 2 *. / 1 1 Byte 1 2 2 >> << 1 1 Byte 2 3 2 ++ 1 1 Byte 3
ftataUausiiilk Варианты заданий 139 № Тип констант Дополнительные операции Оператор цикла Оптимизация Тип данных Тип комментария 4 2 — 1 1 Byte 4 5 8 *./ 2 1 Word 1 6 8 » << 2 1 Word 2 7 8 ++ 2 1 Word 3 8 8 -- 2 1 Word 4 9 16 *,/ 3 1 Integer 1 10 16 >> « 3 1 Integer 2 11 16 ++ 3 1 Integer 3 12 16 -- 3 1 Integer 4 13 2 *,/ 4 1 Byte 1 14 2 » « 4 1 Byte 2 15 2 ++ 4 1 Byte 3 16 2 — 4 1 Byte 4 17 16 *./ 1 1 Word 4 18 16 >> « 1 1 Word 3 19 16 ++ 1 1 Word 2 20 16 -- 1 1 Word 1 21 8 *./ 2 1 Integer 4 22 8 » « 2 1 Integer 3 23 8 ++ 2 1 Integer 2 24 8 — 2 1 Integer 1 25 2 */ 1 2 Byte 1 26 2 » << 1 2 Byte 2 27 2 ++ 1 2 Byte 3 28 2 — 1 2 Byte 4 29 8 *,/ 2 2 Word 1 30 8 » « 2 2 Word 2 31 8 ++ 2 2 Word 3 32 8 -- 2 2 Word 4 33 16 *./ 3 2 Integer 1 34 16 >> << 3 2 Integer 2 35 16 ++ 3 2 Integer 3 36 16 -- 3 2 Integer 4 37 2 *./ 4 2 Byte 1 38 2 >> << 4 2 Byte 2 39 2 ++ 2 Byte 3 40 2 — 4 2 Byte 4 41 16 */ 1 2 Word 4 42 16 >> « 1 2 Word 3 43 16 ++ 1 2 Word 2 44 16 — 1 2 Word 1 продолжение &
140 Курсовая работа Таблица 5.2 (продолжение) № Тип Дополнительные констант операции Оператор цикла Оптимизация Тип данных Тип комментария 45 8 *,/ 2 2 Integer 4 46 8 »« 2 2 Integer 3 47 8 ++ 2 2 Integer 2 48 8 2 2 Integer 1 Ниже поясняются цифровые обозначения, используемые в табл. 5.2. Типы констант: 2 — двоичные; 8 — восьмеричные; 16 — шестнадцатеричные. Дополнительные операции: *, / — умножение и деление;, » « — сдвиги вправо и влево (арифметические или логические — по выбору); ++ — инкремент (увеличение значения переменной на 1); ---декремент (уменьшение значения переменной на 1). Типы дополнительных операторов цикла: 1. Цикл с предусловием вида while <выражение> do <оператор>. 2. Цикл с постусловием типа repeat <оператор> until <выражение>. 3. Цикл с постусловием вида do <оператор> while <выражение>. 4. Два варианта цикла с перечислением по заданной переменной вида for <пе- ременная>:=<выражение> to <выражение> do <оператор> либо for <переменная>:=<вы- ражение> downto <выражение> do <оператор>. Типы комментариев: 1. Комментарий в фигурных скобках: {...}. 2. Комментарий в круглых скобках со звездочкой: (*...*). 3. Комментарий за двойной косой чертой до конца строки: //.... 4. Комментарий внутри косых черт со звездочкой: /*...*/. Методы оптимизации: 1. Исключение лишних операций. 2. Свертка объектного кода. Порядок оценки результатов работы Выполненная курсовая работа оценивается по следующим показателям: □ содержание пояснительной записки;
^iatalladsfk Порядок оценки результатов работы 141 □ функциональность построенного компилятора; □ способность исполнителя отвечать на вопрос ы по содержанию пояснительной записки и по сути работы. Текст пояснительной записки должен удовлетворять требованиям ГОСТ и стан- дартов университета. Содержание пояснительной записки должно удовлетворять требованиям настоящего задания на выполнение курсовой работы. Функциональность компилятора проверяется путем подачи на его вход простей- ших контрольных примеров (в том числе и примеров ошибочных входных про- грамм). При этом полученная результирующая программа проверяется методом компиляции ее в системе программирования Delphi 5 с последующим выполне- нием. Результат выполнения сравнивается с подсчитанным вручную результатом выполнения контрольного примера. Функциональность компилятора в первую очередь оценивается по заданным ми- нимальным требованиям и по работоспособности компилятора (отсутствие «за- висаний» и нерегламентированных сообщений об ошибках при любых входных данных). Дополнительные бонусы при оценке компилятора могут быть получены за следу- ющие расширения заданной минимальной функциональности: □ реализация дополнительных ключей и параметров управления работой ком- пилятора; □ наличие у компилятора дополнительного интерактивного интерфейса с пользо- вателем; □ эффективная («сокращенная») обработка логических операций и операций сравнения (метод ее реализации описан в примере выполнения лабораторной работы № 4 в части, посвященной описанию генератора кода и схем СУ-пере- вода); □ реализация дополнительных операторов и операций входного языка. В каче- стве наиболее очевидного расширения входного языка предлагается реализо- вать оператор выхода из цикла (break) и перехода к следующей итерации цик- ла (continue); □ дополнительный семантический контроль .входной программы; □ любые дополнительные методы оптимизации результирующей программы (как машинно-независимые, так и машинно-зависимые); □ расширенная диагностика ошибок, генерация предупреждений по поводу опе- раторов входного языка, вызывающих сомнение с точки зрения их семантики. Не допускается реализовывать функциональность, предусмотренную другими вариантами курсовой работы, — такая функционал ьееость рассматривается не как дополнительный бонус, а как недостаток компилятора. Дополнительные бонусы не учитываются и не засчитываются, если не реализова- на минимальная функциональность компилятора, предусмотренная вариантом задания.
142 Курсовая работа Способность исполнителя курсовой работы отвечать на вопросы по содержанию пояснительной записки и по сути работы проверяется в личной беседе с препода- вателем при защите курсовой работы. Рекомендации по выполнению работы В любом случае при знакомстве с примером выполнения работы и при выполне- нии работы ио своему заданию надо обратить внимание на следующие основные моменты: 1. Построение грамматики входного языка — это определяющий момент в кур- совой работе. Правильно построенная грамматика существенно упростит вы- полнение работы, а ошибки, напротив, могут существенно увеличить трудо- емкость последующих этапов. Рекомендуется, построив грамматику, сразу же проконсультироваться с преподавателем, чтобы исправить возможные ошиб- ки на ранней стадии. 2. Выполняющий курсовую работу должен решить для себя, как он будет стро- ить лексический и синтаксический анализаторы: самостоятельно (вручную) или автоматизированным методом (с использованием специализированного ПО — рекомендуются программы LEX и YACC). Автоматизированный метод проще и быстрее, но требует от автора работы времени на освоение специали- зированного программного обеспечения. Возможно сочетать оба метода: на- пример, построить лексический анализатор с помощью программы LEX (она достаточно проста в использовании), а синтаксический анализатор — вручную. Рекомендации на этот счет каждый должен выбрать, оценив свои силы в воз- можности освоения нового программного обеспечения. 3. Создание лексического анализатора — это этап, не представляющий особой сложности, так как построение КА для лексического анализатора представля- ет собой полностью формализованный процесс (при выполнении которого в первую очередь важна внимательность). Но при выполнении этого этапа глав- ная задача не в том, чтобы грамотно построить КА — в этом, чаще всего, нет проблем, — а в том, чтобы максимально эффективно разделить анализ, выпол- няемый лексическим анализатором, и анализ, выполняемый анализатором син- таксическим. Как правило, чем больше работы выполняет лексический анали- затор, тем лучше. Уже построив грамматику языка, нужно иметь представле- ние о том, какие элементы языка будут выделяться на этапе лексического анализа. 4. Выбор класса КС-грамматики для создания синтаксического анализатора — это, с точки зрения автора, второй по важности этап после построения грамма- тики. Задание составлено так, что любой входной язык может быть задан грам- матиками, анализируемыми по крайней мере тремя методами: методом рекур- сивного спуска (или же ££(1)-грамматикой), методом на основе грамматик операторного предшествования и методом на основе LR( 1) или LALR( 1 )-грам-
^ialallaus,^ Пример выполнения курсовой работы 143 матик. Важно построить грамматику входного языка так, чтобы опа соответ- ствовала интересующему методу, или же уметь преобразовать ее к требуемо- му виду. К сожалению, тут нет формализованных рекомендаций. Самый про- стой подход — взять для описания грамматики языка приемы и правила, рас- смотренные при выполнении лабораторной работы № 3, тогда для построения синтаксического распознавателя с большой вероятностью подойдет метод на основе грамматик операторного предшествования. 5. Выбор формы внутреннего представления программы, методов оптимизации и генерации результирующего кода — это взаимозависимые процессы. По- скольку рассмотренные ранее методы и алгоритмы были основаны па исполь- зовании триад, автор не рекомендует пытаться использовать другие формы внутреннего представления. Необходимую дополнительную информацию можно найти в литературе по ком- пиляторам и системам программирования [1-4, 7]. Пример выполнения курсовой работы В качестве примера для иллюстрации выполнения курсовой работы будет взят входной язык, который, с одной стороны, не совпадает ни с одним из вариантов задания, а с другой стороны — позволяет хорошо проиллюстрировать все методы и технические приемы, которые полезны при выполнении работы. Многие методы, технические приемы и их реализация в курсовой работе будут взяты из лабораторных работ № 1-4, которые были рассмотрены ранее. Другие методы, наоборот, будут реализованы иначе, чтобы проиллюстрировать все суще- ствующие возможности, их преимущества и недостатки. В каждом случае будет дано пояснение, почему использован тот или иной метод, В примере проиллюстрированы следующие интересные моменты: □ разделение лексического и синтаксического анализаторов с целью упрощения работы последнего (на примере унарной арифметической операции «-» и опе- рации сравнения типа «не равно»); □ обнаружение присваивания значений константам на этапе синтаксического раз- бора (в лабораторных работах № 3 и 4 эта же операция выполнялась на этапе семантического анализа перед генерацией кода); □ возможности преобразования исходной грамматики, изменения синтаксиса входного языка и модификации алгоритма синтаксического разбора на основе анализа правил грамматики (на примере условного оператора); □ модификация остовной грамматики при необходимости различать правила; □ методы обработки логических операций и операции сравнения; □ простейший семантический анализ и модификация результирующего кода на этапе семантического анализа (на примере обработки переменных InpVar и Com- pi 1 eTest);
144 Курсовая работа □ элементарные методы машинно-зависимой оптимизации результирующего кода. Для реализации курсовой работы будут использоваться программные модули, созданные при выполнении лабораторных работ № 1-4, код которых не зависит от входного языка. Такой подход иллюстрирует, насколько удобно и эффективно выделять не зависящую от входного языка часть компилятора в отдельные моду- ли или библиотеки. Задание для примера выполнения работы В качестве примера возьмем следующие условия для входного языка: 1. Тип допустимых констант: десятичные. 2. Дополнительная операция: унарный арифметический минус (-). 3. Оператор цикла: while (<выражение>) do <оператор>. 4. Оптимизация: оба метода (1 и 2). 5. Тип данных: Long integer (longint, 32 бит). 6. Тип комментария: фигурные скобки ({ ... }). Кроме того, модифицируем синтаксис условного оператора (два типа): □ if (<выражение>) <оператор> else <оператор>; □ if (<выражение>) <оператор>; и дополним перечень операций сравнения операцией «не равно» (<>). Получим входной язык, сочетающий в себе элементы синтаксиса языков C++ (эле- менты оператора цикла и условный оператор) и Object Pascal (оператор цикла, составной оператор begin ... end, оператор присваивания и комментарии). В качестве результирующего (выходного) языка компилятора будем использо- вать язык ассемблера процессоров типа Intel 80386 и более поздних модифика- ций в модификации для системы программирования Delphi 5 [9, 23, 28, 41, 44]. Чтобы исключить неоднозначности при работе с этой системой программирова- ния, изменим семантические ограничения входного языка: □ сделаем допустимым использование имени переменной CompileTest в любых операторах входного языка (а не только в операторах присваивания); □ запретим использование переменных с именем Result, так как такое имя пере- менной является предопределенным в целевой вычислительной системе. Кроме того, в именах переменных во входном языке не должны различаться строч- ные и прописные буквы (например, переменные с именами i и I должны воспри- ниматься как одна и та же переменная). Грамматика входного языка Грамматику входного языка построим на основе фрагментов грамматик, рассмот- ренных в заданиях по лабораторной работе № 3. Там имеются правила для ли-
!\alaftaus^k Пример выполнения курсовой работы 145 нейных операций (арифметические и логические операции) и для условного опе- ратора. По аналогии с условным оператором построим оператор цикла. Для со- ставного оператора и всей программы в целом останется определить еще одно понятие — последовательность операторов. Будем рассматривать последователь- ность операторов как цепочку операторов, разделенных знаком «точка с запятой». В результате получим КС-грамматику в форме Бэкуса-Наура: G({prog,end.,if,else,begin,end,while,do,or,xor,and,not,<,>,=,<>,(,),-,+,um,а,с, {S,L,O,B,C,D,E,T,F},P,S) с правилами P: S -» prog L end. L-+O\L-,O\L; О —> if(B) О else О | if(B) О | begin L end | while(B)do О |a:=£ В В or C | В xor C | C С-ь C and D | D D -э E<E | E>E | E=E | E<>E | (B) | not(B) E-*E-T\E+T\T T-+umT\F F-»(£)|a|c Жирным шрифтом выделены терминальные символы. Всего в построенной грамматике G 28 правил. Нетерминальные символы грам- матики имеют следующий смысл: □ 5 — вся программа; □ L — последовательность операторов (может состоять и из одного оператора); □ О — оператор (пять видов: полный условный оператор, неполный условный оператор, составной оператор, оператор цикла, оператор присваивания); □ В, С — логическое выражение и его элементы; □ D — операция сравнения или логическое выражение в скобках; □ E,T,F— арифметическое выражение и его элементы. Можно обратить внимание на следующие моменты в грамматике: □ операция шп («унарный минус») обозначена отдельным терминальным симво- лом, не совпадающим со знаком арифметической операции вычитания («-»), хотя в тексте исходной программы эти два. символа идентичны; □ константы и переменные обозначены двумя различны ми терминальными сим- волами — а и с соответственно — это говорит о том, что они должны разли- чаться на этапе синтаксического анализа; □ операция отрицания not обязательно требует после себя выражения в скоб- ках, что не совсем соответствует традиционной записи логических операций (но не противоречит заданию!), традиционная запись могла бы быть записана в виде правил:
146 Курсовая работа D —> not D | G G-+E<E\E>E\E=E\E<>E\(B) Последний пример показывает, что разработчик грамматики не обязан следовать общепринятым шаблонам: он может отходить от них, если это не противоречит заданию. Нередко это помогает существенно сократить трудоемкость выполне- ния работы (далее будут проиллюстрированы еще две проблемы, связанные с унар- ным знаком «минус» и условным оператором). Описание выбранного способа организации таблицы идентификаторов Для организации таблицы идентификаторов выберем комбинированный способ, поскольку в нем отсутствуют ограничения на количество входных идентифика- торов и он не требует разработки сложной и эффективной хэш-функции (разра- ботка комбинированной таблицы в данном случае проще, чем выбор хорошей хэш- функции). В качестве такого способа возьмем комбинацию хэш-адресации и бинарного де- рева, которая была использована при выполнении лабораторной работы № 1. Программный код, реализующий такую таблицу идентификаторов, приведен в ли- стингах П3.1 и П3.2 в приложении 3. Для того чтобы в таблице идентификаторов в именах переменных не различались строчные и прописные буквы, этот код дол- жен быть откомпилирован с указанием соответствующих условий. Описание лексического анализатора Для построения лексического анализатора воспользуемся подходом, использо- ванном в лабораторной работе № 2. Задача лексического анализатора для описанного выше языка заключается в том, чтобы распознавать и выделять в исходном тексте программы все лексемы этого языка. Лексемами данного языка являются: □ двенадцать ключевых слов языка (prog, end., if, else, begin, end, while, end, not, or, xor и and); □ разделители: открывающая и закрывающая круглые скобки, точка с запятой; □ знаки операций: присваивание, сравнение (четыре знака), сложение, вычита- ние и унарный минус; □ идентификаторы; □ целые десятичные константы без знака. Можно заметить, что end и end. — это разные лексемы. Кроме перечисленных лексем распознаватель должен уметь определять и исклю- чать из входного текста комментарии, принцип построения которых описан выше. Для выделения комментариев ключевыми символами должны быть открываю- щая и закрывающая фигурные скобки.
Пример выполнения курсовой работы 147 Отдельного внимания заслуживает знак «унарный минус*, который не случайно был взят в качестве иллюстрации для этого примера. Если не делать различий между унарным минусом и бинарным, то правила грам- матики G для символов Е, Ти Fимели бы следующий вид: Е-+Е-Т\Е+Т\Т Т-+-Т\Е F-* (£) | а | с Однако такая грамматика будет очень сложна для синтаксического анализа, по- скольку в ней возникает проблема выбора правила между Е-Ти-Тпри выполне- нии свертки (можно проверить и прийти к выводу, что она, например, не являет- ся грамматикой операторного предшествования). Преобразования для этой грам- матики неочевидны. Но возможно более простое решение, если понять, как различить две операции (унарный и бинарный знаки «минус») на этапе лексического анализа. Различие можно сделать, если заметить, что бинарный минус всегда следует после операн- да (переменной или константы) или после закрывагощей круглой скобки, в то время как унарный минус — или после знака операции, или после открывающей круглой скобки. Такой анализ вполне по силам КА, если в него добавить еще одно состояние, которое будет определять, какую лексему (унарный или бинарный минус) сопоставлять с входным символом «-». Поскольку перед унарным мину- сом, как и перед любыми другими лексемами, может быгь комментарий, то при- дется добавить два состояния (чтобы различать, в каком месте КА встретилось начало комментария, и после завершения комментария вернуться в то же место). Таким образом, незначительное усложнение К Алексического анализатора позво- лит избежать серьезных проблем на этапе синтаксического анализа. Данный пример иллюстрирует, как важно рационально провести границу между лексическим и синтаксическим анализом. Другой пример из заданного входного языка еще более очевиден, хотя он и не ведет к столь серьезным осложнениям при лексическом разборе: это знак опера- ции «не равно» — «<>». Его можно рассматривать каке две лексемы или как одну. В первом случае проверка правильности этой операции будет идти на этапе син- таксического анализа, во втором случае — на этапе лексического анализа. Оба ва- рианта могут быть без проблем реализованы, но второй из них представляется все же более логичным. Как правило, если есть возможность выявления ошибки па более ранних стадиях компиляции, лучше такой возможностью воспользоваться. Из этой рекоменда- ции есть исключения — ей лучше не следовать в тех случаях, когда ранний анализ ке дает существенных преимуществ, но может нарушить логическую стройность языка или грамматики, затруднит их восприятие человеком. Например, в том же входном языке сочетания if( и whilе( могут быть рассмотре- ны как единые лексемы (обозначим их if_ и w_l) и выявлены на этапе лексиче-
148 Курсовая работа ского анализа. При этом синтаксический анализатор не получает никаких пре- имуществ, но правила грамматики для нетерминального символа будут иметь вид: О —> if_ В) О else О | if_ В) О | begin L encl | wl B)do О | а:=Е Логическая целостность и структура правил нарушены, так как человеку трудно воспринимать закрывающую скобку при отсутствии открывающей, а потому от такого варианта лучше отказаться (хотя окончательное решение, конечно, всегда остается за разработчиком компилятора). В данном языке лексический анализатор всегда может однозначно определить границы лексемы, поэтому нет необходимости в его взаимодействии с синтакси- ческим анализатором и другими элементами компилятора. Приняв во внимание правила и соглашения, рассмотренные для КА в лаборатор- ной работе № 2, полностью КА можно описать следующим образом: M(QX5,<7o,F): Q= {Н, С, Cl, G, S, L, V, D, Pl, Р2, РЗ, Р4, El, Е2, ЕЗ, И, 12, L2, L3, L4, В1, В2, ВЗ, В4, В5, Wl, W2, W3, W4, W5, О1, 02, DI, D2, XI, Х2, ХЗ, Al, А2, АЗ, Nl, N2, N3, F}; Е = А (все4 допустимые алфавитно-цифровые символы); < 7о = Н; F = {F, 5}. Функция переходов (5) для этого КА приведена в приложении 2. Из начального состояния КА литеры «р», «е», «i», «Ь», «w», «о», «х», «а» и «п» ведут в начало цепочек состояний, каждая из которых соответствует ключевому слову (цепочка, начинающаяся с «е», соответствует трем ключевым словам): □ состояния Р1, Р2, РЗ, Р4 — ключевому слову prog; □ состояния El, Е2, ЕЗ — ключевым словам end и end.; □ состояния И, 12 — ключевому слову if; □ состояния Bl, В2, ВЗ, В4, В5 — ключевому слову begin; □ состояния Wl, W2, W3, W4, В5 — ключевому слову while; □ состояния El, L2, L3, L4 — ключевому слову else; □ состояния DI, D2 — ключевому слову do; □ состояния О1, 02 — ключевому слову or; □ состояния XI, Х2, ХЗ — ключевому слову хог; □ состояния Al, А2, АЗ — ключевому слову and; □ состояния Nl, N2, N3 — ключевому слову not. Остальные литеры ведут к состоянию, соответствующему переменной (иденти- фикатору) — V. Если в какой-то из цепочек встречается литера, не соответствую- щая ключевому слову, или цифра, то КА также переходит в состояние V, а если встречается граница лексемы — запоминает уже прочитанную часть ключевого слова как переменную (чтобы правильно выделять такие идентификаторы, как «Ь> или «els», которые совпадают с началом ключевых слов).
^atattausi^k Пример выполнения курсовой работы 149 Цифры ведут в состояние, соответствующее входной константе, — D. Открываю- щая фигурная скобка ведет в состояние С, которое соответствует обнаружению комментария — из этого состояния КА выходит, только если получит на вход за- крывающую фигурную скобку. Еще одно состояние — G — соответствует лексеме «знак присваивания». В него КА переходит, получив на вход двоеточие, и ожида- ет в этом состоянии символа «равенство». Знаки арифметических операций («+» и-в-*), знаки операций сравнения («<», «>» и «=»), открывающая круглая скобка, а также последние символы клю- чевых слов переводят КА в состояние S, которое отличается от начального состо- яния тем, что в этом состоянии КА воспринимает символ «-» как знак унарной операции отрицания, а не как знак операции вычитания. Если в состоянии S на вход КА поступает открывающая фигурная скобка, го сн переходит в состояние С1 (а не в состояние С), из которого по закрывающей фигурной скобке опять воз- вращается в состояние S. Рис. 5.1. Граф переходов сокра-щенного КА (без учета ключевых слов)
150 Курсовая работа В еще одно состояние — состояние L — КА переходит, когда на его вход поступа- ет знак «<». В состоянии L автомат проверяет, является ли знак «<» началом лек- семы «<>» («не равно») или же это отдельная лексема «<» («меньше»). Состояние Н — начальное состояние КА, а состояния F и S — его конечные со- стояния. Поскольку КА работает с непрерывным потоком лексем, перейдя в ко- нечное состояние Н, он тут же должен возвращаться в начальное состояние, что- бы распознавать очередную лексему. Поэтому в моделирующей программе два состояния (Н и F) можно объединить в одно. В функцию переходов КА не входит состояние «ошибка», чтобы не загромождать ее. В это состояние КА переходит всегда, когда получает на вход символ, по кото- рому нет переходов из текущего состояния. Видно, что граф переходов для данного КА будет слишком громоздким, чтобы его можно было наглядно представить на рисунке. Граф переходов сокращенного КА (без учета распознавания ключевых слов) представлен на рис. 5.1. Реализация данного КА выполнена аналогично реализации КА, построенного в ла- бораторной работе № 2. Для описания структур данных лексем, которые не зависят от входного языка, используется модуль LexElem, который был создан при выполне- нии лабораторной работы № 2 (листинг П3.4, приложение 3). Типы допустимых лек- сем описаны в модуле LexType (листинг ПЗ.З, приложение 3), а функционирование автомата моделируется в модуле LexAuto (листинг П3.5, приложение 3). Описание синтаксического анализатора Для построения синтаксического анализатора будем использовать анализатор на основе грамматик операторного предшествования. Этот анализатор является ли- нейным распознавателем (время анализа линейно зависит от длины входной це- почки), для него существует простой и эффективный алгоритм построения рас- познавателя на основе матрицы предшествования [1-3, 7]. К тому же алгоритм «сдвиг-свертка» для данного типа анализатора был разработан при выполнении лабораторной работы № 3, а поскольку он не зависит от входного языка, он мо- жет быть без модификаций использован в данной работе. Построение распознавателя Для построения анализатора на основе грамматики операторного предшествова- ния необходимо построить матрицу операторного предшествования (порядок ее построения был детально рассмотрен при выполнении лабораторной работы № 3). Построим множества крайних левых и крайних правых символов грамматики G. На первом шаге получим множества, приведенные в табл. 5.3. Таблица 5.3. Множества крайних левых и крайних правых символов. Шаг 1 Символ U L(U) R(U) F (> а, с ), а, с Т urn, F Т, F
^lajatiaus^i Пример выполнения курсовой работы 151 Символ U L(U) R(U) Е D С В 0 L S Е, Т Т (, not, Е Е, ) С, D D В, С С if, begin, while, а О, Е, end L, О О, ; prog end. После завершения построения мы получим множества, представленные в табл. 5.4 (детальное построение множеств крайних левых и ссрайнпх правых символов опи- сано при выполнении лабораторной работы № 3). Таблица 5.4. Множества крайних левых и крайних правых символов. Результат Символ U UU) R(U) F Т Е D С В 0 L S (, а, с ), а, с um, F, (, а, с Г, F, ), а, с Е, Т, um, F, (, a, G Г, F, ), а, с (, not, Е, Т, um, F, а, с Е, Т, F, ), а, с С, D, (, not, Е, Т, um, F, а, с D, Е, Т, F, ), а, с В, С, D, (, not, Е, Т, um, F, а, с С, D, Е, Г, F, ), а, с if, begin, while, а О, Е, end, Т, F, ), а, с L, О О, ;, Е, end, Т, F, ), а, с prog end. После этого необходимо построить множества крайних детых и крайних правых терминальных символов. На первом шаге возьмем все крайние левые и крайние правые терминальные символы из правил грамматики G. Получим множества, представленные в табл. 5.5. Таблица 5.5. Множества крайних левых и крайних правых терминальных симво- лов. Шаг 1 Символ U Lt(U) Rt(U) F Т Е D С В 0 L S (. а, с ), а, с um Um <, >, =, <>, (. not <, >, =, <>,» and and or, xor or, xor if, begin, while, a else, ), end, do, := 1 * Prog end.
152 Курсовая работа Дополним множества, представленные в табл. 5.5, на основе ранее построенных множеств крайних левых и крайних правых символов, представленных в табл. 5.4 (алгоритм выполнения этого действия подробно рассмотрен при выполнении ла- бораторной работы № 3). Получим множества крайних левых и крайних правых терминальных символов, которые представлены в табл. 5.6. Таблица 5.6. Множества крайних левых и крайних правых терминальных симво- лов. Результат Символ U Lt(U) Rt(U) F (, а, с ). a, c Т urn, (, а, с um, ), a, c Е +, urn, (, а, с -, +, um, ), a, c D <, >, =, о, (, not, -, +, urn, а, с <, >, =, <>, ), +, um, a, c С and, <, >, =, О, (, not, -, +, um, а, с and, <, >, =, <>, ), -, +, um, a, c В or, xor, and, <, >, =, о, (, not, -, +, um, a, c or, xor, and, <, >, =, <>, ), -, +, um, a, c О If, begin, while, a else, ), end, do, :=, -, +, um, a, c L ;, if, begin, while, a ;, else, ), end, do, :=, -, +, um, a, c S prog end. После построения множеств, представленных в табл. 5.6, можно заполнять мат- рицу операторного предшествования. Преобразование грамматики, модификация я^ыка и другие способы разрешения конфликтов Однако при заполнении матрицы операторного предшествования возникает про- блема: символ ) стоит рядом с символом else в правиле О —> if(B) О else О (меж- ду ними один нетерминальный символ О). Значит, в клетке матрицы оператор- ного предшествования на пересечении столбца, помеченного else, и строки, по- меченной ), должен стоять знак «=•» («составляют основу»). Но в то же время символ else стоит справа от нетерминального символа О в том же правиле О—> if(B) О else О, а в множество крайних правых терминальных символов R,(0) входит символ ). Тогда в клетке матрицы операторного предшествования на пересечении столбца, помеченного else, и строки, помеченной ), должен сто- ять знак «•>» («следует»). Получаем противоречие (в одну и ту же клетку мат- рицы предшествования должны быть помещены два знака — «=•» и «•>»), кото- рое говорит о том, что исходная грамматика G не является грамматикой опера- торного предшествования. Как избежать этого противоречия? Во-первых, можно изменить входной язык так, чтобы он удовлетворял требова- ниям задания на курсовую работу, но не содержал операторов, приводящих к та- ким неоднозначностям. Например, добавив во входной язык ключевые слова then и endif, для нетерминального символа О получим правила:
Пример выполнения курсовой работы 153 О -»if В then О else О endif | if В then О endif | begin L end | while(B)do О | а:=Е Если построить матрицу операторного предшествования, используя эти правила вместо имеющихся в грамматике G для символа О, то можно заметить, что проти- воречий в ней не будет. Во-вторых, можно, не изменяя языка, попытаться преобразовать грамматику G к такому виду, чтобы она удовлетворяла требованиям грамматик операторного предшествования (как уже отмечалось ранее, а также как сказано в [1, 3, 7], из- вестно, что формальных рекомендаций по выполнению таких преобразований не существует). Например, если добавить во входной язык только ключевое слово then, то для нетерминального символа О получим правила: О —» if В then О else О | if В then О | begin L end | while(B)do О | а:=Е В этом случае в матрице операторного предшествования для ключевых слов then и else возникнет противоречие, аналогичное рассмотренному ранее противоре- чию для лексем ( и else. Добавив в грамматику G еще один нетерминальный сим- вол R, получим правила, аналогичные правилам, приведенным в задании по лабо- раторной работе № 3: О -> if В then R else О | if В then О | begin L end | while(B)do О | а:=Е R —> if В then R else R | begin L end | while(B)do 01 a:=E Если построить матрицу операторного предшествования, используя эти правила вместо имеющихся в грамматике G для символа О, то снова можно заметить, что противоречий в ней не будет. Допустимы оба рассмотренных варианта, а также их комбинации. Первый из них требует добавления нового ключевого слова — а значит, усложняется лексический анализатор, второй ведет к созданию новых нетерминальных символов и новых правил в грамматике — это усложняет синтаксический анализатор и генератор кода. К тому же второй вариант требует неформальных преобразований правил грамматики, которые не всегда могут быть найдены (например, автору не извест- ны такие преобразования, которые могли бы привести рассматриваемую здесь грамматику G к виду операторного предшествования — читатели могут попробо- вать в этом свои силы самостоятельно). Если других препятствий нет, то, с точки зрения автора, первый вариант предпочтительнее (лучше изменить синтаксис входного языка и упростить свою работу)1. Однако бывают случаи, когда проблему можно обойти, не прибегая к преобразо- ваниям языка или грамматики. И в данном случае это именно так. Если посмотреть, к чему ведет размещение в одной клетке матрицы операторного предшествования двух знаков — «=•» и «•>», то можно заметить, что это означает конфликт между выполнением свертки и выполнением переноса при разборе услов- ного оператора. Почему такой конфликт возникает? Этому есть две причины: 1 Создатели реальных компиляторов, конечно же, лишены первого из предлагаемых вариантов — син- таксис входного языка для них строго определен и не может меняться, поэтому они вынуждены искать преобразования грамматик.
154 Курсовая работа □ во-первых, распознаватель не может определить, к какому оператору if отно- сить очередную лексему else (такой конфликт можно наглядно проиллюстри- ровать на примере оператора: if (а<Ь) then 1f(a<c) then а:=с else a:=b:); □ во-вторых, конец логического выражения в условии после ключевых слов if ( определяет лексема ) (закрывающая круглая скобка), но точно такая же лек- сема может стоять и в конце арифметического выражения перед ключевым словом else: распознаватель не может решить, куда относится очередная лек- сема ) — к условному оператору или к арифметическому выражению. Это еще одна причина конфликта. Первое противоречие можно разрешить на основании правил, общепринятых для многих языков программирования: ключевое слово else должно всегда относить- ся к ближайшему оператору if. Второе противоречие можно разрешить, если про- верять, что предшествует закрывающей круглой скобке — логическое или ариф- метическое выражение. Тогда конфликт между сверткой и переносом должен ре- шаться в пользу переноса, чтобы анализатор мог выбрать максимально длинный условный оператор и отнести else к ближайшему if, если перед скобкой следует логическое выражение, в противном случае должна выполняться свертка. Следовательно, из двух знаков, которые могут быть помещены в клетку матрицы операторного предшествования на пересечении столбца, помеченного else, и стро- ки, помеченной ), следует выбрать знак «=•» («составляет основу»), имея в виду, что он требует дополнительного анализа второго символа от верхушки стека. По- скольку других конфликтов в исходной грамматике нет, то можно заполнить мат- рицу операторного предшествования, которая представлена в табл. 5.7 (чтобы сократить размер таблицы, отношения предшествования в ней обозначены сим- волами «<», «>» и «=» без точки «•»). Более подробно о вариантах модификаций алгоритма «сдвиг-свертка» для различ- ных грамматик, в которых присутствуют противоречия между выполнением опе- раций «сдвиг» и «свертка» на этапе синтаксического разбора, можно узнать в [ 1,2]. Для проверки условия наличия логического выражения перед закрывающей скоб- кой и разрешения конфликта между переносом и сверткой для символа else ис- пользуется функция корректировки отношений предшествования CorrectRule (мо- дуль SyntRule, листинг П3.6 в приложении 3). ВНИМАНИЕ -------------------------------------------------------------- Принцип разрешения конфликтов в матрице операторного предшествования на основе согла- шений входного языка следует использовать очень осторожно, и далеко не всегда он может помочь избежать преобразований грамматики. Действительно, зачастую возможны случаи, когда конфликт не может быть раз- решен на основе простого анализа правил исходной грамматики. Например, если бы правила грамматики G для символа О выглядели бы следую- щим образом (без использования ключевого символа do): О —> if(B) О else О | if(B) О | begin L end | while(B) О | а:=£ то разрешить конфликт однозначным образом было бы невозможно, поскольку кроме рассмотренных конфликтов в приведенных правилах грамматики существу-
^lalattaus,^ .7. Матрица операторного предшествования
156 Курсовая работа ет также конфликт между выполнением сдвига или свертки при наличии вложен- ного оператора while перед частью else условного оператора. И если бы был при- менен принцип, на основе которого ранее был разрешен конфликт в матрице, пред- ставленной в табл. 5.7, то это привело бы к тому, что для оператора входного язы- ка: if (а<0) while (а<10) а:=а+1 else а:=1: синтаксический анализатор выдавал бы сообщение об ошибке, что не соответствует истине, а потому недопустимо (именно для того, чтобы избежать этой проблемы, в синтаксис входного языка примера выполнения работы автором было добавле- но ключевое слово do). В таком случае проблематично выполнить преобразования грамматики и привес- ти ее к виду грамматики операторного предшествования без добавления в язык новых ключевых слов. Поскольку приведенный выше синтаксис оператора while соответствует языкам С и C++, можно проиллюстрировать, как указанная про- блема решается в этих языках [13, 25,32,39]. Тогда в грамматику надо включить сразу два новых нетерминальных символа (обозначим их Р и R), а блок правил грамматики G для нетерминальных символов L и О будет выглядеть следующим образом: L-*P\L;P\L; О -э if (В) О; else Р | if( В) R else Р | if( В) Р | while(B) Р | а:=Е R —> begin L end P-*O\R И показанный выше оператор будет выглядеть так: If (а<0) while (а<10) а:=а+1; else а:=1; В языках С и C++ операторным скобкам begin и end соответствуют лексемы { и }, а оператор присваивания обозначается одним символом: =. Но суть подхода этот пример иллюстрирует верно: в этих языках для условного оператора правила раз- личны в зависимости от того, входит в него составной оператор или одиночный оператор (точка с запятой ставится перед else для одиночных операторов в отли- чие от языка Pascal, где этой проблемы нет, так как конфликт между then и else может быть разрешен указанным выше способом, как в табл. 5.7). Желающие мо- гут построить для такого языка матрицу операторного предшествования и убе- диться, что она строится без конфликтов. Построение остовной грамматики После того как заполнена матрица операторного предшествования, на основе ис- ходной грамматики G можно построить остовную грамматику G'({prog,end.,if, else,begin,end,while,do,or,xor,and,not,<,>,=,<>,(,),-,+,um,a,c,{£},₽',E) с правилами P': E —> prog E end. — правило № 1 E —> E | E;E | E; — правила № 2,3 и 4
Пример выполнения курсовой работы 157 Е —> if(E) Е else Е | if(Е) Е | begin Е end | while(E)do Е | а:=Е — правила № 5-9 Е —> Е or Е | Е xor Е | Е — правила № 10, 11 и 12 Е —> Е and Е | Е — правила № 13 и 14 Е —> Е<Е | Е>Е | Е=Е | Е<>Е | (Е) | not(E) — правила № 15-20 Е —> Е-Е | Е+Е | Е — правила № 21, 22 и 23 Е —> um Е | Е — правила № 24 и 25 Е —> (Е) | а | с — правила № 26, 27 и 28 Всего имеем 28 правил. Жирным шрифтом в грамматике и в правилах выделены терминальные символы. При внимательном рассмотрении видно, что в остовной грамматике неразличи- мы правила 2, 12, 14, 23 и 25, а также правила 19 и 26. Но если первая группа пра- вил не имеет значения, то во втором случае у распознавателя могут возникнуть проблемы, связанные с тем, что некоторые ошибочные входные цепочки он будет считать допустимыми (например оператор а:=(а ог Ь);, который во входном язы- ке недопустим). Это связано с тем, что круглые скобки определяют приоритет как логических, так и арифметических операций, и хотя они несут одинаковую син- таксическую нагрузку, распознаватель должен их различать, поскольку семанти- ка этих скобок различна. Для этого дополним остовную грамматику еще одним нетерминальным символом В, который будет обозначать логические выражения. Подставив этот символ в соответствующие правила, получим новую остовную грамматику G»({prog,end.,if,else,begin,end,while,do,or,xor,and,not,<,>,=,<>,(,),- ,+,шп,а,с,;,:=}, {Е,В},Р»,Е) с правилами Р»: Е —> prog Е end. — правило № 1 Е —> Е | Е;Е | Е; — правила № 2-4 Е —> if(B) Е else Е | if(B) Е | begin Е end | while(B)do Е | а:=Е — правила № 5-9 В —> В ог В | В хог В | В — правила № 10-12 В —> В and В | В — правила N_> 13 и 14 В —> Е<Е | Е>Е | Е=Е | Е<>Е | (В) | not(B) — правила № 15-20 Е —> Е-Е | Е+Е | Е — правила № 21-23 Е —> um Е | Е — правила № 24 и 25 Е —> (Е) | а | с — правила № 26-28 После выполнения всех преобразований можно приступить к реализации синтак- сического распознавателя. Реализация синтаксического распознавателя Для реализации синтаксического распознавателя воспользуемся программными модулями, созданными при выполнении лабораторной работы № 3. Модуль SyntSymb (листинг П3.7, приложение 3), который реализует функциониро- вание алгоритма «сдвиг-свертка» для грамматик операторного предшествования, можно использовать, не внося в него никаких изменений, так как он не зависит от входного языка. Требуется перестроить только модуль SyntRule, внеся в него новые правила грамматики и новую матрицу операторного предшествования. Полученный
158 Курсовая работа в результате программный модуль представлен в листинге П3.6 в приложении 3 (обратите внимание на функцию MakeSymbolStr, которая возвращает имена нетер- минальных символов для правил остовной грамматики!). На этом построение синтаксического распознавателя закончено. Структуры дан- ных, используемые этим распознавателем и порождаемые в результате его рабо- ты, были рассмотрены при выполнении лабораторной работы № 3. Внутреннее представление программы и генерация кода Выбор форм внутреннего представления программы В качестве формы внутреннего представления программы будут использоваться триады. Преимущества и недостатки триад были рассмотрены ранее (при выпол- нении лабораторной работы № 4). В данном случае в пользу выбора триад гово- рят два определяющих фактора: □ для работы с триадами уже имеются необходимые структуры данных (соот- ветствующие программные модули созданы при выполнении лабораторной работы №4); . □ алгоритмы оптимизации, которые предполагается использовать, основаны на внутреннем представлении программы в форме триад. В данной работе создается простейший компилятор, поэтому другие формы внут- реннего представления программы не понадобятся. Результирующая программа будет порождаться на языке ассемблера на самой последней стадии компиляции, ее внутреннее хранение и обработка не предусмотрены. Описание используемого метода порождения результирующего кода Для порождения результирующего кода будет использоваться рекурсивный ал- горитм порождения списка триад на основе дерева синтаксического разбора. Схе- мы СУ-перевода для такого алгоритма были рассмотрены ранее (при выполне- нии лабораторной работы №4). В данном входном языке мы имеем следующие типы операций: □ логические операции (or, xor, and и not); □ операции сравнения (<, >, = и <>); □ арифметические операции (сложение, вычитание, унарное отрицание); □ оператор присваивания; □ полный условный оператор (if... then ... else ...) и неполный условный опе- ратор (if... then...); □ оператор цикла с предусловием (while(...)do...); □ операции, не несущие смысловой нагрузки, а служащие только для создания синтаксических конструкций исходной программы (заголовок программы, операторные скобки begin...end, круглые скобки и точка с запятой).
Пример выполнения курсовой работы 159 Схемы СУ-перевода для арифметических операций (которые являются линей- ными операциями), оператора присваивания и условных операторов были по- строены при выполнении лабораторной работы № 4. Здесь их повторять не будем. Схему СУ-перевода для оператора цикла с предусловием построим аналогично схе- мам СУ-перевода для условных операторов (которые были приведены на рис. 4.1 в лабораторной работе № 4). Генерация кода для цикла с предусловием выполняется в следующем порядке: □ Порождается блок кода№ 1, вычисляющий логическое выражение, находя- щееся между лексемами while ( (первая и вторая нижележащие вершины) и ) (четвертая нижележащая вершина) — для этого должна быть рекурсивно вы- звана функция порождения кода для третьей нижележащей вершины. □ Порождается команда условного перехода, которая передает управление в за- висимости от результата вычисления логического выражения: в начало блока кода № 2, если логическое выражение имеет ненулевое зна- чение; в конец оператора, если логическое выражение имеет нулевое значение. □ Порождается блок кода № 2, соответствующий операциям после лексемы do (пятая нижележащая вершина) — для этого должна быть рекурсивно вызвана функция порождения кода для шестой нижележащей вершины. □ Порождается команда безусловного перехода в начало блока кода № 1. Схема СУ-перевода для оператора цикла с предусловием представлена на рис. 5.2. Блок кода № 1 между лексемами while (и) do Триада if (•,♦> Блок кода Ns 2 после лексемы do Триада jmp (1, •)- Оператор цикла с предусловием Рис. 5.2. Схема СУ-перевода для оператора цикла с предусловием Таким образом, для реализации оператора цикла достаточно иметь те же типы триад, которые необходимы для реализации условных операторов: □ 1Г(<операнд1>,<операнд2>) — триада условного перехода; □ JMP (1. <операнд2>) — триада безусловного перехода. Смысл операндов для этих триад был описан при выполнении лабораторной работы № 4.
160 Курсовая работа Отдельно следует остановиться на генерации кода для операций сравнения и логи- ческих операций. При выполнении лабораторной работы № 4 логические опера- ции рассматривались как линейные операции и код для них строился соответству- ющим образом (аналогично коду для арифметических операций). Иной подход тогда не был возможен, поскольку тогда речь шла о побитовых логических операциях над целыми числами. Однако в данном случае во входном языке логические операции выступают как операции булевой алгебры, которые выполняются только над двумя значениями: «истина» (1) и «ложь» (0). Исходными данными для них служат операции срав- нения, результатом которых тоже могут быть только два указанных значения (кон- станты типа «истина» (TRUE) и «ложь» (FALSE) во входном языке отсутствуют, но даже если бы они и были, суть дела это не меняет). При таких условиях возможно иное вычисление логических выражений, поскольку нет необходимости выпол- нять все операции: □ для операции OR нет необходимости вычислять выражение, если один из опе- рандов TRUE, поскольку вне зависимости от другого операнда результат будет всегда TRUE; □ для операции 0R нет необходимости вычислять выражение, если один из опе- рандов FALSE, поскольку вне зависимости от другого операнда результат будет всегда FALSE. Рассмотрим в качестве примера фрагмент кода для условного оператора: If (a<b or а<с and b<c) a:=0 else a:=l; При генерации кода для операций сравнения и логических операций как для ли- нейных операций получим фрагмент последовательности триад: 1: < (а. Ь) 2: < (а. с) 3: < (Ь. С) 4: and (А2. А3) 5: or (*1. *4) 6: if (А5. *9) 7: := (а. 0) 8: jmp (1. ж10) 9: := (а. 1) Если же использовать свойства булевой алгебры, то можем получить следующий фрагмент последовательности триад: 1: < (а, Ь) 2: ifOl ГЗ. *7) 3: < (а. с) 4: ifOl Г9. А5) 5: < (b. С) б: IfOl Г9. ж7) 7: := (а. 0)
^lataHaus^. Пример выполнения курсовой работы 161 8: jmp (1. *10) 9: := (а. 1) Триада условного перехода IF01 здесь имеет следующий смысл: 1Г01(<операнд1>, <операнд2>) передает управление на триаду, указанную первым операндом, если предыдущая триада имеет значение 0 («Ложь»), иначе — передает управление на триаду, указанную вторым операндом. Во втором варианте кода при том же количестве построенных триад в зависимо- сти от значений переменных код будет в ряде случаев выполнять существенно мень- ше операций сравнения, чем в первом варианте, где при любых условиях выполня- ются все три операции. Правда, второй вариант кода содержит существенно боль- ше операций передачи управления, что несколько снижает его эффективность на современных процессорах (передача управления нарушает конвейерную обработ- ку данных, чего не происходит при линейной последовательности операций). Разница в эффективности выполнения кода не столь велика, и ею можно было бы пренебречь, если бы операции сравнения не содержали вложенных операций. Например, при порождении кода для оператора по второму варианту: if (a<b or Fl(a)<c and b<c) a:=0 else a:=l: функция Fl не будет вызвана, если выполняется условие а < Ь, а это уже принци- пиально важно. Еще один пример: if (а>0 and М[а]<>0) М[а]:=0; также показывает преимущества второго варианта порождения кода. Если для этого фрагмента построить код по первому варианту, то вычисление выражения М[а] о 0 может привести к выходу за границы массива М и даже к нарушению ра- боты программы при отрицательных значениях переменной а, хотя в этом нет никакой необходимости — после того как не выполняется условие а > 0, проверя- ющее левую границу массива М, нет надобности обращаться к условию М[а] о 0. При порождении кода по второму варианту этого не произойдет, и данный опера- тор будет выполняться корректно. Для того чтобы порождать код по второму варианту, схема СУ-перевода для ло- гических операций и операций сравнения должна зависеть от вышележащих уз- лов синтаксического дерева — от вышележащих узлов ей в качестве параметров должны передаваться адреса передачи управления для значений «истина» и «ложь». Будем считать, что рассмотренные далее схемы СУ-перевода получают на вход два аргумента: адрес передачи управления для значения «истина» — At и адрес передачи управления для значения «ложь» — А2. Схема СУ-перевода для операций сравнения будет выглядеть следующим образом: 1. Порождается блок кода для операции сравнения по схеме СУ-перевода для линейной операции. 2. Порождается триада IF01, первый аргумент которой — адрес А2, а второй аргу- мент — адрес Ар 6 Зак 68
162 Курсовая работа Схема СУ-перевода для операции AND будет выглядеть следующим образом: 1. Порождается блок кода № 1 для первого операнда. Для этого рекурсивно вы- зывается функция порождения кода для первой нижележащей вершины, в ка- честве первого аргумента ей передается адрес блока кода № 2, а в качестве вто- рого аргумента — адрес А2. 2. Порождается блок кода № 2 для второго операнда. Для этого рекурсивно вы- зывается функция порождения кода для третьей нижележащей вершины, в ка- честве первого аргумента ей передается адрес At, а в качестве второго аргумен- та — адрес А2. Схема СУ-неревода для операции OR будет выглядеть следующим образом: 1. Порождается блок кода № 1 для первого операнда. Для этого рекурсивно вы- зывается функция порождения кода для первой нижележащей вершины, в ка- честве первого аргумента ей передается адрес Аь а в качестве второго аргумен- та — адрес блока кода № 2. 2. Порождается блок кода № 2 для второго операнда. Для этого рекурсивно вы- зывается функция порождения кода для третьей нижележащей вершины, в ка- честве первого аргумента ей передается адрес Аь а в качестве второго аргумен- та — адрес А2. Схема СУ-перевода для операции NOT будет выглядеть следующим образом: Порождается блок кода для единственного операнда. Для этого рекурсивно вызы- вается функция порождения кода, в качестве первого аргумента ей передается ад- рес А2, а в качестве второго аргумента — адрес Ai (аргументы меняются местами). Видно, что при использовании таких схем СУ-перевода логические операции фак- тически не порождают кода, а лишь определяют порядок вызова операций срав- нения и ход передачи управления между ними. В приведенных описаниях схем есть одно логическое противоречие: необходимо передавать в качестве аргумента функции адрес блока кода, который еще не построен. Но при реализации этот момент можно обойти: например, передавать аргументом какое-то фиктивное зна- чение (скажем, отрицательное число), а потом, после построения блока кода, ме- нять его на известном интервале списка триад на вновь построенный адрес1. Такой подход потребует изменить схемы СУ-перевода для условных операторов и для оператора цикла. Для условных операторов генерация кода может выполняться в следующем по- рядке: 1. Порождается блок кода№ 1, вычисляющий логическое выражение, находя- щееся между лексемами if (первая нижележащая вершина) и then (третья ни- жележащая вершина). Для этого должна быть рекурсивно вызвана функция порождения кода для второй нижележащей вершины, в качестве первого ар- гумента ей передается адрес блока кода № 2, а в качестве второго аргумента — ' Эта же проблема всегда возникает при порождении кода для условных операторов и операторов цик- ла — желающие могут посмотреть в листинге П3.12 в приложении 3, как она решается на практике.
Пример выполнения курсовой работы 163 адрес блока кода № 3 (для полного условного оператора) или адрес конца опе- ратора (для неполного условного оператора). 2. Порождается блок кода № 2, соответствующий операциям после лексемы then (третья нижележащая вершина) — для этого должна быть рекурсивно вызва- на функция порождения кода для четвертой нижележащей вершины (оба ар- гумента нулевые). 3. Для полного условного оператора порождается команда безусловного перехо- да в конец оператора. 4. Для полного условного оператора порождается блок кода № 3, соответствую- щий операциям после лексемы else (пятая нижележащая вершина) — для это- го должна быть рекурсивно вызвана функция порождения кода для шестой нижележащей вершины (оба аргумента нулевые). Генерация кода для цикла с предусловием выполняется в следующем порядке: 1. Порождается блок кода№ 1, вычисляющий логичесЕ<ое выражение, находя- щееся между лексемами while ( (первая и вторая нижележащие вершины) и ) (четвертая нижележащая вершина). Для этого должна быть рекурсивно вы? звана функция порождения кода для третьей нижележащей вершины, в каче- стве первого аргумента ей передается адрес блока кода № 2, а в качестве вто- рого аргумента —адрес конца оператора. 2. Порождается блок кода№ 2, соответствующий операциям после лексемы do (пятая нижележащая вершина) — для этого должна быть рекурсивно вызвана функция порождения кода для шестой нижележащей вершины (оба аргумен- та нулевые). 3. Порождается команда безусловного перехода в начало блока кода № 1. Современные компиляторы порождают различный код для логических операций: □ для побитовых операций порождается код как для линейных операций; □ для операций со значениями булевой алгебры по умолчанию порождается код по рассмотренной выше схеме (вычисление операции прерывается, как только се значение становится известным). Например, в языке Object Pascal код, порождаемый для операций and, ог, хог и not, зависит от типов операндов (являются ли они логическими или целочисленны- ми), а в языках С и C++ логические и побитовые операции даже обозначаются разными знаками операций. При этом в современных компиляторах существует команда, позволяющая разработчику отключить порождеЕше «сокращенного» кода (обычно она называется «Complete Boolean evaluations») — тогда для всех логи- ческих выражений порождается полный линейный код. В данной работе будут использованы схемы порождения линейного кода для опе- раций сравнения и логических операций. Это допустимо, поскольку входной язык не допускает вложенных вызовов функций, обращений к массивам и других опе- раций, которые могли бы приводить к побочным эффектам. Кроме того, и это
164 Курсовая работа наиболее важно, в работе должны быть проиллюстрированы методы оптимиза- ции, работающие для линейных участков программы, поэтому желательно мак- симально увеличить количество линейных участков. При наличии конвейерной обработки команд в линейных процессорах на эффективности кода такой подход существенно не отразится. Линейное порождение кода для логических операций существенно проще в реа- лизации, и потому автор рекомендует именно его для выполняющих курсовую работу (результатом курсовой работы все-таки является простейший, а не про- мышленный компилятор). СОВЕТ------------------------------------------------------------------- Желающие могут попробовать свои силы в порождении эффективного кода для логических операций на основе предложенных выше схем СУ-перевода и имеющихся в приложении 3 струк- тур данных и функций. Реализация такого подхода рассматривается как дополнительный бонус для выполняющего курсовую работу студента (по согласованию с преподавателем). Генерация кода для сокращенного вычисления логических выражений подробно рассмотрена в [2]. Реализация генератора триад Все возможные типы триад перечислены в модуле TrdType (листинг П3.8, прило- жение 3). Структуры данных, использованные в лабораторной работе № 4, не зависят от входного языка. Поэтому имеет смысл использовать их для генерации триад в кур- совой работе. Эти структуры данных описаны в модуле Triads (листинг П3.10, приложение 3). Генератор триад также реализован на базе модуля, который был использован для генерации триад в лабораторной работе № 4. В данный модуль были внесены из- менения в соответствии с изменившимся синтаксисом входного языка, добавле- ны новые линейные операции (арифметические операции и операции сравнения), а также добавлена реализация схемы СУ-перевода для оператора цикла (которая была представлена на рис. 5.2). Для проверки заданных семантических ограничений в генератор триад добавле- ны следующие проверки: □ при определении имени операнда любой линейной операции проверяется, что имя не совпадает с недопустимым именем «Result»; □ при определении имени операнда операции присваивания проверяется, что имя не совпадает с недопустимыми именами «InpVar» и «Result». Если хотя бы одна из этих проверок не выполняется, выдается сообщение о нали- чии семантической ошибки в программе (присваивание значения константе в дан- ном входном языке обнаруживаете# как синтаксическая ошибка). Текст полученного программного модуля TrdMake приведен в листинге П3.12, при- ложение 3.
Пример выполнения курсовой работы 165 Генератор ассемблерного кода Порождение ассемблерного кода для триад не представляет проблем. Соответ- ствующие алгоритмы реализованы в модуле TrdAsm (листинг П3.13, приложение 3). Этот модуль зависит от внутреннего представления программы (от типов триад) и от целевой вычислительной системы (выходного языка). Главная задача за- ключается в том, чтобы распределить память и регистры процессора для хране- ния промежуточных результатов триад в тех случаях, когда эти результаты ис- пользуются в качестве операнда в других триадах. Такое распределение можно выполнить элементарным образом, если с каждой триадой связать временную переменную, имя которой можно дать в зависимости от порядкового номера триады. Тогда после вычисления триады результат вы- числения записывается в эту переменную, а если он будет востребован позже, то читается из этой переменной. Однако такое распределение будет чрезвычайно неэффективно хотя бы потому, что оно потребует столько же временных переменных, сколько в списке имеется триад, порождающих результаты. В то же время, нет необходимости хранить ре- зультаты вычисления всех триад — например, этого не надо делать в том случае, если результат вычисления триады используется только в следующей по списку триаде и более нигде не требуется. Поэтому простейшее распределение можно улучшить, если пометить в списке такие триады, результат вычисления которых используется где бы то ни было, кроме следующих по списку триад, и временные переменные создавать только для этих триад. Но эффективность алгоритма распределения временных переменных и регист- ров процессора можно еще увеличить, если принять во внимание область дей- ствия каждой триады. Областью действия триады будем считать фрагмент спис- ка триад от порядкового номера триады, следующей заданной триадой, до поряд- кового номера триады, где последний раз используется ее результат. Например, последовательности операторов: d := а + b + с: с = d *(а + Ь); а := d *(а + b) + 1: будет соответствовать последовательность триад: 1: + (а. Ь) 2: + (*1. с) 3: := (d. Л2) 4: * (d. А1) 5; := (с. Л4) 6: + 04. 1) 7: := (а. ^6) Область действия для каждой триады в этой последовательности показана на рис. 5.3. Если отбросить триады, область действия которых не распространяется дальше одной триады (как было сказано выше, для них не требуется хранение промежу-
166 Курсовая работа точных результатов), то по рис. 5.3 видно, что для данной последовательности триад достаточно одной временной переменной, в которую сначала необходимо занести значение триады № 1, а затем — значение триады № 4. Если пользоваться рассмотренным ранее алгоритмом, то потребовалось бы как минимум две времен- ных переменных. 1 2 3 4 5 6 7 + (а. Ь) + Г1.с) (d А2) * (d.Al) Область действия триады № 2 Область действия триады № 1 := (С.А4) + Г4.1) := (а.А6) Область действия триады № 4 Область действия триады № 6 Рис. 5.3. Области действия триад в списке триад Область действия каждой триады можно легко определить, если просматривать список триад с конца: тогда первая же встреченная ссылка на триаду будет макси- мальной границей ее области действия, а номер триады будет определять мини- мальную границу ее области действия. Именно такой алгоритм распределения временных переменных и регистров реа- лизован в функции MakeRegisters в модуле TrdAsm. Эта функция просматривает список триад с конца и распределяет регистры по порядку начиная от первого упоминания каждой триады. Номер закрепленного регистра записывается в ин- формационное поле каждой триады (если это ноле равно 0, считается, что нет не- обходимости хранить промежуточный результат вычисления триады). Минималь- ная граница области действия триады, в пределах которой регистр не может быть распределен повторно, запоминается в специальном списке регистров (в функ- ции этот список представлен переменной listReg). Количество регистров, упомя- нутых в нем, и будет равно необходимому количеству регистров для вычисления списка триад. Генератор ассемблерного кода ориентирован на процессоры типа Intel 80386 и бо- лее поздних модификаций в защищенном режиме работы. В этом режиме в про- цессоре доступно шесть регистров общего назначения по 32 бит каждый [41,44]: □ еах; □ ebx; □ есх; □ edx; □ esi; □ edi. Регистр esp используется как указатель стека, а регистр ebp — как базовый указа- тель стека (хранение временных переменных в стеке с использованием двух ре-
^latattaus,^. Пример выполнения курсовой работы 167 гистров описано в разделе, посвященном организации дисплеев памяти процедур и функций в [2, 3, 7]). С учетом того, что регистр еах необходим для организации вычислений, остается пять регистров, доступных для хранения промежуточных результатов вычисле- ний триад. Если алгоритму требуется больше регистров, то остальные временные результаты размещаются во временных переменных, которые генератор кода в свою очередь размещает в стеке. Предложенный алгоритм правильно определяет минимально необходимое коли- чество регистров процессора и временных переменных, необходимых для хране- ния промежуточных результатов вычисления триад. Однако доступные регистры он распределяет произвольным образом (каждая триада получает для хранения своего результата первый попавшийся свободный регистр). Логично было бы в первую очередь выделять регистры для тех триад, чьи результаты используют- ся наиболее часто, а для хранения результатов других триад использовать вре- менные переменные, поскольку доступ к регистру осуществляется быстрее, чем к области памяти, в которой хранится переменная. Алгоритмы такого распреде- ления существуют, но в данном случае в них нет необходимости, поскольку для простейшего компилятора, обрабатывающего незначительные по объему входные программы, не требуется столь сложная подготовка результирующего кода. После того как регистры распределены, остается построить ассемблерный код. Для этого для каждой триады строится соответствующий ей фрагмент ассемб- лерного кода, и все построенные фрагменты объедкшяются в общую последова- тельность команд результирующей программы по порядку следования триад в списке. Для выполнения всех операций и хранения их результатов в пределах одной триа- ды будем использовать регистр аккумулятора — еах. Кроме того, что это наглядно и удобно, в процессорах серии Intel 80x86 некоторые команды с этим регистром занимают меньше памяти и выполняются быстрее, чем команды с другими регист- рами (а в ряде команд этот регистр является единственно возможным) [41,44]. Порождение ассемблерного кода по списку триад выполняется функцией MakeAsmCode из модуля TrdAsm (листинг П3.13, приложение 3). Для унарных линейных операций последовательность действий при генерации ассемблерного кода такова: 1. Запоминается имя операнда. Для переменных именем операнда является имя переменной, для константы — значение константы, а для ссылки на другую триаду, кроме предыдущей, — имя регистра или временной переменной, в ко- торой хранится результат вычисления триады (для предыдущей триады имя операнда пустое). 2. Если имя операнда не пустое, то операнд надо загруз итьв регистр еах. Для этого порождается команда mov, но если операнд — результат вычисления предыду- щей триады (имя операнда пустое), то загружать в еах его не нужно, так как он уже находится там после вычисления триады, и никакая команда на этом шаге не порождается.
168 Курсовая работа 3. Порождается команда, соответствующая унарной операции над регистром, еах (в данном результирующем языке: not — для логического отрицания; neg — для унарного арифметического минуса). 4. Если одной команды недостаточно, порождается еще одна команда (в данном случае для логического отрицания требуется еще команда and). 5. Если для триады требуется сохранить промежуточный результат, порождает- ся команда mov, которая сохраняет результат из регистра еах в регистр или вре- менную переменную, связанную с триадой. Для бинарных линейных операций последовательность действий при генерации ассемблерного кода такова: 1. Запоминаются имена обоих операндов. Для переменных именем операнда Яв- ляется имя переменной, для константы — значение константы, а для ссылки на другую триаду, кроме предыдущей — имя регистра или временной перемен- ной, в которой хранится результат вычисления триады (для предыдущей три- ады имя операнда пустое). 2. Если имя одного из операндов пустое (операнд получен при вычислении пре- дыдущей триады), то нет необходимости загружать его в регистр еах, иначе по- рождается команда mov, которая загружает первый операнд в регистр еах. 3. Порождается команда, соответствующая бинарной операции над регистром еах. Если имя второго операнда пустое, то первый операнд триады становится вто- рым операндом команды, иначе — второй операнд триады становится вторым операндом команды. 4. Если одной команды недостаточно, порождается еще одна (в данном резуль- тирующем языке это необходимо только для команды вычитания sub в том случае, если операнды менялись местами — чтобы получить верный резуль- тат, требуется еще команда neg). 5. Если для триады требуется сохранить промежуточный результат, порождает- ся команда mov, которая сохраняет результат из регистра еах в регистр или вре- менную переменную, связанную с триадой. Определение имени операнда выполняется вспомогательной функцией GetOpName. Порождение ассемблерного кода выполняется функцией MakeOperl — для унар- ных операций, и функцией Маке0рег2 — для бинарных операций. Можно обратить внимание, что функция GetOpName проверяет имя переменной на совпадение его с предопределенным именем Compil eTest, и если имена совпадают, заменяет имя переменной на предопределенное имя Result. Эта проверка и подстановка — про- стейший пример модификации компилятором результирующего кода в зависи- мости от семантических соглашений (предопределенное имя Result всегда обо- значает результат функции в выходном языке). В промышленных компиляторах такие модификации, как правило, связаны с неявными преобразованиями типов данных, принятыми во входном языке. Последовательность порождения ассемблерного кода для триад, представляющих линейные операции, практически не зависит от внутреннего представления про-
^atattaus^. Пример выполнения курсовой работы 169 граммы и может быть использована для любых типов триад, соответствующих линейным операциям (от типа триады зависит только тип порождаемой ассемб- лерной команды). Для триад присваивания значений и для триад безусловного перехода (JMP) по- рождение команд элементарно просто и не требует пояснений. Для операций сравнения интерес представляет получение результата, поскольку при выполнении команд сравнения в различных процессорах результатом, как правило, являются биты в специальном регистре — регистре флагов. Биты в ре- гистре флагов могут быть непосредственно использованы в командах условных переходов, и если компилятор порождает код для логических операций, основан- ный на порядке их вычисления (неполное вычисление логических выражений было рассмотрено ранее), то он может этим воспользоваться. Но когда операции сравнения обрабатываются как линейные операций, нужно загрузить результат из регистра флагов в регистр общего назначения. Для этого также можно исполь- зовать условные переходы, например для триады типа: 1: < (а. Ь) можно построить последовательность команд вида: mov еах. а cmp еах. b jl @М1_1 хог еах. еах jmp @М1_2 @М1_1: хог еах. еах i пс еах @М1_2: которая будет обеспечивать запись в регистр аккумулятора (еах) логического ре- зультата операции сравнения (0 — «ложь», 1 — «истина»). Однако, как уже было сказано, большое количество операций передачи управле- ния не способствует эффективности выполнения г ротраммы. К тому же рассмот- ренный выше подход порождает много лишних команд. Как правило, в процессо- рах есть команды, позволяющие организовать либо прямой обмен между регист- ром флагов и регистром аккумулятора, либо обмен данными через стек. В процессорах типа Intel 80x86 это команды групп»! set<*>, где <*> зависит от не- обходимого флага [41,44]. Тогда для того же самого примера порядок команд бу- дет иным: mov еах. а cmp еах. b set! al and еах. 1 В предлагаемом генераторе кода используется именно такой подход. А в осталь- ном порождение кода для операций сравнения не огл и чается от порождения кода для прочих линейных операций.
170 Курсовая работа Еще несколько слов необходимо сказать о триаде условного перехода IF. Для нее ситуация иная, чем для операций сравнения — чтобы выполнить условный пере- ход, надо установить регистр флагов на основе регистра аккумулятора. Для этого можно воспользоваться простейшей командой процессора для сравнения регист- ра аккумулятора с ним самим, например: test еах. еах однако эффективность результирующего кода можно увеличить, если учесть, что триаде IF всегда предшествует либо триада сравнения, либо триада логической операции, а следовательно, при выполнении кода, порожденного для этих триад, флаги уже будут установлены соответствующим образом. Тогда нет необходимо- сти порождать дополнительную команду для установки флагов и для триады IF достаточно построить только команду условного перехода по флагу «ноль» (в процессорах типа Intel 80x86 это команда jz). Но система команд процессоров типа Intel 80x86 имеет одну особенность: команды условного перехода могут передавать управление не далее, чем на 128 байт вперед или назад от места команды. В момент генерации кода для триады IF, как правило, не известно, будет ли передача управления происходить в пределах 128 байт кода или выйдет за рамки данного ограничения. Чтобы обойти это ограничение, переда- чу управления можно организовать с помощью двух команд: сначала команда ус- ловного перехода по обратному условию «не ноль» передает управление на локаль- ную метку, а потом команда безусловного перехода передает управление па требу- емую «дальнюю» метку: jnz @Fx jmp @Мх Fx: ... Здесь @Fx — локальная («обходная») метка, а @Мх — та метка, на которую необхо- димо передать управление. Именно такой подход реализован в разработанном генераторе ассемблерного кода1. Есть еще одна особенность в генерации кода для триады IF: поскольку в разрабо- танном генераторе триад операции сравнения и логические операции обрабатыва- ются как линейные операции, а потому могут быть оптимизированы, первый опе- ранд триады может оказаться константой. При этом триада IF будет выполнять не условный, а безусловный переход на одну из частей условного оператора в зависи- мости от значения этого операнда. Например, в последовательности операторов: а := 1; if (а<0) b:=0 else Ь:=Г, первая часть условного оператора (Ь:=0) никогда не будет выполнена и в резуль- тате выполнения оптимизации это станет очевидным (первый операнд триады IF * На самом деле эту проблему можно было бы оставить без внимания, поскольку ассемблерный код порождается для системы программирования Delphi 5, которая сама умеет обрабатывать такие си- туации. Но в примере выполнения работы целенаправленно обращается внимание па эту пробле- му, чтобы проиллюстрировать, какие ситуации могут возникать при порождении результирующего кода для различных целевых вычислительных систем.
^laiattaus^i Пример выполнения курсовой работы 171 будет равен 0). Генератор ассемблерного кода порождает соответствующий код: если первый операнд равен 0 — команду безусловного перехода; если первый опе- ранд не равен 0, никаких команд для триады IF вообще не порождается. Можно отметить, что в этом случае вообще нет необходимости порождать код для одной из ветвей условного оператора, что сократит объем результирующего кода, но такая оптимизация требует существенных модификаций всего списка триад, что не предусмотрено в данном примере выполнения работы. Описание используемого метода оптимизации Машинно-независимые методы оптимизации Оба используемых машинно-независимых метода оптимизации — метод свертки объектного кода и метод исключения лишних операций — были описаны при вы- полнении лабораторной работы № 4, поэтому нет необходимости описывать их здесь повторно. Эти методы оптимизации не зависят ни от входного, ни от ре- зультирующего языка, а потому реализующие их алгоритмы, разработанные при выполнении лабораторной работы № 4, могут быть без модификаций использо- ваны в курсовой работе. Функции, осуществляющие оба метода машинно-независимой оптимизации, ре- ализованы в модуле TrdOpt (листинг П3.11, приложение 3). Для алгоритма опти- мизации методом свертки объектного кода необходимо вычислять значения три- ад, которые могут входить в состав линейных участков кода. Типы таких триад, а также функции вычисления их значений зависят от входного языка (поэтому при выполнении лабораторной работы № 4 они были выделены в отдельный мо- дуль). Вычисления триад для алгоритма свертки объектного кода для курсовой работы реализованы в модуле TrdCal с (листинг П3.9, приложение 3). Кроме этих двух методов при генерации результирующего кода реализован еще один простейший метод оптимизации, который зависит от семантики входного языка. Этот метод основан на особенностях выполнения арифметических и логи- ческих операций. Учитываются следующие особенности: □ для логической операции 0R нет необходимости порождать код, выполняющий эту операцию, если один из операндов равен 0; □ для операции AND нет необходимости порождать код, выполняющий эту опера- цию, если один из операндов равен 1; □ для арифметической операции сложения нет необходимости порождать код, выполняющий эту операцию, если любой из операндов равен 0, а для арифме- тической операции вычитания — если второй операнд равен 0. В отличие от двух ранее рассмотренных методов оптимизации, эта оптимизация выполняется не над внутренним представлением программы (триадами), а над результирующей программой при генерации ассемблерного кода. Поэтому соот- ветствующие действия реализованы в функции MakeOpcode в модуле TrdAsm (лис- тинг П3.13, приложение 3).
172 Курсовая работа Машинно-зависимые методы оптимизации В качестве дополнительных возможностей в компиляторе, построенном в ходе выполнения примера курсовой работы, реализованы простейшие машинно-зави- симые методы оптимизации. Эти методы не претендуют ни на полноту, ни на су- щественное повышение эффективности результирующей программы, но на их основе можно показать, как выполняется машинно-зависимая оптимизация. Реализованные методы машинно-зависимой оптимизации основаны на двух осо- бенностях системы команд процессоров типа Intel 80x86 [41,44]: 1. Особенность загрузки данных в регистр аккумулятора еах. 2. Особенность выполнения арифметических операций. В первом случае учитывается, что команда загрузки нулевого значения в регистр аккумулятора еах mov еах. О выполняется дольше и имеет большую длину, чем команда очистки регистра еах, которая может быть осуществлена с помощью операций хог (исключающее или) или sub (вычитание) над этим регистром процессора. Например: хог еах. еах Поэтому в тех случаях, когда в регистр аккумулятора еах требуется загрузить ну- левое значение, генератор ассемблерного кода порождает именно команду очист- ки регистра. Аналогично, если необходимо загрузить значение, равное 1, то по- рождается пара команд хог еах. еах inc еах а для значения -1 — пара команд хог еах. еах dec еах Оптимизация загрузки регистра аккумулятора выполняется при порождении ре- зультирующего кода. Она реализована в функции MakeMove в модуле TrdAsm (лис- тинг П3.13, приложение 3). Надо отметить, что эта оптимизация существенно зависит и от целевой вычисли- тельной системы (поскольку она использует особенности системы команд про- цессоров типа Intel 80x86), и от результирующего языка (например, если бы опе- рандами были однобайтовые величины, эффективность такой оптимизации была бы сомнительна). Во втором случае учитывается, что при выполнении операций сложения и вычи- тания в тех случаях, когда один из операндов равен 1 или-1, результирующий код будет более эффективным, если использовать ассемблерные команды увели- чения и уменьшения значения регистра на 1 (команды inc и dec): □ для операции сложения порождается команда inc вместо команды add, если один из операндов равен 1;
^ataftau^Jk Пример выполнения курсовой работы 173 □ для операции сложения порождается команда dec вместо команды add, если один из операндов равен -1; □ для операции вычитания порождается команда 1нс вместо команды sub, если второй операнд равен -1; □ для операции вычитания порождается команда dec вместо команды sub, если второй операнд равен 1. Оптимизация арифметических операций также происходит при генерации резуль- тирующего кода. Она реализована в функции MakeOpcode в модуле TrdAsm (лис- тинг П3.13, приложение 3). Надо отметить, что эта оптимизация меньше зависитот целевой вычислительной системы (поскольку практически во всех типах процессоров есть команды, уве- личивающие или уменьшающие значение регистра на 1) и совсем не зависит от результирующего языка. Машинно-зависимые методы оптимизации выполняются компилятором на эта- пе порождения результирующей программы. Причем функции генерации кода, упомянутые выше, сочетают в себе машинно-зависимую и машинно-независимую оптимизацию. Текст программы компилятора Полный текст всех модулей компилятора, созданного при реализации примера выполнения курсовой работы, приведен в Приложении 3. Те из этих модулей, которые це зависят от входного языка, были использованы ранее при выполне- нии лабораторных работ № 1 -4. Кроме того, модули можно найти в архиве, рас- полагающемся на веб-сайте издательства, в подкаталогах CURSOV и COMMON. Все функциональные модули и их назначение в работе были рассмотрены выше. Организация интерфейса с пользователем По заданию компилятор должен получать входные данные из командной строки (обработка командной строки описана далее). Дополнительно для созданного ком- пилятора реализован графический интерфейс с пользователем, аналогичный ин- терфейсу, использованному в лабораторных работах 2-4. Окно графического интерфейса открывается в том случае, когда командная строка не указана. Модуль, обеспечивающий интерфейс с пользователем (FormLab4), реализует гра- фическое окно TCursovForm на основе класса TFormбиблиотеки VCL. Он обеспечи- вает интерфейс средствами Graphical User Interface (GUI) в ОС типа Windows на основе стандартных органов управления из системных библиотек данной ОС. Этот модуль обеспечивает также обработку входной командной строки компилятора и включает в себя две составляющие: □ файл программного кода (файл FormLab4.pas); □ файл описания ресурсов пользовательского интерфейса (файл FormLab4.dfm). Более подробно принципы организации пользовательского интерфейса на осно- ве GUI и работа систем программирования с ресурсами интерфейса описаны
174 Курсовая работа в [3, 5-7]. Полный текст программного кода модуля интерфейса с пользователем приведен в листинге ПЗ. 14 в приложении 3. Описание ресурсов пользовательс- кого интерфейса, связанное с этим модулем, можно найти в архиве, располагаю- щемся на веб-сайте издательства, в файле FormLab4.dfm в подкаталоге CURSOV. Модуль FormLab4 построен на основе такого же модуля, который использовался для реализации интерфейса с пользователем в лабораторной работе № 4. Он со- держит все данные, управляющие и интерфейсные элементы, которые были ис- пользованы в лабораторных работах № 2-£. Такой подход оправдан, поскольку этапы компиляции при выполнении курсовой работы совпадают с этапами вы- полнения соответствующих лабораторных работ. Кроме органов управления, использованных в лабораторных работах № 2-4, ин- терфейсная форма, описанная в модуле FormLab4, содержит органы управления для генератора ассемблерного кода, которые созданы для курсовой работы: □ в многостраничной вкладке (PageControl 1) появилась новая закладка (SheetAsm) под названием «Команды»; □ на закладке SheetAsm расположены интерфейсные элементы: •ListAsm — список для вывода и просмотра порожденных ассемблерных команд; □ на первой закладке SheetFile («Исходный файл») появился дополнительный орган управления — флажок с двумя состояниями («пусто» или «отмечено»): CheckAsm — при включении этого флажка выполняется оптимизация результи- рующего кода, а при отключении — нс выполняется. Внешний вид новой закладки интерфейсной формы TCursovForm приведен на рис. 5.4. Рис. 5.4. Внешний вид пятой закладки интерфейсной формы для курсовой работы
NalaHausii^ Пример выполнения курсовой работы 175 Обработка входного файла в курсовой работе происходит в той же последователь- ности и в том же порядке, как это было описано при выполнении лабораторной работы № 4. Последним этапом, который отсутствовал в лабораторной работе, является этап порождения результирующего кода. На этом этапе выполняется следующая последовательность действий: □ вызывается функция MakeRegisters (модуль TrdAsm — листинг П3.13, приложе- ние 3), которая распределяет регистры процессора по списку триад, результа- том выполнения функции является количество необходимых временных пе- ременных для списка триад (если регистров процессора не хватило), это зна- чение запоминается; □ очищается содержимое списка ассемблерных команд ListAsm; □ в список ассемблерных команд записываются строки заголовка программы в со- ответствии с заданием; □ запоминается перечень всех идентификаторов программы (с помощью функ- ции IdentList из модуля FncTree — листинг П3.2, приложение 3); □ если перечень идентификаторов не пустой, то: в список строк записывается ключевое слово var; в список строк записываются все идентификаторы через запятую с указа- нием требуемого типа данных (integer); □ в список ассемблерных команд записываются строки заголовка функции Compil eTest в соответствии с заданием; □ если количество необходимых временных переменных больше нуля, то: в список строк в тело функции помещается ключевое слово var; за ключевым словом в списке строк записываются все имена временных переменных — таким образом временные переменные для хранения значе- ний триад становятся локальными переменными функции Compi 1 eTest и раз- мещаются в стеке; □ в список заносится заголовок ассемблерного кода (ключевые слова begin и asm и команда pushad для сохранения значений регистров процессора в сте- ке); □ вызывается функция MakeAsmCode (листинг П3.13, приложение 3), которая за- полняет список текстом ассемблерных команд; □ в список заносится конец ассемблерного кода (команда popad для восстановле- ния значений регистров процессора из стека и два ключевых слова end); □ в список помещаются строки тела главной программы в соответствии с зада- нием. В отличие от лабораторных работ вся обработка данных в курсовой работе выне- сена в отдельную функцию CompRun. Это сделано для того, чтобы для выполнения компиляции одна и та же функция вызывалась вне зависимости от того, как запу- щен компилятор — с командной строкой или без нее.
176 Курсовая работа Обработка командной строки Как требуется по заданию, созданный компилятор должен уметь работать с ко- мандной строкой. Для созданного в данном примере компилятора командная стро- ка должна иметь вид: <компилятор> <входной_файл> [<ключи>] где: <компилятор> — имя исполняемого файла компилятора; <входной_файл> — имя входного файла и путь к нему (если путь не указан, то ком- пилятор ищет файл в текущем каталоге), первый обязательный параметр команд- ной строки; <ключи> — необязательные параметры (ключи) запуска компилятора (второй и по- следующие параметры). Если входной файл не указан (нет ни одного параметра в командной строке), то открывается окно графического интерфейса с пользователем, которое было опи- сано выше. Иначе, если входной файл указан, компилятор читает исходную про- грамму из этого файла, обрабатывает ее, и если программа не содержит ошибок — помещает результаты в выходной файл, а сообщения об ошибках — в специаль- ный файл ошибок. После этого компилятор сразу же завершает свою работу (ни- какое окно на экране в этом случае не отображается). Имя выходного файла и имя файла для сообщений об ошибках компилятор определяет, исходя из указанных параметров (ключей запуска). Если указанный в строке запуска входной файл нс найден, то компилятор выдает сообщение об ошибке чтения файла и после этого открывает окно графического интерфейса с пользователем (как если бы имя файла не было указано). Проверка наличия параметров в командной строке запуска компилятора выпол- няется сразу при старте компилятора (функция FormCreate, модуль FormLab4, лис- тинг П3.14 в приложении 3). Для этого используется системная функция Param- Count из библиотеки языка Object Pascal, которая возвращает количество пара- метров в командной строке (0 — параметры отсутствуют). Для анализа параметров командной строки используется системная функция ParamStr из библиотеки язы- ка Object Pascal, которая возвращает строковое значение параметра по его поряд- ковому номеру в командной строке. При этом нулевым параметром считается имя исполняемого файла. Второй и последующий параметры в командной строке считаются ключами — необязательными параметрами запуска компилятора. Ключей в командной стро- ке может быть любое количество (в том числе может не быть ни одного ключа, в этом случае командная строка содержит только один параметр — имя входно- го файла). Ключи могут следовать в командной строке в любом порядке. Ключи определяют режим работы компилятора и некоторые условия компиляции. Каж- дый ключ является отдельным параметром. Ключи отделяются друг от друга про- белами (если ключ должен содержать пробелы внутри себя, его следует взять в двойные кавычки — — как это принято для параметров командных строк в ОС).
Пример выполнения курсовой работы 177 Каждый ключ должен начинаться с символа «-» (минус), за которым следует сим- вол ключа (строчная или прописная буква латинского алфавита), а за ним — па- раметр ключа, если требуется. Символы ключей имеют следующее значение (строчные и прописные буквы имеют одинаковое значение, поэтому здесь рассматриваются только прописные буквы): □ А — флаг оптимизации команд ассемблера, определяет, выполняется или нет оптимизация результирующего кода; за символом должно идти указание зна- чения флага: 1 — оптимизация выполняется (флаг включен); 0 — оптимизация не выполняется (флаг выключен), любой другой символ, следующий за символом А, воспринимается как 0; □ С — флаг свертки объектного кода, определяет, выполняется или нет оптими- зация списка триад методом свертки объектного кода; за символом должно идти указание значения флага: 1 — оптимизация выполняется (флаг включен); 0 — оптимизация не выполняется (флаг выключен), дюбой другой символ, следующий за символом С, воспринимается как 0; □ S — флаг исключения лишних операций, определяет, выполняется или нет оп- тимизация списка триад методом исключения лишних операций; за символом должно идти указание значения флага: 1 — оптимизация выполняется (флаг включен); 0 — оптимизация не выполняется (флаг выключен), любой другой символ, следующий за символом S, воспринимается как 0; □ 0 — имя результирующего файла, непосредственно за символом должно идти имя файла и путь к нему (если путь не указан, считается, что файл будет нахо- диться в текущем каталоге, откуда запущен компилятор); □ Е — имя файла с ошибками, непосредственно за символом должно идти имя файла и путь к нему (если путь не указан, считается, что файл будет находить- ся в текущем каталоге, откуда запущен компилятор). Если какой-то из ключей не указан, то компилятор принимает значение ключа по умолчанию. Для флагов установлены следующие значения по умолчанию: □ флаг оптимизации команд ассемблера — включен; □ флаг свертки объектного кода — включен; □ флаг исключения лишних операций — включен. Имя файла результирующей программы по умолчанию считается совпадающим с именем входного файла, но с расширением .asm. Имя файла с ошибками ком- пиляции по умолчанию также считается совпадающим с именем входного файла, но с расширением .err. Путь к этим файлам по умолчанию устанавливается таким же, как и к входному файлу (первый параметр командной строки).
178 Курсовая работа Анализ параметров командной строки запуска компилятора реализован в функ- ции ProcessParams (модуль FormLab4, листинг П3.14 в приложении 3). Например, командная строка: cursov.exe myfile.txt запустит на выполнение компилятор (cursov.exe) для обработки файла myfile.txt. При успешной компиляции результирующая программа будет записана в файл myfile.asm, а ошибки, если они будут обнаружены, — в файл myfile.err. При этом компилятор будет выполнять все виды оптимизации (оптимизацию методом сверт- ки объектного кода, оптимизацию методом исключения лишних операций и оп- тимизацию результирующего ассемблерного кода), поскольку ни один ключ не указан и все флаги будут установлены по умолчанию. Командная строка: cursov.exe infile.txt -Ooutfile.asm -Eerrfile.txt -АО -SO запустит на выполнение компилятор (cursov.exe) для обработки файла infile.txt. При успешной компиляции результирующая программа будет записана в файл outfile.asm, а ошибки, если они будут обнаружены, — в файл errfile.txt. При этом компилятор будет выполнять только оптимизацию методом свертки объектного кода, поскольку два ключа (- АО и -SO) отключают два других метода оптимизации. Информацию в файл ошибок компилятор всегда дописывает в конец файла, и толь- ко если такого файла нет, он создается заново. Дата и командная строка запуска компилятора всегда записываются в файл ошибок. Поэтому в том случае, когда компиляция выполнена успешно, в файл ошибок помещаются только две строки — командная строка и время запуска компилятора. В остальных случаях там будет присутствовать и информация об ошибке (построенный компилятор умеет обна- руживать только одну ошибку — ту, которая первой встретится ему в исходном тек- сте входной программы). Файл с результирующей программой всегда создается заново. Если такой файл уже существовал в момент запуска компилятора, то он будет уничтожен. Пример входной программы и результирующей программы Для иллюстрации работы созданного компилятора взяты два примера входной программы: 1. Программа, вычисляющая факториал числа. 2. Программа, на примере которой можно иллюстрировать работу оптимизиру- ющих алгоритмов. Оба примера приведены в приложении 4. Первый пример вычисляет факториал входной величины, причем если величина отрицательная или превышает 31, то программа возвращает 0. Умножение реали- зовано через цикл операций сложения. Входной файл приведен в листинге П4.1, а полученный результирующий файл — в листинге П4.2, приложение 4.
Пример выполнения курсовой работы 179 Второй пример содержит почти бессмысленную программу, которая всегда воз- вращает значение, равное 0, но на примере этой программы можно хорошо проил- люстрировать работу оптимизирующих алгоритмов. Входной файл приведен в ли- стинге П4.3, в листинге П4.4 приведен результирующий файл, полученный без применения оптимизации, а в листинге П4.5 — файл, полученный с применени- ем оптимизации. Желающие могут сравнить ассемблерный код этих двух файлов и проверить эффективность используемых алгоритмов оптимизации*. Выводы по проделанной работе В результате выполнения курсовой работы для заданного входного языка построен компилятор, порождающий результирующий код на языке ассемблера для процес- соров типа Intel 80386 и более поздних модификаций. Компилятор может работать с командной строкой, а при ее отсутствии предоставляет пользователю графический интерфейс, позволяющий указать входной файл и условия работы компилятора. Построенный компилятор обнаруживает все синтаксические ошибки языка, а так- же семантические ошибки: □ присваивание значений константам (когда первый операнд в операторе при- сваивания — константа); □ присваивание значений предопределенной входной переменной InpVar; □ использование предопределенной переменной Result. При наличии одной ошибки любого типа информация о ней с указанием пози- ции ошибки во входном файле заносится в файл информации об ошибках, а при наличии графического интерфейса пользователю выдается сообщение с позици- онированием указателя к местоположению ошибки. При наличии нескольких ошибок обнаруживается только первая из них, и дальнейший анализ исходного текста прекращается. Построенный компилятор также выполняет оптимизацию результирующей про- граммы следующими методами: □ свертка объектного кода; □ исключение лишних операций; □ исключение бесполезных арифметических и логических операций; □ модификация операций загрузки значения в регистр с учетом особенностей процессоров типа Intel 80x86. Это позволяет сократить объем результирующего ассемблерного кода и время выполнения объектного кода, который может быть построен на его основе. 1 Поскольку входной файл легко преобразовать в файл на языке Object Pascal, то ради сравнения оптимизирующих алгоритмов построенного компилятора с промышленным компилятором можно воспользоваться, например, системой программирования Delphi (любой версии) и построить с ее помощью ассемблерный код для этого входного файла (для просмотра ассемблерного кода удобнее всего воспользоваться окном отладчика). Интересно также построить этот код в двух режимах — с полным и неполным вычислением логических выражений. Автор очень рекомендует всем любо- пытствующим не полениться и проделать такую работу.
180 Курсовая работа Компилятор выполняет обработку исходной программы за шесть проходов: 1. Лексический анализ исходного текста и построение таблицы лексем. 2. Синтаксический анализ по таблице лексем и построение дерева синтаксиче- ского разбора. 3. Построение списка триад по дереву синтаксического разбора. 4. Оптимизация списка триад методом свертки объектного кода. 5. Оптимизация списка триад методом исключения лишних операций. 6. Построение результирующего ассемблерного кода по списку триад. На каждом проходе компилятора исходными данными являются результаты, по- лученные при выполнении предыдущего прохода. Количество проходов построенного компилятора может быть сокращено, посколь- ку все операции выполняются последовательно и не требуют обращений к дан- ным, отличным от данных, полученных на предыдущем проходе. Однако постро- енный компилятор как'нельзя лучше подходит для целей иллюстрации последо- вательности обработки исходной программы на различных этапах компиляции, когда каждому этапу компиляции соответствует один или несколько проходов. Построенный компилятор выполняет генерацию объектного кода для логических операций и для операций сравнения как для линейных операций, логические вы- ражения всегда вычисляются полностью — это позволяет оптимизировать логи- ческие выражения как линейные участки программы, но не вполне соответствует правилам, принятым в промышленных компиляторах. Кроме того, в построенном компиляторе использованы далеко не все возможности оптимизации объектного кода, ориентированного на язык ассемблера процессоров типа Intel 80x86. В целом можно заключить, что компилятор, построенный в примере выполнения курсовой работы, хорошо иллюстрирует технику и методы, лежащие в основе по- строения компиляторов, но из-за этого имеет меньшую эффективность обработки исходных программ. На учебных входных программах это никак не отражается, поскольку время их компиляции слишком мало, чтобы заметить такие недостатки.
ПРИЛОЖЕНИЕ 1 Функция переходов конечного автомата для лабораторной работы № 2 Условные обозначения: □ А — любой алфавитно-цифровой символ; □ А(*) — любой алфавитно-цифровой символ, кроме перечисленных в скобках; □ П — любой незначащий символ (пробел, знак табуляции, перевод строки, воз- врат каретки); □ Б — любая буква английского алфавита (прописная или строчная) или сим- вол подчеркивания («_»); □ Б(*) — любая буква английского алфавита (прописная или строчная) пли сим- вол подчеркивания («_»), кроме перечисленных в скобках; □ Ц — любая цифра от 0 до 9; □ F — функция обработки таблицы лексем, вызываемая при переходе КА из од- ного состояния в другое; обозначения ее аргументов: v — переменная, запомненная при работе КА; d — константа, запомненная при работе КА; а — текущий входной символ КА. В остальных случаях аргументом функции Fявляется соответствующая лексема. Конечный автомат: M(Q,Z,5// ,F): о
182 Приложение 1 • Функция переходов конечного автомата Q= {Н, С, G, V, D, И, 12, Tl, Т2, ТЗ, Т4, El, Е2, ЕЗ, Е4,01, 02, XI, Х2, ХЗ, Al, А2, АЗ, F} Е = А (все допустимые алфавитно-цифровые символы); q = Н; F = {F}. о В таблице П1.1. указаны значения функции переходов 5. Таблица П1.1. Функция переходов 5 8(Н,«(») = F I F(a) 8(H,«)») — F | F(a) 8(H,«;») = F | F(a) 8(Н,«{») = С ' 8(H,«:») = G 8(H,6(i,t,e,o,x,a)) = V 8(H,«i»)) = И 8(H,«t»)) = T1 8(H,«e»)) = E1 8(Н,«о»)) = О1 8(H,«x»)) = X1 8(H,«a»)) = A1 8(Н,Ц) = D 8(Н,П) = F 8(С,А(})) = С 8(C,«}») = F 8(G,«=») = F | F(:=) 8(V,«(») = F | F(v,a) 8(V,«)») = F | F(v,a) 8(V,«;») = F | F(v,a) 8(V,«{») = С | F(v) 8(V,«:») = G | F(v) 8(V,B) = V 8(V,U) = V 8(V,n) = F | F(v) 8(D,«(») = F | F(d,a) 8(D,«)») = F | F(d,a) 8(D,«;») = F | F(d,a) 8(D,«{») = C | F(d) 8(D,«:») = G | F(d) 8(D,I4) = D 8(D,n) = F | F(d) 8(11,«(») = F | F(v,a) 8(11,«)») = F | F(v,a) 8(11,«;») = F | F(v,a) 8(11,«{») = C | F(v) 8(11,«:») = G | F(v) 8(11,B(f)) = V 8(11, Ц) = V 8(11,П) = F | F(v) 8(11, «f») = I2 8(I2,«(») = F | F(if,a) 8(I2,«)«) = F | F(if,a) 8(I2,«;») = F | F(if,a) 8(I2,«{») = C | F(if) 8(I2,«:») = G | F(if) 8(12,6) = V 8(12,Ц) = V 8(12,П) = F | F(if) 8(T1,«(») = F | F(v,a) 8(T1,«)») = F | F(v,a) 8(T1,«;») = F | F(v,a) 8(T1,«{») = C | F(v) 8(T1,<») = G | F(v) 8(T1,B(h)) = V 8(Т1,Ц) = V 8(Т1,П) = F | F(v) . 8(T1,«h») = T2 8(T2,«(») = F | F(v,a) 8(T2,«)») = F | F(v,a) 8(T2,«;») = F | F(v,a) 8(Т2,«{») = C | F(v) 8(T2,«:») = G | F(v) 8(Т2,Б(е)) = V 8(Т2,Ц) = V 8(Т2,П) = F | F(v) 8(Т2,«е») = T3 8(ТЗ,«(») = F | F(v,a) 8(T3,«)») = F | F(v,a) 8(T3,«;«) = F | F(v,a) 8(T3,«{«) = C | F(v) 8(T3,«:»>) = G | F(v) 8(T3,B(n)) = V 8(ТЗ,Ц) = V 8(ТЗ,П) = F | F(v) 8(ТЗ,«п») = T4 8(Т4,«(») = F | F(then,a) 8(T4,«)») = F | F(then,a) 8(T4,«;») = F | F(then,a) 8(Т4,«{») = C | F(then) 8(T4,«:») = G | F(then) 8(Т4,Б) = V 8(Т4,Ц) = V 8(Т4,П) = F | F(then) 8(E1,«(») = F | F(v,a) 8(E1,«)») = F | F(v,a) 8(E1,«;») = F | F(v,a) 8(E1,«{») = C | F(v) 8(E1 ,«:>>) = G | F(v) 8(E1,6(I)) = V 8(Е1,Ц) = V 8(Е1,П) = F | F(v) 8(E1,«I») = E2 8(Е2,«(») = F | F(v,a) 8(E2,«)») = F | F(v,a) 8(E2,«;») = F | F(v,a) 8(Е2,«{») = C | F(v) 8(E2,«:»>) = G | F(v) 8(E2,6(s)) = V 8(Е2,Ц) = V 8(Е2,П) = F | F(v) 8(E2,«s») = E3 8(E3,«(») = F | F(v,a) 8(E3,«)») = F I F(v,a) 8(E3,«;«) = F | F(v,a)
Функция переходов конечного автомата для лабораторной работы № 2 183 8(ЕЗ,«{») = С | F(v) 8(ЕЗ,«:») = G | F(v) 8(ЕЗ,Б(е)) = V • 8(ЕЗ,Ц) = V 8(ЕЗ,П) = F | F(v) 8(ЕЗ,«е») = E4 8(Е4,«(») = F | F(else,a) 8(Е4,«)») = F | F(else,a) 8(E4,«;») = F | F(else,a) 8(Е4,«{») - С | F(else) 8(Е4,«:») = G | F(else) 8(Е4,Б) = V 8(Е4,Ц) = V 8(Е4,П) = F | F(else) 8(01,«(») = F | F(v,a) 8(01,«)») = F | F(v,a) 8(O1,«;»)= F | F(v,a) 8(О1,«{») = С | F(v) 8(01,«:») = G | F(v) 8(01,Б(г)) = V 8(01,Ц) = V 8(01,П) = F | F(v) 8(01,«г») = 02 8(02,«(») = F | F(or,a) 8(02,«)») = F | F(or,a) 8(02,«;») = F | F(or,a) 8(02,«{») = С | F(or) 8(02,«:») = G | F(or) 8(02,Б) = V 8(02,Ц) = V 8(02,П) = F | F(or) 8(Х1,«(») = F | F(v,a) 8(X1,«)») = F | F(v,a) 8(X1,«;») = F | F(v,a) 8(Х1,«{») = С | F(v) 8(X1,«:») = G | F(v) 8(Х1,Б(о)) = V 8(Х1,Ц) = V 8(Х1,П) = F | F(v) 8(X1,«o») = X2 8(Х2,«(») = F | F(v,a) 8(X2,«)») = F | F(v,a) 8(X2,«;») = F | F(v,a) 8(Х2,«{») = С | F(v) 8(X2,«:») = G | F(v) 8(Х2,Б(г)) = V 8(Х2,Ц) = V 8(Х2,П) = F | F(v) 8(Х2,«г») = X3 8(ХЗ,«(») = F | F(xor.a) 8(X3,«)») = F | F(xor,a) 8(X3,«;») = F | F(xor,a) 8(ХЗ,«{») = С | F(xor) 8(X3,«:») = G | F(xor) 8(ХЗ,Б) = V 8(ХЗ,Ц) = V 8(ХЗ,П) = F | F(xor) 8(А1,«(») = F | F(v,a) 8(A1,«)») = F | F(v,a) 8(A1,«;») = F ] F(v,a) 8(А1,«{») = С | F(v) 8(A1,«:») = G | F(v) 8(А1,Б(п)) = V 8(А1,Ц) = V 8(А1,П) = F | F(v) 8(А1,«п») = A2 8(А2,«(») = F | F(v,a) 8(A2,«)») = F | F(v,a) 8(A2,«;») = F | F(v,a) 8(А2,«{») = С | F(v) 8(A2,«:») = G | F(v) 8(A2,B(d)) = V 8(А2,Ц) = V 8(А2,П) = F | F(v) 8(A2,«d») = A3 8(АЗ,«(») = F | F(and.a) 8(A3,«)») = F | F(and,a) 8(A3,«;») - F | F(and,a) 8(АЗ,«Ь) = С | F(and) 8(A3,«:») = G | F(and) 8(АЗ,Б) = V 8(АЗ,Ц) = V 8(АЗ,П) = F | F(and) При описании функции переходов через разделитель «|» указаны вызовы функ- ции F, необходимые при выполнении того или иного перехода (если они есть).
Функция переходов конечного автомата для курсовой работы 185 Е = А (все допустимые алфавитно-цифровые символы); q = Н; F = [F, 5}. о В таблице П2.1. указаны значения функции переходов 8. Таблица П2.1. Функция переходов 8 ПРИЛОЖЕНИЕ 2 Функция переходов конечного автомата для курсовой работы Условные обозначения: □ А — любой алфавитно-цифровой символ; □ А(*) — любой алфавитно-цифровой символ, кроме перечисленных в скобках; □ П — любой незначащий символ (пробел, знак табуляции, перевод строки, воз- врат каретки); □ Б — любая буква английского алфавита (прописная или строчная) или сим- вол подчеркивания («_»); □ Б(*) — любая буква английского алфавита (прописная или строчная) или сим- вол подчеркивания («_»), кроме перечисленных в скобках; □ Ц — любая цифра от 0 до 9; □ F— функция обработки таблицы лексем, вызываемая при переходе КА из од- ного состояния в другое, обозначения ее аргументов: v — переменная, запомненная при работе КА; d — константа, запомненная, при работе КА; а — текущий входной Символ КА. В остальных случаях аргументом функции ^является соответствующая лексема. Конечный автомат: M(Q,E,8,? ,F): Q= {Н, С,°С1, G, S, L, V, D, Pl, Р2, РЗ, Р4, El, Е2, ЕЗ, И, 12, L2, L3, L4, Bl, В2, ВЗ, В4, В5, Wl, W2, W3, W4, W5,01,02, DI, D2, XI, Х2, ХЗ, Al, А2, АЗ,Nl,N2,N3, F} 8(Н,«(») = S | F(a) 8(H,«)») = F | F(a) 8(H,«;») = F | F(a) 8(Н,«-») = S | F(a) 8(H,«+») = S | F(a) 8(H,«=») = S | F(a) 8(Н,«<») = L 8(H,«>») = S | F(a) 8(H1B(p,e,i,b,w,o,x,aln)) = V 8(Н,«р»)) = Р1 8(H,«e»)) = E1 8(H,«i»)) = 11 8(Н,«Ь»)) = В1 8(H,«w»)) = W1 8(H,«o»)) = O1 8(H,«d»)) = D1 8(H,«x»)) = X1 8(H,«a»)) = A1 8(Н,«п»)) = N1 8(H,«:») = G ‘ 8(Н,Ц) = D 8(Н,П) = F 8(Н,«{») = C 8(S,«(») = S | F(a) 8(S,«-»>) = S | F(um) 8(S,B(p,e,i,b,w,o,x,a,n)) = V 8(S,«p»)) = Р1 8(S,«e»)) = E1 8(S,«i»)) = 11 8(S,«b»)) = В1 8(S,«w»)) = W1 8(S,«o»)) = O1 8(S,«d»)) = D1 8(S,«x»)) = X1 8(S,«a»)) = A1 8(S,«n»)) = N1 8(S,«:») = G 8(S,U) = D 8(S,n) = S 8(S,«{») = C1 8(L,«(») = S | F(«<»,a) 8(L,«-») = S | F(«<»,um) SfL.Bfp.ej.b.w.o.x.a.n)) = V | F(«<») 8(L,«p»)) = P1 | F(«<») 8(L,«e»)) = E1 | F(«<») 8(L,«i»)) = 11 | F(«<») 8(L,«b»)) = B1 | F(«<») 8(L,«w»)) = W1 | F(«<») 8(L,«o»)) =O1 | F(«<») 8(L,«d»)) = D1 | F(«<») 8(L,«x»)) = X1 | F(«<») 8(L,«a»)) = A1 | F(«<») 8(L,«n»)) = N1 | F(«<») 8(L,«:») = G | F(«<») 8(1_Ц) = D | F(«<») 8(L,n) = S | F(«<») 8(L,«>») = S | F(«<>») 8(L,«{»)= C1 | F(«<») 8(C,A(})) = C 8(C,«}») = F 8(G,«=») =S | F(:=) 8(C1,A(})) = C1 8(C1,«}») = S 8(V,«(») = S | F(v,a) 8(V,«)») = F | F(v,a) 8(V,«;») = F | F(v,a) 8(V,«-») = S | F(v,a) 8(V,«+») = S | F(v,a) 8(V,«=») = S | F(v,a) 8(V,«<») = L | F(v) 8(V,«>») = S | F(v,a) • 8(V,«{») = C | F(v) 8(V,«:») = G | F(v) 8(V,B) = V;8(V,U) = V 8(V,n) = F | F(v) 8(D,«(») = S | F(d,a) 8(D,«)») = F | F(d,a) 8(D,«;») = F | F(d,a) 8(D,«-») = S | F(d,a) 8(D,«+») = S | F(d,a) 8(D,«=») = S | F(d,a) 8(D,«<») = L | F(d) 8(D,«>») = S | F(d,a) 8(D,«{») = C | F(v) 8(D,«:») = G | F(d) 8(D,U) = D 8(D,n) = F | F(d) 8(P1,«(») = S | F(v,a) 8(P1,«)») = F | F(v,a) 8(P1,«;») = F | F(v,a) 8(P1,«-») = S | F(v,a) 8(P1,«+») = S | F(v,a) 8(P1,«=») = S | F(v,a) 8(P1,«<») = L | F(v) 8(P1 ,«>») = S | F(v,a) 8(Р1,«{») = C | F(v) 8(P1,«:») = G ] F(v) 8(Р1,Б(г)) = V 8(Р1,Ц) = V 8(Р1,П) = F | F(v) 8(Р1,«г») = P2 8(P2,«(») = S | F(v,a) 8(P2,«)») = F | F(v,a) 8(P2,«;») = F | F(v,a) 8(P2,«-») = S | F(v,a) 8(P2,«+») = S | F(v,a) 8(P2,«=>>) = S | F(v,a) 8(P2,«<») = L | F(v) 8(P2,«>») = S | F(v,a) 8(Р2,«{») = C | F(v) 8(P2,«:») = G | F(v) 8(Р2,Б(о)) = V 8(Р2,Ц) = V продолжение &
186 Приложение 2 • Функция переходов конечного автомата Таблица П2.1 (продолжение) 8(Р2,П) = F | F(v) 8(Р2,«о») = P3 8(РЗ,«(») = S | F(v,a) 8(P3,«)») = F | F(v,a) 8(P3,«;») = F | F(v,a) 8(РЗ,«-») = S | F(v,a) 8(P3,«+») = S | F(v,a) 8(P3,«=») = S | F(v,a) 8(РЗ,«<») = L | F(v) 8(P3,«>») = S | F(v,a) 8(РЗ,«{») = C | F(v) 8(РЗ,«:») = G | F(v) 8(P3,E(g)) = V;8(P3,I4) = V 8(РЗ,П) = F | F(v) 8(РЗ,«д») = Р4 8(P4,«(») = S | F(prog,a) 8(P4,«)») = F | F(prog,a) 8(Р4,«;») = F | F(prog.a) 8(P4,«-») = S | F(prog,um) 8(P4,«+») = S | F(prog.a) 8(Р4,«=») = S | F(prog,a) 8(P4,«<») = L | F(prog) 8(P4,«>») = S | F(prog,a) 8(Р4,«{») = С1 | F(prog) 8(P4,«:»>) = G | F(prog) 8(Р4,Б) = V 8(Р4,Ц) = V 8(Р4,П) = S | F(prog) 8(Е1,«(») = S | F(v,a) 8(E1,«)») = F | F(v,a) 8(E1,«;») = F | F(v,a) 8(Е1,«-») = S | F(v,a) 8(E1,«+») = S | F(v,a) 8(E1,«=») = S | F(v,a) 8(Е1,«<») = L | F(v) 8(E1,«>») = S | F(v,a) 8(Е1,«{») = C | F(v) 8(Е1,«:») = G | F(v) 8(Е1,Б(п,1)) = V 8(Е1,Ц) = V 8(Е1,П) = F | F(v) 8(Е1,«п») = E2 8(Е1,«1») = L2 8(Е2,«(») = S | F(v,a) 8(E2,«)») = F | F(v,a) 8(Е2,«;») = F | F(v,a) 8(Е2,«-») = S | F(v,a) 8(E2,«+») = S | F(v,a) 8(E2,«=») = S | F(v,a) 8(Е2,«<») = L | F(v) 8(E2,«>») = S | F(v,a) 8(Е2,«{») = C | F(v) 8(Е2,«:») = G | F(v) 8(E2,E(d)) = V 8(Е2,Ц) = V 8(Е2,П) = F | F(v) 8(E2,«d») = E3 8(ЕЗ,«(») = S | F(end,a) 8(E3,«)») = F | F(end,a) 8(E3,«;») = F | F(end,a) 8(ЕЗ,«-») = S | F(end,um) 8(E3,«+») = S | F(end,a) 8(E3,«=») = S | F(end,a) 8(ЕЗ,«<») = L | F(end) 8(E3,«>») = S | F(end,a) 8(E3,«{”) = C1 | F(end) 8(ЕЗ,«:») = G | F(end) 8(ЕЗ,Б) = V 8(ЕЗ,Ц) = V 8(ЕЗ,«.») = F | F(end.) 8(ЕЗ,П) = S | F(end) 8(11,«(») = S | F(v,a) 8(11,«)») = F | F(v,a) 8(11,«;») = F | F(v,a) 8(11,«-») = S | F(v,a) 8(11,«+») = S | F(v,a) 8(I1,«=») = S | F(v,a) 8(11,«<») = L | F(v) 8(11,«>») = S | F(v,a) 8(11,«{») = C | F(v) 8(11,«:») = G | F(v) 8(11,5(f)) = V 8(11,Ц) = V 8(11,П) = F | F(v) 8(11,«f») = I2 8(12,П) = S | F(if) 8(12,«(») = S | F(if,a) 8(I2,«)») = F | F(if,a) 8(I2,«;») = F | F(if,a) 8(12,«-») = S | F(if,um) 8(12,«+») = S | F(if,a) 8(I2,«=») = S | F(if,a) 8(12,«<») = L | F(if) 8(12,«>») = S | F(if,a) 8(I2,«{») = C1 | F(if) 8(12,«:») = G | F(if) 8(12,Б) = V 8(12,Ц) = V 8(L2,«(») = S | F(v,a) 8(L2,«)») = F | F(v,a) 8(L2,«;») = F | F(v,a) 8(L2,«-») = S | F(v,a) 8(L2,«+») = S | F(v,a) 8(l_2,«=») = S | F(v,a) 8(L2,«<») = L | F(v) 8(L2,«>») = S | F(v,a) 8(L2,«{») = C | F(v) 8(L2,«:») = G | F(v) 8(L2,E(s)) = V 8(1_2,Ц) = V 8(1_2,П)' = F | F(v) 8(L2,«s») = L3 8(L3,«(») = S | F(v,a) 8(L3,«)») = F | F(v,a) 8(L3,«;») = F | F(v,a) 8(L3,«-») = S | F(v,a) 8(L3,«+») = S | F(v,a) 6(L3,«=») = S | F(v,a) 8(L3,«<») = L | F(v) 8(L3,«>») = S | F(v,a) • 8(L3,«{») = C | F(v)
JValaHaws^i Функция переходов конечного автомата для курсовой работы 187 8(L3,«:») = G | F(v) 8(L3,B(e)) = V 8(L3,L|) =V 8(1_3,П) = F | F(v) 8(L3,«e») = L4 8(L4,P) = S | F(else) 8(L4,«(») = S | F(else,a) 8(L4,«)») = F | F(else,a) 8(L4,«;») - F | F(else,a) 8(L4,«-») = S | F(else.um) 8(L4,«+») = S | F(else,a) 8(L4.«=») = S | F(else,a) 8(L4,«<») = L | F(else) 8(L4,«>») - S | F(else,a) 8(L4,«{») = C1 | F(else) 8(L4,«:») = G | F(else) 8(L4,B) = V 8(L4.U) = V 8(B1,«(») = S | F(v,a) 8(B1,«)») = F | F(v,a) 8(B1,«;») = F | F(v,a) 8(B1,«-») = S | F(v,a) 8(B1,«+») = S | F(v,a) 8(B1,«=») = S | F(v,a) 8(B1,«<») = L | F(v) 8(B1,«>») = S | F(v,a) 8(B1,«b) = C | F(v) 8(B1,«:») = G | F(v) 8(В1,Б(е)) = V 8(B1,I4)=V • 8(В1,П) = F | F(v) 8(B2,«(») = S | F(v,a) 8(B1 ,«е») = B2 8(B2,«)») = F | F(v,a) 8(B2,«;») = F | F(v,a) 8(B2,«-») = S | F(v,a) 8(B2,«+») = S | F(v,a) 8(B2,«=>) = S | F(v,a) 8(B2,«<») = L | F(v) 8(B2,«>») = S | F(v,a) 8(B2,«b)= C | F(v) 8(B2,«:») = G | F(v) 8(B2,B(g)) = V 8(В2,Ц) = V 8(В2,П) = F | F(v) 8(B2,«g») = B3 8(В5,Б) = V;8(B5,U) = V 8(B3,«(») = S | F(v,a) 8(B3,«)») = F | F(v,a) 8(B3,«>) = F | F(v,a) 8(B3,«-») = S | F(v,a) 8(B3,«+») = S | F(v,a) 8(B3,«=>) = S | F(v,a) 8(B3,«<») = L | F(v) 8(B3,«>») = S | F(v,a) 8(B3,«b) = C | F(v) 8(B3,«:») = G | F(v) 8(B3,B(i)) = V 8(ВЗ,Ц) =V 8(ВЗ,П) = F | F(v) 8(B3,«i«) = B4 8(В5,П) = S | F(begin) 8(B4,«(») = S | F(v,a) 8(B4,«)») = F | F(v,a) 8(B4,«;») = F | F(v,a) 8(B4,«-») = S | F(v,a) 8(B4,«+») = S | F(v,a) 8(B4,«=») = S | F(v,a) 8(B4,«<») = L | F(v) 8(B4,«>») = S | F(v,a) 5(B4,«b) = C | F(v) 8(B4,«:») = G | F(v) 8(B4,B(n)) = V 8(В4,Ц) = V 8(В4,П) = F | F(v) 8(В4,«п») = B5 8(В5,«=») = S | F(begin,a) 8(B5,«(») = S | F(begin.a) 8(B5,«)»>) = F | F(begin,a) 8(B5,«;») = F | F(begin,a) 8(B5,«-») = S | F(begin,um) 8(B5,«+») = S ] F(begin,a> 8(B5,«<») = L | F(begin) 8(B5,«>») = S | F(begin.a) 8(B5,«P>) = 01 | F(begin) 8(B5,«>) = G | F(begin) 8(W1,«(») =S | F(v,a) 8(W1,«)») = F | F(v,aJ 8(W1,<;»)= F | F(v,a) 8(W1,«-») =S | F(v,a) 8(W1,«+») = S | F(v,a) 8(W1,«=») = S | F(v,a) 8(W1,«<») = L | F(v) 8(W1 ,«>») = S | F(v,a) 8(W1,«(») = C | F(v) 8(W1 ,«:») = G | F(v) 8(W1,B(h)) = V 8(W1,U) = V 8(W1,n) = F | F(v) 8(W2,«(») =S | F(v,a) 8(W1,«h») = W2 8(W2,«)») = F | F(v,a) 8(W2,«;»)= F | F(v,a) 8(W2,«-») = S | F(v,a) 8(W2,«+») = S | F(v,a) 8(W2,«=») = S | F(v,a) 8(W2,«<») = L | F(v) 8(W2,«>») = S | F(v,a) 8(W2,«<») = C | F(v) 8(W2,«:») = G | F(v) 8(W2,B(i)) = V 8(W2,UJ = V 8(W2,n) = F | F(v) 8(VV2,«i») = W3 8(W3,n) = -F | F(v) 8(W3,«(») = S | F(v,a) 8(W3,«)>») = F | F(v,aJ 8(W3,<;»)= F 1 F(v,a) 8(W3,«-») = S | F(v,a) 8(W3,«+») = S | F(v,a) 8(W3,«=») = S | F(v,a) 8(W3,«<») = L | F(v) 8(W3,«>») = S | F(v,a) 8(W3,<{») = C | F(v) продолжение &
188 Приложение 2 • Функция переходов конечного автомата Таблица П2.1 (продолжение) 8(W3,«:») = G | F(v) 8(W3,6(I)) = V 8(W3,U) = V 8(W3,«I») = W4 8(W4,«(») = S | F(v,a) 8(W4,«)») = F | F(v,a) 8(W4,«;») = F | F(v,a) 8(W4,«-») = S | F(v,a) 8(W4,«+») = S | F(v,a) 8(W4,«=») = S | F(v,a) 8(W4,«<») = L | F(v) 8(W4,«>») = S | F(v,a) 8(W4,«{») = C | F(v) 8(W4,«:») = G | F(v) 8(W4,B(e)) = V 8(W4,I4) = V 8(W4,n) = F | F(v) 8(W4,«e») = W5 8(W5,«(») = S | F(while.a) 8(W5,«)») = F | F(while,a) 8(W5,«;») = F | F(while,a) 8(W5,«-») = S | F(while,um) 8(W5,«+») = S | F(while,a) 8(W5,«=») = S | F(while,a) 8(W5,«<») = L | F(while) 8(W5,«>») = S | F(while.a) 8(W5,«{») = C1 | F(while) 8(W5,«:») = G | F(while) 8(W5,B) = V;8(W5,U) = V 8(W5,n) = S | F(while) 8(01,«(») = S | F(v,a) 8(01,«)») = F | F(v,a) 8(01,«;») = F | F(v,a) 8(01,«-») = S | F(v,a) 8(01,«+») = S | F(v,a) 8(01,«=») = S | F(v,a) 8(01,«<») = L | F(v) 8(01,«>») = S | F(v,a) 8(О1,«{») = C | F(v) 8(01 ,«:») = G | F(v) 8(O1,B(r)) = V 8(01 ,Ц) = V 8(01,П) = F | F(v) 8(01,«г») = 02 8(02,П) = S | F(or) 8(02,«(») = S | F(or,a) 8(02,«)») = F | F(or,a) 8(O2,«;») = F | F(or,a) 8(02,«-») = S | F(or,um) 8(02,«+») = S | F(or,a) 8(02,«=») = S | F(or,a) 8(02,«<») = L | F(or) 8(02,«>») = S | F(or,a) 8(02,«{») = C1 | F(or) 8(02,«:») = G | F(or) 8(02,6) = V 8(02,Ц) = V 8(D1,«(») = S | F(v,a) 8(D1,«)») = F | F(v,a) 8(D1,«;») = F | F(v,a) . 8(D1,«-») = S | F(v,a) 8(D1,«+») — S | F(v,a) 8(D1,«=») = S | F(v,a) 8(D1,«<») = L | F(v) 8(D1,«>») = S | F(v,a) 8(D1,«{») = C | F(v) 8(D1,«:») = G | F(v) 8(D1,B(r)) = V 8(D1,I4) = V 8(D1,«o») =»D2 8(D2,«(») = S | F(do,a) 8(D2,«)») = F | F(do,a) 8(D2,«;») = F | F(do,a) 8(D2,«-») = S | F(do,um) 8(D2,«+») = S | F(do,a) 8(D2,«=») = S | F(do,a) 8(D2,«<») = L | F(do) 8(D2,«>») = S | F(do,a) 8(D2,«{») = C1 | F(do) 8(D2,«:») = G | F(do) 8(D2,6) = V 8(D2,U) = V 8(X1,«(») = S | F(v,a) 8(D2,n) = S | F(do) 8(X1,«)») = F | F(v,a) 8(X1,«;») = F | F(v,a) 8(X1,«-») = S | F(v,a) 8(X1,«+») = S | F(v,a) 8(X1,«=») = S | F(v,a) 8(X1 ,«<») = L | F(v) 8(X1,«>») = S | F(v,a) 8(Х1,«{») = C | F(v) 8(X1,«:») = G | F(v) 8(Х1,Б(о)) = V 8(Х1,Ц) = V 8(Х1,П) = F | F(v) 8(X2,«(») = S | F(v,a) 8(Х1,«о») = X2 8(X2,«)») = F | F(v,a) 8(X2,«;») = F | F(v,a) 8(X2,«-») = S | F(v,a) 8(X2,«+») = S | F(v,a) 8(X2,«=») = S | F(v,a) 8(X2,«<») = L | F(v) 8(X2,«>») = S | F(v,a) 8(Х2,«{») = C | F(v) 8(X2,«:») = G | F(v) 8(X2,6(r)) = V 8(Х2,Ц) = V 8(Х2,П) = F | F(v) 8(Х2,«г») = X3 8(ХЗ,П) = S | F(xor) 8(X3,«(») = S | F(xor,a) 8(X3,«)») = F | F(xor,a) 8(X3,«;») = F | F(xor,a) 8(X3,«-») = S | F(xor,um) 8(X3,«+») = S | F(xor,a) 8(X3,«=») = S | F(xor,a) 8(X3,«<») = L | F(xor) 8(X3,«>») = S | F(xor,a) 8(ХЗ,«{») = C1 | F(xor) 8(X3,«:») = G | F(xor) 8(ХЗ,Б) = V 8(ХЗ,Ц) = V
Функция переходов конечного автомата для курсовой работы 189 8(А1,«(») = S | F(v,a) 8(A1,«)») = F | F(v,a) 8(A1,«;») = F | F(v,a) 8(А1,«-») = S | F(v,a) 8(A1,«+») = S I F(v,a) 8(A1,«=«) = S | F(v,a) 8(А1 ,«<») = L | F(v) 8(A1,«>») = S | F(v,a) 8(A1 ,«!») = C | F(v) 8(А1,«:») = G | F(v) 8(A1,E(n)) = V 5(A1 ,Ц) = V 8[А1,П) = F | F(v) 8(А1,«п») = A2 8(А2,«(») = S | F(v,a) 8(A2,«)») = F | F(v,a) 5(A2,«;>) = F | F(v,a) 8(А2,«-») = S | F(v,a) 8(A2,«+») = S | F(v,a) 8(A2,«=») = S | F(v,a) 8(А2,.«<») = L | F(v) 8(A2,«>») = S | F(v,a) 8(А2,«<») = C | F(v) 8(А2,«:») = G | F(v) 8(A2,E(d)) = V 8(А2,Ц) = V 8(А2,П) = F | F(v) 8(A2,«d») = A3 8(АЗ,П) = S | F(and) 8[АЗ,«(») = S | F(and,a) 8(A3,«)») = F | F(and,a) 5(A3,«;») = F | F(and,a) 8(АЗ,«-») = S | F(and,um) 8(A3,«+») = S | F(and,a) 8(A3,«=») = S | F(and,a) 8(АЗ,«<») = L | F(and) 8(A3,«>») = S | F(and,a) 8(АЗ,«{») = C1 | F(and) 8(АЗ,«:») = G | F(and) 8(АЗ,Б) = V 8(АЗ,Ц) = V 8(N1,«(») = S | F(v,a) 8(N1,«)») = F | F(v,a) 8(N1,«;») = F | F(v,a) 8(N1,«-») = S | F(v,a) 8(N1,«+») = S | F(v,a> 8(N !,«=») = S | F(v,a) 8(N1 ,«<») = L | F(v) 8(N1,«>») = S | F(v,a) 8(N1,«{>>) = C | F(v) 8(N1,«:») = G | F(v) 8(N1,E(o)) = V 5(N1,U) = V 8[N1,n)= F | F(v) 8(N1,«o») = N2 8(N2,«(») = S | F(v,a) 8(N2,«)») = F | F(v,a) 8(N2t«;») = F | F(v,a) 8(N2,«-») = S | F(v,a) 8(N2,«+») = S | F(v,a) 8(N2,«=») = S | F(v,a) 8(N2,«<») = L | F(v) 8(N2,«>») = S | F(v,a) 8(N2,«{”) = C | F(v) 8(N2,«:») = G | F(v) 8(N2,E(t)) = V 8(N2,U) = V 8(N2,n) = F | F(v) 8(N2,«t») = N3 8(N3,«(») = S | F(hot,a) 8(N3,«)») = F | F(not,a) 8(N3,«;») = F | F(not.a) 8(N3,«-») = S | F(not,um) 8(N3,«+») = S | F(not,a) 8(N3,‘<=») = S | F(not,a) 3(N3,«<») = L | F(not) 8(N3,«>») = S | F(not,a) 6(N3,«{»>) = C1 | F(not) 8(N3,«:») = G | F(not) 8(N3,B) = V 8(N3,U) = V 8(N3,n) = S | F(not) При описании функции переходов через разделитель «|» указаны вызовы функ- ции F, необходимые при выполнении того или иного перехода (если они есть).
ПРИЛОЖЕНИЕ 3 Тексты программных модулей для курсовой работы Модуль структуры данных для таблицы идентификаторов Следует обратить внимание, что функция Upper в листинге П3.1 построена на ос- нове условной компиляции: □ если при компиляции определено имя «REGNAME», то таблицы идентификато- ров строятся на основе имен переменных, не зависящих от регистра символов (прописные и строчные буквы не различаются); □ если при компиляции имя «REGNAME» не определено, то таблицы идентифика- торов строятся на основе имен переменных, зависящих от регистра символов (пропцсные и строчные буквы различаются). Листинг П3.1. Описание структуры данных для элементов таблицы идентификаторов unit TblElem: Interface { Модуль, описывающий структуру данных элементов таблицы идентификаторов } type TAddVarlnfo = class(TObject) { Класс для описания базового типа данных, связанных с элементом таблицы идентификаторов}
191 ^atattaus^. Модуль структуры данных для таблицы идентификаторов public procedure SetlnfodIdx: Integer; 11nfo: longlnt): virtual; abstract; function Getlnfodldx; Integer); longlnt; virtual: abstract: property Info[1Idx: Integer]: longlnt read Getlnfo write Setinfo: default: end: TVarlnfo = class(TObject) protected { Класс для описания элемента хэш-таблицы } sName: string; { Имя элемента ) plnfo: TAddVarlnfo: { Дополнительная информация } mlnEl.maxEl: TVarlnfo; { Ссылки на меньший и больший элементы для организации бинарного дерева } public { Конструктор создания элемента хэш-таблицы } constructor Create(const sN: string): { Деструктор для освобождения памяти, занятой элементом } destructor Destroy: override; { Функция заполнения дополнительной информации элемента } procedure Setlnfo(pl: TAddVarlnfo): { Функции для удаления дополнительной информации } procedure Cl earInfo: procedure ClearAll Info: { Свойства "Имя элемента" й "Дополнительная информация" } property VarName: string read sName; property Info: TAddVarlnfo read plnfo write Setinfo; { Функции для добавления элемента в бинарное дерево } function AddEICntIconst sAdd: string; var iCnt; integer): TVarlnfo; function AddElemlconst sAdd: string): TVarlnfo: { Функции для поиска элемента в бинарном дереве } function FindEICntIconst sN: string: var ICnt: integer): TVarlnfo: function FindElem(const sN: string): TVarlnfo; {Функция записи всех имен идентификаторов в одну строку) function GetElList(const sL1m.sInp.s0ut: string): string: end; function Upper(const x:string); string; implementation продолжение &
192 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.1 (продолжение) uses SysUtils; { Условная компиляция: если определено имя REGNAME. то имена переменных считаются регистронезависимыми. иначе - регистрозависимыми } {$IFDEF REGNAME} function Upper(const x:strIng): string: begin Result := UpperCase(x): end: {$ELSE} function Upper(const x:string): string; begin Result := x: end; {$ENDIF} constructor TVarlnfo.Create(const sN: string): { Конструктор создания элемента хэш-таблицы } begin inherited Create; {Вызываем конструктор базового класса) { Запоминаем имя элемента и обнуляем все ссылки } sName := sN: plnfo := nil; minEl := nil: maxEl := nil; end; destructor TVarlnfo.Destroy: { Деструктор для освобождения памяти, занятой элементом } begin {Освобождаем память по каждой ссылке, при этом в дереве рекурсивно будет освобождена память для всех элементов} ClearAllInfo: minEl.Free: maxEl.Free: inherited Destroy: {Вызываем деструктор базового класса} end: function TVarInfo.GetElList(const $1_1т{разделитель списка}. sInp.sOut{nMeHa. не включаемые в строку}: string): string; { Функция записи всех имен идентификаторов в одну строку } var sAdd: string; begin Result := ": { Первоначально строка пуста } { Если элемент таблицы не совпадает с одним из невключаемых имен, то его нужно включить в строку } if (Upper(sName) <> Upper(slnp)) and (Upper(sName) <> Upper(sOut)) then Result := sName; if minEl <> nil then { Если есть левая ветвь дерева }
Модуль структуры данных для таблицы идентификаторов 193 begin { Вычисляем строку для этой ветви } sAdd := minEl.GetElList(sLim.slop.sOut): if sAdd <> then { Если она не пустая. } begin { добавляем ее через разделитель } if Result <> " then Result := Result + sLim + sAdd else Result : = sAdd: end: end: if maxEl <> nil then { Если есть правая ветвь дерева } begin { Вычисляем строку для этой ветви } sAdd := maxEl.GetEl Li st(sLim.sInp.sOut); if sAdd <> ” then { Если она не пустая. } begin { добавляем ее через разделитель } if Result <> '' then Result := Result + sLim + sAdd else Result := sAdd: end; end:. end: procedure TVarlnfo.Setinfolpl: TAddVarlnfo): { Функция заполнения дополнительной информации элемента } begin plnfo := pl; end: procedure TVarlnfo.Clearinfo; { Функция удаления дополнительной информации элемента } begin plnfo.Free; plnfo := nil: end: procedure TVarlnfo.ClearAllInfo; { Функция удаления связок и дополнительной информации } begin if minEl <> nil then minEl.ClearAlI Info: if maxEl <> nil then maxEl.ClearAll Info: Clearinfo: end: function TVarlnfo.AddElCnt(const sAdd: string; var iCnt: integer): TVarlnfo: { Функция добавления элемента в бинарное дерево с учетом счетчика сравнений } var i: integer; begin Inc(iCnt): { Увеличиваем счетчик сравнений } продолжение & 7 Зак 6Х
194 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.1 (продолжение) { Сравниваем имена элементов (одной функцией!) } i := StrComp(PChar(Upper(sAdd)).PChar(Upper(sName))); if 1 < 0 then { Если новый элемент меньше, смотрим ссылку на меньший } begin { Если ссылка не пустая, рекурсивно вызываем функцию добавления элемента } if minEl <> nil then Result := minEl.AddElCnt(sAdd.iCnt) else begin { Если ссылка пустая, создаем новый элемент и запоминаем ссылку на него } Result := TVarlnfo.Create(sAdd): minEl := Result; end: end el se { Если новый элемент больше, смотрим ссылку на больший } if i > 0 then begin { Если ссылка не пустая, рекурсивно вызываем функцию добавления элемента } if maxEl <> nil then Result := maxEl.AddElCnt(sAdd.iCnt) else begin { Если ссылка пустая, создаем новый элемент и запоминаем ссылку на него } Result := TVarlnfo.Create(sAdd): maxEl ;= Result: end; end { Если имена совпадают, то такой элемент уже есть в дереве - это текущий элемент } else Result := Self: end: function TVarInfo.AddElem(const sAdd: string): TVarlnfo; { Функция добавления элемента в бинарное дерево } var iCnt: integer; begin Result := AddElCnt(sAdd.iCnt); end; function TVarlnfo.FindEICnt(const sN: string; var iCnt: integer): TVarlnfo: { Функция поиска элемента в бинарном дереве с учетом счетчика сравнений } var i: integer;
Модуль таблицы идентификаторов на основе хэш-адресации 195 begin Inc(iCnt); { Увеличиваем счетчик сравнений } { Сравниваем имена элементов (одной функцией!) } 1 := StrComp(PChar( Upper (sN)). PChardlpper С sName) )Э: if 1 < 0 then {Если искомый элемент меньше, смотрим ссылку на меньиий} begin {Если ссылка не пустая, рекурсивно вызываем для нее функцию поиска элемента, иначе - элемент не найден} if minEl <> nil then Result := minEl.FindEICntCsN.ICnt) else Result : = nil: end else if i > 0 then {Если искомый элемент больше, смотрим ссылку на больший} begin {Если ссылка не пустая, рекурсивно вызываем для нее функцию поиска элемента, иначе - элемент не н-айден} if maxEl <> nil then Result := maxEl.FindElCnt(sN.iCnt) else Result := nil; .end { Если имена совпадают, то искомый элемент найден } else Result := Self: end: function TVanInfo.FindElem(const sN: stning): TVarlnfo: { Функция поиска элемента в бинарном дереве } var iCnt: integer; begin Result := FindElCnt(sN.iCnt): end: end. Модуль таблицы идентификаторов на основе хэш-адресации в комбинации с бинарным деревом Листинг П3.2. Модуль таблицы идентификаторов на основе хэш-адресации в комбинации с бинарным деревом unit FncTree: interface { Модуль, обеспечивающий работу с комбинированной таблицей идентификаторов, построенной на основе хэш-функции п бинарного дерева } uses TblElem; продолжение &
196 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.2 (продолжение) { Функция начальной инициализации хэш-таблицы } procedure InitTreeVar: { Функция освобождения памяти хэш-таблицы } procedure ClearTreeVar; • { Функция удаления дополнительной информации в таблице } procedure ClearTreelnfo; { Добавление элемента в таблицу идентификаторов } function AddTreeVaг(const sName: string): TVarlnfo: { Поиск элемента в таблице идентификаторов } function GetTreeVar(const sName: string): TVarlnfo; { Функция, возвращающая количество операций сравнения } function GetTreeCount: integer; { Функция записи всех имен идентификаторов в одну строку } function Identl_ist(const sLim.sInp.sOut: string): string: implementation const { Минимальный и максимальный элементы хэш-таблицы } HASH_MIN = Ord('О')+0rd('О'); {(охватывают весь диапазон} HASH_MAX = Ord('z')+Ord('z'): { значений хэш-функции)} var { Массив для хэш-таблицы } HashArray : array[HASH_MIN.,HASH_MAX] of TVarlnfo: iCmpCount : integer; { Счетчик количества сравнений } function GetTreeCount: integer: begin Result := iCmpCount; end; function IdentList(const sLim.sInp.sOut: string): string: { Функция записи всех имен идентификаторов в одну строку } var i: integer; { счетчик идентификаторов } sAdd: string; { строка для временного хранения данных } begin Result := "; { Первоначально строка пуста } for i:=HASH_MIN to HASH_MAX do begin { Цикл по всем идентификаторам в таблице } { Если ячейка таблицы пустая, то добавлять не нужно. } if HashArray[i] = nil then sAdd := " { иначе вычисляем добавочную часть строки } else sAdd := HashArray[i].GetElList(sLim.sInp.sOut); i f sAdd <> '' then begin { Если добавочная часть строки не пуста.
ftalaHausqv 197 Модуль таблицы идентификаторов на основе хэш-адресации то добавляем ее в общую строку через разделитель } If Result <> '' then Result := Result + sLim + sAdd else Result := sAdd; end; end{for}-, end; function VarHash(const sName; string); longint; { Хэш-функция - сумма кодов первого и среднего символов } begin Result := (Ord(sName[l]) + Ord(sName[(l_ength(sName)+l) div 2]) - HASH_MIN) mod (HASH_MAX-HASH_MIN+1)+HA5H_MIN; if Result < HASH_MIN then Result ;= HASH_MIN; end; procedure InitTreeVar; {Начальная инициализация хэш-таблицы - все элементы пусты} var i: integer; begin for i;=HASH_MIN to HASH_MAX do HashArrayfi] ;= nil; end; procedure ClearTreeVar; { Освобождение памяти для всех элементов хэш-таблицы } var i: integer; begin for i;=HASH_MIN to HASH_MAX do begin HashArrayEi].Free; HashArrayEi] : = nil; end: end: , procedure ClearTreelnfo; [ Удаление дополнительной информации для всех элементов } var i: integer; begin for i:=HASH_MIN to HASH_MAX do if HashArrayEi] <> nil then HashArrayEi].ClearAlIJrifo-: end: function AddTreeVar(const sName: string); TVarlnfa: [ Добавление элемента в хэш-таблицу и дерево } var ihash; integer: продолжение &
198 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.2 (продолжение) begin ICmpCount := 0: { Обнуляем счетчик количества сравнений } iHash := VarHash(Upper(sName)); { Вычисляем хэш-адрес } if HashArrayEiHash] <> nil then Result ; = HashArrayEiHash].AddElCnt(sName.ICmpCount) else begin Result := TVarInfo.Create(sName); HashArrayEiHash] := Result; end: end: function GetTreeVar(const sName: string): TVarlnfo; { Поиск элемента в таблице идентификаторов } var iHash: integer; begin ICmpCount := 0; { Обнуляем счетчик сравнений } iHash ;= VarHash(Upper(sName)); { Вычисляем хэш-адрес } if HashArrayEiHash] = nil then Result := nil { Если ячейка по адресу пуста - элемент не найден. } else { иначе вызываем функцию поиска по дереву } Result := HashArrayEiHash].FindElCnt(sName.ICmpCount) end: initialization {Вызов начальной инициализации таблицы при загрузке модуля} InitTreeVar; finalization { Вызов освобождения памяти таблицы при выгрузке модуля } ClearTreeVar: end. Модуль описания всех типов лексем Листинг ПЗ.З. Описание всех типов лексем unit LexType; {!!! Зависит от входного языка !!!} interface { Модуль, содержащий описание всех типов лексем } type TLexType = { Возможные типы лексем в программе } (LEX_PROG. LEX_FIN. LEX SEMI. LEXJF, LEX_OPEN. LEX_CLOSE. LEX-ELSE. LEX_BEGIN. LEX_END. LEX_WHILE. LEX_DO. I_EX_VAR.
^atallaus^ 199 Модуль таблицы идентификаторов на основе хэш-адресации LEX_CONST. LEX_ASSIGN. LEX_OR. LEX_XOR, LEX_AND. LEXJ.T. LEX-GT. LEX_EQ. LEX_NEQ. LEX_NOT. LEX-SUB. LEX-ADD. LEXJJMIN. LEX_START): { Функция получения строки наименования типа лексемы } function LexTypeNamedexT: TLexType): string; { Функция получения текстовой информации о типе лексемы } function LexTypelnfoClexT: TLexType): string: implementation function LexTypeNameClexT: TLexType): string: { Функция получения строки наименования типа лексемы } begin case lexT of LEX_OPEN: Result := ’Открывающая скобка': LEX_CLOSE: Result := 'Закрывающая скобка'; LEX_ASSIGN: Result := 'Знак присвоения': LEX-VAR: Result := 'Переменная'; LEX_CONST: Result := 'Константа'; LEX_SEMI: Result := 'Разделитель'; LEX-ADD.LEX_SUB.LEX-UMIN.LEX-GT.LEX-LT.LEX-EQ. LEX-NEQ: Result := 'Знак операции': else Result := 'Ключевое слово': end: end; function LexTypelnfoClexT: TLexType): string: { Функция получения текстовой информации о типе лексемы } begin case lexT of LEX_PROG: LEX_FIN: Result Result = 'prog': = 'end.': LEX_SEMI: Result = •. . LEX-IF: Result = 'if; LEX_OPEN: Result = '(': LEX_CLOSE: Result = ')’; LEX-ELSE: Result = 'else'; LEX_BEGIN; Result = 'begi n’ LEX-END: Result = 'end'; LEX_WHILE: Result = 'while' LEX_DO: Result = 'do'; LEX-VAR: Result = 'a': LEX CONST: Result = 'C: продолжение &
200 Приложение 3 • Тексты программных модулей для курсовой работы Листинг ПЗ.З (продолжение) LEX_ASSIGN: Result ;= LEX_OR: Result := 'or'; LEX_XOR; Result := 'xor' LEX-AND: Result := 'and' LEX_LT: Result := LEX_GT: Result := '>': LEX-EQ: Result := '=': LEX-NEQ: Result := '<>’: LEX-NOT: Result := 'not' LEX_ADD: Result := '+'; LEX-SUB, LEX-UM1N: Result := else Result :== ”; end; end; end. Модуль описания структуры элементов таблицы лексем Листинг П3.4. Описание структуры элементов таблицы лексем unit LexElem; interface { Модуль, описывающий структуру элементов таблицы лексем } uses Classes. TblElem. LexType; type TLexInfo = record { Структура для информации о лексемах } case LexType: TLexType of LEX_VAR: (Varlnfo: TVarlnfo): LEX_CONST: (ConstVal: integer); LEX_START: (szlnfo: PChar): end: TLexem = class(TObject) { Структура для описания лексемы } protected Lexlnfo: TLexInfo; { Информация о лексеме } { Позиция лексемы в исходном тексте программы } iStr.iPos.iAllP: integer; public { Конструкторы для создания лексем разных типов} constructor CreateKeydexKey: TLexType;
^latattaus,^. 201 Модуль таблицы идентификаторов на основе хэш-адресации iA.1St.1P: integer): constructor CreateVar(VarInf: TVarlnfo; iA.iSt.iP: integer); constructor CreateConst(iVal: integer; iA.iSt.iP; integer); constructor Createlnfo(slnf: string; iA.iSt.iP: integer); destructor Destroy; override; { Свойства для получения информации о лексеме } property LexType: TLexType read Lexlnfо.LexType; property Varlnfo: TVarlnfo read LexInfo.Varlnfo; property ConstVal: integer read Lexlnfo.ConstVal: {Свойства для чтения позиции лексемы в тексте программы] property StrNum: integer read iStr: property PosNum: integer read iPos; property PosAll: integer read iAllP: function LexInfoStr: string; { Строка о типе лексемы } function VarName: string; { Имя для лексемы-переменной } end; TLexList = class(TList) public { Структура для описания списка лексем } { Деструктор для освобождения памяти } destructor Destroy: override; procedure Clear; override: { Процедура очистки списка } { Процедура и свойство для получения лексемы по номеру } function GetLexem(iIdx: integer): TLex£m; property Lexem[i: integer]:TLexem read GetLexem: default: end; implementation uses SysUtils. LexAuto; constructor TLexem.CreateKey(LexKey: TLexType; IA.iSt.iP: integer): { Конструктор создания лексемы типа "ключевое слово" } begin inherited Create: {Вызываем конструктор базового класса] LexInfо.LexType ; = LexKey; { запоминаем тип } iStr := iSt; { запоминаем позицию лексемы } iPos : = iP; iAllP := iA; end; продолжение
202 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.4 (продолжение) constructor TLexem.CreateVar(VarInf: TVarlnfo; 1A.iSt.1P: integer); { Конструктор создания лексемы типа "переменная" } begin inherited Create; {Вызываем конструктор базового класса) Lexlnfo.LexType ;= LEX_VAR; { тип - "переменная" } { запоминаем ссылку на таблицу идентификаторов } Lexlnfo.Varlnfo : = Varlnf; iStr := iSt; { запоминаем позицию лексемы } iPos := IP; 1A11P : = 1А; end: constructor TLexem.CreateConsttiVal; integer; 1A.iSt.iP; integer); { Конструктор создания лексемы типа "константа" } begin inherited Create; {Вызываем конструктор базового класса) LexInfo.LexType ;= LEX_CONST; { тип - "константа" ) { запоминаем значение константы } Lexlnfo.ConstVal : = iVal; IStr := ISt; { запоминаем позицию лексемы ) IPOS := IP: 1A11P := 1A; end; constructor TLexem.CreateInfo(sInf: string; 1A.iSt.1P: integer); { Конструктор создания информационной лексемы ) begin inherited Create; {Вызываем конструктор базового класса) Lexlnfo.LexType : = LEXSTART; { тип - "доп. лексема" } { выделяем память для информации } Lexlnfo.szlnfo := StrAHoc(Length(sInf)+1); StrPCopy(LexInfo.szInfo.sInf); { запоминаем информацию ) IStr := ISt; { запоминаем позицию лексемы ) IPOS := IP: 1А11Р := iA; end; destructor TLexem.Destroy; { Деструктор для удаления лексемы ) begin {Освобождаем память, если это информационная лексема) if LexType = LEX_START then StrDispose(LexInfo.szInfo); Inherited Destroy; {Вызываем деструктор базового класса) end:
^latattaus,^. Модуль таблицы идентификаторов на основе хэш-адресации 203 function TLexem.VarName: string: { Функция получения имени лексемы типа "переменная" } begin Result := Varlnfo.VarName; end; function TLexem.LexInfoStr: string; { Текстовая информация о типе лексемы } begin case LexType of { Выбор информации по типу лексемы } LEXVAR: Result := VarName: {для переменной - ее имя} LEX_CONST: Result : = IntToStr(ConstVal); { для константы - значение } LEX_START: Result := StrPasCLexInfo.szInfo): { для инф. лексемы - информация } else Result := LexTypeInfo(LexType); { для остальных - имя типа } end: end; procedure TLexList.Clear; { Процедура очистки списка } var i: integer; begin { Уничтожаем все элементы списка } for i:=Count-l downto 0 do Lexem[i].Free; inherited Clear; { вызываем функцию базового класса } end: destructor TLexList.Destroy: {Деструктор для освобождения памяти при уничтожении списка} begin Clear; { Уничтожаем все элементы списка } inherited Destroy; {Вызываем деструктор базового класса} end; function TLexList.GetLexem(iIdx: integer); TLexem; { Получение лексемы из списка по ее номеру } begin Result ;= TLexem(Items[iIdx]); end; end. Модуль заполнения таблицы лексем по исходному тексту программы Листинг П3.5. Заполнение таблицы лексем по исходному тексту программы unit LexAuto; {!!! Зависит от входного языка !!!} interface продолжение &
204 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.5 (продолжение) { Модуль построения таблицы лексем по исходному тексту } uses Classes. TblElem. LexType. LexElem: { Функция создания списка лексем по исходному тексту } function MakeLexList(11stFile: TStrings; UstLex: TLexList): integer: implementation uses SysUtils, FncTree; type {Перечень всех возможных состояний конечного автомата} TAutoPos = ( AP_START.AP_IF1.AP_IF2.AP_NOT1,AP_N0T2.AP_N0T3. APJLSEl. AP_ELSE2. APJLSE3. APJLSE4. AP_END2. AP_END3. AP_PROG1.AP_PR0G2.AP_PR0G3.AP_PR0G4.AP_OR1.AP_0R2. AP_BEGINI.AP_BEGIN2.AP_BEGIN3.AP_BEGIN4.AP_BEGIN5. AP_XOR1.AP_X0R2.AP_X0R3.AP_AND1.AP_AN02.AP_AND3. AP_WH IL El. AP_WH ILE2. AP_WH ILE3. AP_WH ILE4. AP_WH IL E5. AP_COMM.AP_COMMSG.AP_ASSIGN.AP_VAR.AP_CONST. AP_D01. AP_002. AP_SIGN. AP_LT. AP_F IN. AP_ERR); function MakeLexList(listFile: TStrings: UstLex: TLexList): integer; { Функция создания списка лексем по исходному тексту } var 1 .j.ICnt.iStr. { Переменные и счетчики циклов } 1А11.{ Счетчик общего количества входных символов } { Переменные для запоминания позиции начала лексемы } IStComm.iStart: integer: posCur: TAutoPos:{ Текущее состояние конечного автомата } sCurStr.sTmp: string: { Строки для временного хранения } { Несколько простых процедур для работы со списком лексем } procedure AddVarToList(posNext: TAutoPos: IP: integer): { Процедура добавления переменной в список } begin { Выделяем имя переменной из текущей строки } sTmp := System.Copy(sCurStr.iStart.IP-iStart): { При создании переменной сначала она заносится в таблицу идентификаторов, а потом ссылка на нее - в таблицу лексем } 1 i stLex.Add(TLexem.CreateVa г(AddTreeVa r(sTmp). IStComm.i.iStart)): iStart := j; iStComm : = 1A11-1; posCur := pOSNext; end:
yataltaus^i' 205 Модуль таблицы идентификаторов на основе хэш-адресации procedure AddVarKeyToList(keyAdd: TLexType; posNext; TAutoPos); { Процедура добавления переменной и разделителя в список } begin { Выделяем имя переменной из текущей строки } sTmp := System.Copy(sCurStr.iStart.j-iStart); { При создании переменной сначала она заносится в таблицу идентификаторов, а потом ссылка на нее - в таблицу лексем } 1i stL ex.Add(TLexem.C rea teVa r(AddTreeVa r(sTmp). IStComm.i.iStart)): . { Добавляем разделитель после переменной } 1 i stLex.Add(TLexem.CreateKey(keyAdd. i Al 1. i. j)); iStart : = j; iStComm := iAll-1; posCur := posNext; end; procedure AddConstToList(posNext: TAutoPos; iP: integer): { Процедура добавления константы в список } begin { Выделяем константу из текущей строки } sTmp :» System.Copy(sCurStr.iStart.iP-iStart); { Заносим константу в список вместе с ее значением } 1i stLex.Add(TLexem.CreateConst(St rToInt(sTmp), iStComm.i.iStart)): iStart := j: iStComm :» iAll-1; posCur := posNext: end: procedure AddConstKeyToList(keyAdd: TLexType: posNext: TAutoPos); { Процедура добавления константы и разделителя в список } begin { Выделяем константу из текущей строки } sTmp := System.Copy(sCurStr.iStart.j-iStart); { Заносим константу в список вместе с ее значением } 1i stLex.Add(TLexem.CreateConst(StrToInt(sTmp).i StComn. i .iStart)); { Добавляем разделитель после константы } 1i stLex.Add(TLexem.CreateKey(keyAdd.iAl 1.i.j)): iStart := j; iStComm := iAll-1; posCur := posNext; end; procedure AddKeyToList(keyAdd: TLexType; posNext: TAutoPos): { Процедура добавления ключевого слова или разделителя } begin listLex.Add(TLexem.CreateKey(keyAdd.iStComm.i .iStert)). продолжение
206 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.5 (продолжение) iStart := j: iStComm : = iAll-1; posCur := posNext: end: procedure Add2KeysToList(keyAddl.keyAdd2: TLexType: posNext: TAutoPos); { Процедура добавления ключевого слова и разделителя } begin 1 istLex.Add(TLexem.CreateKey(keyAddl.iStComm.i.iStart)): listLex.Add(TLexem.CreateKey(keyAdd2.1Al 1.i.j)); iStart := j: iStComm := iAll-1; posCur := posNext: end: procedure KeyLetter(chNext: char; posNext: TAutoPos): { Процедура проверки очередного символа ключевого слова } begin case sCurStr[j] of 1:': AddVarToList(AP_ASSIGN.j): : AddVarKeyToList(LEX_SUB.AP_SIGN): : AddVarKeyToList(LEX_ADD.AP_SIGN); ’=: AddVarKeyToList(LEX_EQ.AP_SIGN); >': AddKeyToList(LEX_GT.AP_SIGN); '<': AddVarToList(AP_LT.j); '(': AddVarKeyToLiSt(LEX_OPEN.AP_SIGN); ')': AddVarKeyToLiSt(LEX_CLOSE.AP_START); ';‘: AddVarKeyToList(LEX_SEMI,AP_START); ’{’: AddVarToList(AP_COMM.j); ’.#10.#13.#9: AddVarToList(AP_START.j); else if sCurStr[j] = chNext then posCur := posNext else if sCurStr[j] in [’0'..'9'.'A'..'Г .’a'..’z‘. then posCur := AP_VAR else posCur := AP_ERR: end{case list}; end: procedure KeyFinish(keyAdd: TLexType): { Процедура проверки завершения ключевого слова } begin case sCurStr[j] of 1 -': Add2KeysToList(keyAdd.LEX_(JMIN.AP_SlGN): '+': Add2KeysToList(keyAdd.LEX_ADD.AP_SIGN); •=’: Add2KeysToList(keyAdd.LEX_EQ.AP_SIGN); >’: Add2KeysToList(keyAdd.LEX_GT.AP_SIGN):
^alallaustlk Модуль таблицы идентификаторов на основе хэш-адресации 207 ’<': AddKeyToL i st(keyAdd.AP_LT); Add2KeysToList(keyAdd.LEX_0PEN.AP_SIGN); •)': Add2KeysToList(keyAdd.LEX_CL0SE.AP_START): ';': Add2KeysToList(keyAdd.LEX_SEMI,AP_START); 'O'..'9'.'A'..'Z'.’a'..'z'.; posCur : = AP_VAR; '{': AddKeyToL1 st(keyAdd.AP_COMMSG): ’ ' .#10.#13.#9: AddKeyToL1st(keyAdd.AP_SIGN); else posCur : = AP_ERR; end{case list}; end; begin { Тело главной функции } iAll ;= 0; { Обнуляем общий счетчик символов } Result :== 0; { Обнуляем результат функции } posCur := AP-START;[Устанавливаем начальное состояние КА} iStComm : = 0: iCnt := 1istFile.Count-1; for i:=0 to iCnt do {Цикл по всем строкам входного файла} begin iStart ; = 1; { Позиция начала лексемы - первый символ } sCurStr ;= listFile[i]; { Запоминаем текущую строку } iStr ;= Length(sCurStr); for j:=l to iStr do { Цикл по символам текущей строки } begin Inc(iAll): { Увеличиваем общий счетчик символов } { Моделируем работу конечного автомата в зависимости от состояния КА и текущего символа входной строки } case posCur of AP-START: begin { В начальном состоянии запоминаем позицию начала лексемы } iStart := j; iStComm := iAll-1; case sCurStr[j] of 'O': posCur ;= AP-BEGINl; 'i'; posCur := AP_IF1; 'p': posCur ;= AP_PROG1; 'e': posCur := AP-ELSEl; ’w’: posCur := AP-WHILEl; 'd': posCur := AP_D01: 'o'; posCur := AP-ORl; 'x': posCur := AP-XORl; 'a'; posCur : = AP_AND1: 'n'; posCur := AP-NOTl; ':': posCur ;= AP_ASSIGN; AddKeyToList(LEX_SUB.AP_SIGN); продолжение &
208 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.5 (продолжение) ’ + ': AddKeyToLiSt(LEX_ADD.AP_SIGN); ' = ': AddKeyToList(LEX_EQ.AP_SIGN); •>': AddKeyToLiSt(LEX_GT.AP_SIGN); ’<': posCur := AP_LT: '(': AddKeyToList(LEX_OPEN,AP_SIGN): ')': AddKeyToList(LEX_CLOSE.AP_START); ;': AddKeyToLiSt(LEX_SEMI,AP_START); '0'..'9'; posCur := AP_CONST; 'A'..'Z'.'c'.'f'..'h'.'j'..’m'. 'q'..'v*.'y'.'z'.: posCur : = AP_VAR; posCur := AP_COMM; ' ‘.#10.#13.#9; ; else posCur : = AP_ERR; end{case list}; end; AP_SIGN; begin { Состояние, когда может встретиться унарный минус } iStart := j: iStComm ;= iAll-1; case sCurStr[j] of 'b': posCur := AP_BEGIN1; 'i’: posCur ;= AP_IF1; 'p'; posCur ;= AP_PROG1; 'e'; posCur ;= AP_ELSE1; 'w': posCur := AP_WHILE1: 'd': posCur : = AP_D01: 'O': posCur ;= AP_OR1; 'x': posCur ;= AP_XOR1; ’a': posCur := AP_AND1; ’П': posCur := AP_NOT1; : AddKeyToList(LEX_UMIN.AP_SIGN); • ('; AddKeyToLiSt(LEX_OPEN.AP_SIGN); ')': AddKeyToList(LEX_CLOSE.AP_START); '0'..'9': posCur := AP_CONST; 'A'..'Z'.'c'.'f'..'h'.'j'..'m'. ’q'..'v'.'y'.'z'.: posCur := AP_VAR; '{': posCur := AP_COMMSG; ' '.#10.#13.#9: ; else posCur := AP_ERR; end{case list}; end; AP_LT; { Знак меньше или знак неравенства? } case sCurStrfj] of
^atattaus,^. Модуль таблицы идентификаторов на основе хэш-адресации 209 b1: AddKeyToList(LEX__LT.AP_BEGINl); 'I': AddKeyToLiSt(LEX_LT.AP_IFl); р•: AddKeyToList(LEX_LT.AP_PROG1); 'e': AddKeyToList(LEX_LT.AP_ELSEl); ’w’: AddKeyToList(LEX_LT.AP_WHILEI): d: AddKeyToLi st(LEX_LT.AP_DO1); 'o': AddKeyToList(LEXJ_T.AP_OR1); x1: AddKeyToList(LEX_LT.AP_XORl); 'a': AddKeyToList(LEX_LT.AP_ANDl): n’: AddKeyToLiSt(LEX_LT.AP_NOT1); : AddKeyToList(LEX_NEQ.AP_SIGN); : Add2KeysToList(LEX_LT.LEX_l)MIN.AP_SIGN): ’(1: Add2KeysToLiSt(LEX_LT.LEX_OPEN.AP_SIGN); ’0’..•9•: AddKeyToLi St(L E X_LT.AP_CONST); ’A’.. *Z". *c'. 'f ..'h'.'j‘..'rn‘.‘q'..'v'. 'y1. ’z’: AddKeyToList(LEX_LT.AP_VAR): 1{1: AddKeyToList(LEX_LT.AP_C0MMSG); ' 1.#10.#13.#9: AddKeyToLiSt(LEX_LT.AP_SIGN); else posCur := AP_ERR; end{case list}; AP_ELSE1: { "else", или же "end”, или переменная? } case sCurStr[j] of '1’: posCur := AP_ELSE2; ’n’: posCur := AP_END2; ': AddVarToList(AP_ASSIGN.j); '-’: AddVarKeyToLiSt(LEX_SUB.AP_SIGN); '+: AddVarKeyToLiSt(LEX_ADD.AP_SIGN); '=': AddVarKeyToLiSt(LEX_EQ.AP_SIGN); ’>•: AddKeyToLiSt(LEX_GT.AP_SIGN); AddVa^ToLisUAP-LT.j); '('; AddVarKeyToList(LEX_OPEN.AP_SIGN): ')': AddVarKeyToList(LEX_CLOSE,AP_START); 1;’: AddVarKeyToLiSt(LEX_SEMI.AP_START); AddVarToLiSt(AP_COMM.j); 'O'.. ,,9'. 'A'.. 'Z'. 'a'.. 'k'.'m'. 'o'..'Z'.: posCur := AP_VAR; 1 '.#10.#13.#9: AddVarToListCAP_START.j); else posCur := AP_ERR; end{case list}; AP_IF1: KeyLetterE’f*,AP_IF2): APJF2: KeyFinish(LEX_IF); AP_ELSE2: KeyLetterC’s',AP_ELSE3); AP_ELSE3: KeyLetterC'e1.AP_ELSE4); продолжение &
210 Приложение 3* Тексты программных модулей для курсовой работы Листинг П3.5 (продолжение) AP_ELSE4: KeyFlnish(LEXELSE): AP_OR1: KeyLetterC’ г'.AP_0R2); AP_0R2: KeyF1nish(LEX_0R): AP_D01: KeyLetter('o',AP_D02): AP_D02: KeyFinish(LEX_DO); APJCORl: KeyLetterC'o'.AP_X0R2); AP_X0R2: KeyLetterC’r'.AP_X0R3): AP_X0R3: KeyFinish(LEX_XOR); AP_AND1: KeyLetterC'n',AP_AND2): AP_AND2: KeyLetter('d’.AP_AND3): AP_AND3: KeyFinish(LEX_AND); AP_NOT1: KeyLetter('o'.AP_N0T2); AP_N0T2: KeyLetter('t'.AP_N0T3); AP_N0T3: KeyFinish(LEX_NOT); AP_PROG1: KeyLetter('r'.AP_PR0G2); AP_PR0G2: KeyLetter('o'.AP_PR0G3): AP_PR0G3: KeyLetter('g'.AP_PR0G4); AP_PR0G4: KeyFinish(LEX_PROG); AP_WHILE1: KeyLetterC’h’ ,AP_WHILE2); AP_WHILE2: KeyLetterC'i',AP_WHILE3): AP_WHILE3: KeyLetterC'Г,AP_WHILE4); AP_WHILE4: KeyLetterC'e',AP_WHILE5): AP_WHILE5: KeyFinish(LEX_WHILE); AP_BEGIN1: KeyLetterC'e',AP_BEGIN2): AP_BEGIN2: KeyLetterC'g',AP_BEGIN3); AP-BEGIN3: KeyLetterC'i',AP_BEGIN4); AP_BEGIN4: KeyLetterC'n',AP_BEGIN5); AP_BEGIN5: KeyFinish(LEX_BEGIN); AP_END2: KeyLetterC'd'.AP_END3); AP_END3: { "end", или же "end.", или переменная? } case sCurStrEj] of : Add2KeysToLiSt(LEX_END.LEX_UMIN.AP_SIGN); '+': Add2KeysToList(LEX_END.LEX_ADD.AP_SIGN); : Add2KeysToLiSt(LEX_END.LEX_EQ.AP_SIGN); '>': Add2KeysToLiSt(LEX_EN0.LEX_GT.AP^SIGN); '<’: AddKeyToLiSt(LEX_END.AP_LT); '(': Add2KeysToList(LEX_END.LEX_0PEN.AP_SIGN); ')‘:Add2KeysToList(LEX_END.LEX_CL0SE.AP_START): ':’: Add2KeysToList(LEX_END.LEX_SEMI.AP_START); '.': AddKeyToLiSt(LEX_FIN,AP_START); ,0'..'9'.,A'..'Z'.'a'..'z'.'_': posCur := AP-VAR; '{': AddKeyToLIst(LEX END.AP_COMMSG);
^alatfaus^ 211 Модуль таблицы идентификаторов на основе хэш-адресации • •.#10.#13.#9: AddKeyToLiSt(LEX_END.AP_SIGN); else posCur : = APERR: end{case list}: AP_ASSIGN: ( Знак присваивания } case sCurStr[j] of '=•: AddKeyToLiSt(LEX_ASSIGN.AP_SIGN); else posCur : = AP_ERR; end{case list}: AP_VAR: { Переменная } case sCurStrEj] of ‘ : AddVarToList(AP__ASSIGN. j); : AddVarKeyToList(LEX_SUB,AP_SIGN): • +': AddVarKeyToList(LEX_ADO.AP_SIGN): • =': AddVarKeyToList(LEX_EQ.AP_SIGN): • >': AddVarKeyToLiSt(LEX_GT.AP_SIGN): AddVarToList(AP_LT.j); ‘: AddVarKeyToList(LEX_OPEN.AP_SIGN): 1)': AddVarKeyToList(LEX_CLOSE.AP_START): •:': AddVarKeyToList(LEX_SEMI.AP_START): ’O'..’9'.'A'..'Z'.'a'..'z',: posCur := APJ/AR; ‘ ‘: AddVarToLiSt(AP_COMM.j); • '.#10.#13.#9: AddVarToList(AP_START.j): else posCur := AP_ERR; endfcase list}: AP_CONST: { Константа } case sCurStrEj] of ':': AddConstToLiSt(AP_ASSIGN.j): ' -': AddConstKeyToList(LEX_SUB.AP_SIGN): : AddConstKeyToList(LEX_ADD.AP_SIGN); '=': AddConstKeyToList(LEX_EQ.AP_SIGN): >': AddConstKeyToList(LEX_GT.AP_SIGN); AddConstToLiSt(AP_LT.j): ' (': AddConstKeyToList(LEX_OPEN.AP_SIGN); ') ’: AddConstKeyToList(LEX_CLOSE.AP_START); ’ :': AddConstKeyToList(LEX_SEHI,AP_START): posCur := AP_CONST; AddConstToList(AP_COMM.j); ' '.#10.#13.#9: AddConstToList(AP_START.j); else posCur := AP_ERR; end{case list}; AP-COMM: { Комментарий с начальной позиции } case sCurStr[j] of продолжение &
212 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.5 (продолжение) ’}’: posCur := AP_START; end{case list}: AP_COMMSG: { Комментарий после знака операции } case sCurStr[j] of ’}': posCur : = AP_SIGN: end{case list}; end{case pos}: if j = iStr then { Проверяем конец строки } begin { Конец строки - это конец текущей лексемы } case posCur of AP_IF2:•AddKeyToLi st(LEXJ F.AP_SIGN): AP_PR0G4: AddKeyToList(LEX_PROG.AP_START); AP_ELSE4: AddKeyToLiSt(LEX_ELSE.APJSTART): AP_BEGIN5: AddKeyToList(LEX_BEGIN.AP_START): AP_WHILE5: AddKeyToLiSt(LEX_WHILE.AP_SIGN); AP_END3: AddKeyToList(LEX_END.AP_START); AP_0R2: AddKeyToList(LEX_OR.AP_SIGN): AP_DO2: AddKeyToL i st(LEX_DO.AP_SIGN): AP_XOR3: AddKeyToLiSt(LEX_XOR.AP_SIGN): AP_AND3: AddKeyToList(LEX_AND.AP_SIGN): AP_N0T3: AddKeyToLi st(LEX_AND.AP_SIGN): AP_LT: AddKeyToList(LEX_LT.AP_SIGN): AP_FIN: AddKeyToList(LEX_FIN.AP_START); AP_CONST: AddConstToL i st(AP_START. j+1): AP_ASSIGN: posCur := AP_ERR; APJFl.AP_PROG1.AP_PR0G2.AP_PR0G3. AP_ELSE1.AP_ELSE2.AP_ELSE3.AP_XOR1.AP_X0R2. AP_OR1.AP_DO1.AP_AND1.AP_AND2.AP_NOT1.AP_N0T2. AP_WHILE1.AP_WHILE2.AP_WHILE3.AP_WHILE4. AP_END2.AP_BEGINI.AP_BEGIN2.AP_BEGIN3.AP_BEGIN4. AP_VAR: AddVa rToL i st(AP_START.j+1): end{case pos2}; end: if posCur = AP_ERR then {Проверяем, не было ли ошибки} begin { Вычисляем позицию ошибочной лексемы } iStart := (j - iStart)+l: { Запоминаем ее в виде фиктивной лексемы в начале списка } listLex.Insert(0.{для детальной диагностики ошибки} TLexem.CreatelnfoC’Недопустимая лексема’. iAll-iStart.i.iStart)): Break: { Если ошибка, прерываем цикл } end; end{for j}:
Модуль таблицы идентификаторов на основе хэш-адресации 213 Inc(iAll.2): { В конце строки увеличиваем общий счетчик символов на 2: конец строки и возврат каретки } if posCur = AP_ERR then {Если ошибка, запоминаем чекер} begin { ошибочной строки и прерываем цикл } Result : = 1+1: Break; end: end{for i}; if posCur in [AP_COMM.AP_COMMSG] then begin { Если комментарий не был закрыт, то это отгибка } listLex.Insert(0. TLexem.CreateInfo('Незакрытый комментарий'. iStComm.iCnt.iAll-iStComm)); Result := iCnt; end else if not (posCur in [AP_START,AP_SIGN.AP_ERR]) then begin {Если KA не в начальном состоянии -} 1istLex.Insert(0. {это неверная лексема} TLexem.CreateInfo('Незавершенная лексема'. i Al 1 -iStart.i Cnt.i Start)): Result := iCnt: end: end; end. Модуль описания матрицы предшествования и правил исходной грамматики Листинг П3.6. Описание матрицы предшествования ы правил исходной грамматики unit SyntRule: {!!! Зависит от входного языка !!!] interface { Модуль, содержащий описание матрицы предшествования и правил грамматики } uses LexType. Classes; const { Максимальная длина правила } RULE_LENGTH =7; { (в расчете на символы грамматично ] RULE_NUM = 28: { Общее количество правил грамматичп } Var { Матрица операторного предшествования } GramMatrix: агray[TLexType,TLexType] of char = ( {pr. end. : if ( ) else beg end whl do a с := зг xor and < > = <> not - + um ’ } продолжение
214 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.6 (продолжение) {pr.} (• ' {end.}( {if} ( {(} ( {)} ( {else}( {beg.}(1 {end} (‘ {whil}( {do} ( {a} ( {c} ( {or} ( {xor} (' {and} ( {not} ( {um} (
^alattaus,^. 215 Модуль таблицы идентификаторов на основе хэш-адресации {!} (•<•.' •/ ')); { Правила исходной грамматики } GramRules: array[l.,RULE_NUM] of string = (’progEend.’. ’E'.'E;E’.'E;'.'if(B)EelseE'.’ 1 f(B)E'. ’beginEend'.‘while(B)doE‘.'a:=E'.‘BorB’.'BxorB'.’B’. 'BandB'.'B'.'E<E'.’E>E'.'E=E'.’E<>E’.'(B)'.’notCB)’. 'E-E'.'E+E*.'E'.'-E’.'E'.’(E)'.'a'.'c'): { Функция имени нетерминала для каждого правила } function MakeSymbolStr(iRuleNum: integer): string; { Функция корректировки отношений предшествования для расширения матрицы предшествования } function CorrectRule(cRule: char: lexTop.lexCur: TLexType; symbStapk: TList): char: implementation uses SyntSymb; function MakeSymbolStr(iRuleNum: integer): string: begin if iRuleNum in [10..20] then Result := 'B' else Result := 'E': end; function CorrectRule(cRule: char: lexTop.lexCur: TLexType: symbStack: TList): char: var j: integer; begin { Корректируем отношение для символа "else". если в стеке не логическое выражение } Result := cRule; if (cRule = '=') and (lexTop = LEX_CLOSE) and (lexCur = LEX_ELSE) then begin j := TSymbStack(symbStack).Count-l; if (j > 2) and (TSymbStack(symbStack)[j-2].SymbolStr <> 'B') then Result := ’>’; end; end; end.
216 Приложение 3 • Тексты программных модулей для курсовой работы Модуль описания структур данных синтаксического анализатора и реализации алгоритма «сдвиг-свертка» Листинг П3.7. Описание структур данных синтаксического анализатора и реализация алгоритма «сдвиг-свертка» unit SyntSymb: interface { Модуль, обеспечивающий выполнение функций синтаксического разбора с помощью алгоритма "сдвиг-свертка" } uses Classes. LexElem. SyntRule; { Типы символов: терминальные (лексемы) и нетерминальные } type TSymbKind = (SYMB_LEX. SYMB_SYNT); TSymblnfo = гесогб{Структура данных для символа грамматики} case SymbType: TSymbKind of { Тип символа } { Для терминального символа - ссылка на лексему } SYMB_LEX: (LexOne: TLexem); { Для нетерминального символа - ссылка на список символов, из которых он был построен } SYMB_SYNT: (LexList: TList); end; TSymbol = class; {Предварительное описание класса "Символ"} { Массив символов, составляющих правило грамматики } TSymbArray = array[0..RULE_LENGTH] of TSymbol; TSymbol = class(TObject) protected { Структура, описывающая грамматический символ } Symblnfo: TSymblnfo; { Информация о символе } iRuleNum: integer: {Номер правила, которым создан символ} public { Конструктор создания терминального символа по лексеме } constructor CreateLex(Lex: TLexem); { Конструктор создания нетерминального символа } constructor CreateSymbCiR.iSymbN; integer; const SymbArr: TSymbArray); { Деструктор для удаления символа } destructor Destroy; override; {Функция получения символа из правила по номеру символа} function Getltem(iIdx: integer); TSymbol;
217 Модуль описания структур данных синтаксического анализатора { Функция получения количества символов в правиле } function Count: integer: { Функция, формирующая строковое представление символа } function Symbol Str: string; { Свойство, возвращающее тип символа } property SymbType: TSymbKind read Symblnfo.SymbType: {Свойство "Ссылка на лексему" для терминального символа} property Lexem: TLexem read Symblnfo.LexOne: { Свойство, возвращающее символ правила по номеру } property Itemsfi: integer]:TSymbol read Getltem: default; { Свойство, возвращающее номер правила } property Rule: integer read iRuleNum; end; TSymbStack = class(TList) public { Структура, описывающая синтаксический стек } destructor Destroy; override; { Деструктор для стека } procedure Clear; override; { Функция очистки стека } { Функция выборки символа по номеру от вершины стека } function GetSymboKildx: integer): TSymbol; { Функция помещения в стек входящей лексемы } function Push(lex; TLexem); TSymbol; { Свойство выборки символа по номеру от вершины стека } property Symbols[iIdx; integer]: TSymbol read GetSymbol: default; { Функция, возвращающая самую верхнюю лексему в стеке } function TopLexem: TLexem; { Функция, выполняющая свертку и помещающая новый символ на вершину стека } function MakeTopSymb: TSymbol; end; { Функция, выполняющая алгоритм "сдвиг-свертка" } function BuiIdSyntListCconst listLex; TLexList: symbStack; TSymbStack): TSymbol; implementation uses LexType. LexAuto: constructor TSymbol.CreateLex(Lex: TLexem); { Создание терминального символа на основе лексемы } begin inherited Create: { Вызываем конструктор базового класа } продолжение &
218 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.7 (продолжение) Symblnfo.SymbType := SYMB_LEX;{Ставим тип "терминальный"} Symblnfo.LexOne := Lex; { Запоминаем ссылку на лексему } iRuleNum : = 0: { Правило не используется, поэтому "0" } end; constructor TSymbol.CreateSymb(iR{HOMep правила). iSymbN{количество исходных символов}: integer: const SymbArr: TSymbArray{Массив исходных символов}); { Конструктор создания нетерминального символа на основе правила и массива символов } var 1; integer; begin inherited Create; { Вызываем конструктор базового класа } { Тип символа "нетерминальный" } Symblnfo.SymbType := SYMB_SYNT; { Создаем список для хранения исходных символов } Symblnfo.LexList ;= TList.Create; {Переносим исходные символы в список в обратном порядке} for i:=iSymbN-l downto 0 do Symblnfo.LexLi st.Add(SymbArr[i]): iRuleNum := iR; { Запоминаем номер правила } end; function TSymbol.Getltem(iIdx: integer): TSymbol; { Функция получения символа из правила по номеру символа } begin Result := TSymbol(Symblnfo.LexList[iIdx]) end; function TSymbol.Count: integer; { Функция, возвращающая количество символов в правиле } begin Result := Symblnfo.LexList.Count; end:' function TSymbol.SymbolStr: string: { Функция, формирующая строковое представление символа } begin { Если это нетерминальный символ, формируем его представление в зависимости от номера правила } if SymbType = SYMB_SYNT then Result := MakeSymbolStr(iRuleNum) { Если это терминальный символ, формируем его представление в соответствии с типом лексемы } else Result : = Lexem.LexInfoStr: end: destructor TSymbol.Destroy;
Модуль описания структур данных синтаксического анализатора 219 { Деструктор для удаления символа } var 1: integer: begin if Symblnfo.SymbType = SYMB_SYNT then with Symblnfo.LexList do begin { Если это нетерминальный символ. } { удаляем все его исходные символы из списка } for i:=Count-l downto 0 do TSymbol(Iterns[i]).Free; Free: [ Удаляем сам список символов,} end: inherited Destroy; { Вызываем деструктор базового класа } end; destructor TSymbStack.Destroy: { Деструктор для удаления синтаксического стека } begin Clear; { Очищаем стек } inherited Destroy: { Вызываем деструктор базового класа } end; procedure TSymbStack.Clear; { Функция очистки синтаксического стека } var i: integer; begin [ Удаляем все символы из стека } for i:=€ount-l downto 0 do TSymbol(Items[i]).Free: inherited Clear; { Вызываем функцию базового класса } end; function TSymbStack.GetSymboKildx; integer]: TSymbol; { Функция выборки символа по номеру от вершины стека } begin Result : = TSymbol(Iterns[iIdx]); end; function TSymbStack.TopLexem: TLexem; { Функция, возвращающая самую верхнюю лексему в стеке } var i: integer; begin Result := nil; { Начальный результат функции пустой } for i:=Count-l downto 0 бо{Для символов от вершины стека} if Symbols[i].SymbType = SYMBLEX then begin { Если это терминальный символ } Result := Symbols[i].Lexem; {Беррм ссылку на лексему} Break; [ Прекращаем поиск } end; end; продолжение
220 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.7 (продолжение) function TSymbStack.Push(lex; TLexem): TSymbol; { Функция помещения лексемы в синтаксический стек } begin { Создаем новый терминальный символ } Result .:= TSymbol.CreateLex(lex): Add(Result); { Добавляем его в стек } end; function TSymbStack.MakeTopSymb; TSymbol: { Функция, выполняющая свертку. Результат функции: nil - если не удалось выполнить свертку, иначе - ссылка на новый нетерминальный символ (если свертка выполнена).} van symCur: TSymbol; {Текущий символ стека} SymbArr: TSymbArray;{Массив хранения символов правила} i.iSymbN: integer;{Счетчики символов в стеке и в правиле} sRuleStr: string: {Строковое представление правила} { Функция добавления символа в правило } procedure AddToRule(const sStr; string.{Строка символа} sym: TSymbol{Тек. символ}); begin symCur := sym; { Устанавливаем ссылку на текущий символ } { Добавляем очередной символ в массив символов правила } SymbArr[iSymbN] := Symbols[i]; { Добавляем его в строку правила (слева!) } sRuleStr ;= sStr + sRuleStr; Deleted); { Удаляем символ из стека } Inc(iSymbN); { Увеличиваем счетчик символов в правиле } end; begin Result := nil; { Сначала’обнуляем результат функции } iSymbN ;= 0: { Сбрасываем счетчик символов } symCur ;= nil; { Обнуляем текущий символ } sRuleStr := "; { Сначала строка правила пустая } for i;=Count-l downto 0 do{ Выполняем алгоритм } begin { Для всех символов начиная с вершины стека } if Symbols[i].SymbType = SYMB SYNT then { Если это нетерминальный символ, то добавляем его в правило, текущий символ при этом не меняется } AddToRu1е(Symbols[i].Symbol St r.symCur) else { Если это терминальный символ } if symCur = nil then {и текущий символ пустой } { Добавляем его в правило и делаем текущим } AddT oRulе(LexTyреInfо(Symbols[i].Lexem.LexType).
221 Модуль описания структур данных синтаксического анализатора Symbols[i]) else { Если это терминальный символ и он связан отношением ”=" с текущим символом } 1 f GramMatriх[Symbols[i].Lexem.LexType. symCur.Lexem.LexType] = '=' then { Добавляем его в правило и делаем текущим } AddT oRu1е(LexTyреInfо(Symbol s[1].Lexem.LexType). Symbols[i]) else { Иначе - прерываем цикл, дальше искать не нужно } Break: if ISymbN > RULE_LENGTH then Break: { Если превышена максимальная длина правила, цикл прекращаем } end: if ISymbN <> 0 then begin { Если выбран хотя бы один символ из стека, то ищем простым перебором правило, у которого строковое представление совпадает с построенной строкой } for i: =1 to RULE_NUM do if GramRules[i] = sRuleStr thenfEcnw правило найдено.} begin { создаем новый нетерминальный символ } Result := TSymbol.CreateSymb(i. i SymbN.SymbArr); Add(Result): { и добавляем его в стек. } Break; { Прерываем цикл поиска правил } end: { Если не был создан новый символ (правило не найдено). надо удалить все исходные символы, это ошибка } if Result = nil then for i:=0 to iSymbN-1 do SymbArr[1].Free; end: end; function Bui 1dSyntList( const UstLex: TLexList{BxoflHafl таблица лексем}: symbStack: TSymbStack{cTeK для работы алгоритма} ): TSymbol: { Функция, выполняющая алгоритм "сдвиг-свертка". Результат функции: - нетерминальный символ (корень синтаксического дерева), если разбор был выполнен успешно; - терминальный символ, ссылающийся на лексему, где была обнаружена ошибка, если разбор выполнен с ошибками. } var i.iCnt: integer: {счетчик лексем и длина таблицы лексем} продолжение &
222 Приложение 3* Тексты программных модулей для курсовой работы Листинг П3.7 (продолжение) 1 exStop: TLexem: { Ссылка на начальную лексему } lexTCur: TLexType: { Тип текущей лексемы } cRule: char:{ Текущее отношение предшествования } begin Result := nil; { Сначала результат функции пустой } iCnt := listLex.Count-1: { Берем длину таблицы лексем } { Создаем дополнительную лексему "начало строки" } lexStop :- TLexem.CreatelnfoC'Начало файла'.0.0.0); try { Помещаем начальную лексему в стек } symbStack.Push(1 exStop); i := 0: { Обнуляем счетчик входных лексем } while i<=iCnt do { Цикл по всем лексемам от начала } begin { до конца таблицы лексем } { Получаем тип лексемы на вершине стека } lexTCur :- symbStack.TopLexem.LexType; { Если на вершине стека начальная лексема. а текущая лексема - конечная, то разбор завершен } if (lexTCur = LEX_START) and (TistLexEiJ.LexType = LEX_START) then Break: { Смотрим отношение лексемы на вершине стека и текущей лексемы в строке } cRule := GramMatrix[lexTCur.listLex[i].LexType]; { Корректируем отношение. Если корректировка матрицы предшествования не используется, то функция должна вернуть то же самое отношение } cRule := CorrectRuleCcRule.lexTCur. 1i stLex[i].LexType.symbStack): case cRule of {• Надо выполнять сдвиг (перенос) } begin { Помещаем текущую лексему в стек } symbStack.Push(1istLex[i]); Inc(i): { Увеличиваем счетчик входных лексем } end; ’>': { Надо выполнять свертку } if symbStack.MakeTopSymb = nil then begin { Если не удалось выполнить свертку. } { запоминаем текущую лексему как место ошибки } Result := TSymbol.CreateLex(listLex[i]); Break; { Прерываем алгоритм } end; else { Отношение не установлено - ошибка раэбора } begin {Запоминаем текущую лексему (место ошибки)} Result ;= TSymbol.CreateLex(listLex[i]);
^ataHaus,^. Модуль описания структур данных синтаксического анализатора 223 Break: { Прерываем алгоритм } end: end{case}: end{while}; if Result = nil then { Если разбор прошел без ошибок } begi побеждаемся. что в стеке осталось только 2 символа} if symbStack.Count = 2 then { Если да. то верхний символ - результат разбора } Result := symbStack[l] { Иначе это ошибка - отмечаем место ошибки } else Result : = TSymbol.CreateLex(listLex[iCntJ): end: finally { Уничтожаем временную начальную лексему } 1 exStop.Free: end; end: end. Модуль описания допустимых типов триад Листинг П3.8. Описание допустимых типов триад unit TrdType: {!!! Зависит от входного языка !!!} interface { Модуль для описания допустимых типов триад } const { Имена предопределенных функций и переменных } NAME_PR0G = ’MyCurs': NAME_INPVAR = ’InpVar’: NAME_RESULT = 'Result'; NAME_FUNCT = ’Compi1 eTest’; NAME_TYPE = 'integer'; type { Типы триад, соответствующие типам допустимых операций, а также три дополнительных типа триад; - CONST - для алгоритма свертки объектного кода; - SAME - для алгоритма исключения лишних операций: - NOP (No Operations) - для ссылок на конец списка триад. ] TTri a dType = (TRD_ IF. TRDOR. TRD_XOR. TRD_AND. TRD_N0 T. TRD_LT. TRD_GT. TRD_EQ. TRD_NEQ. TRD_ADD. TRD_SUB. TRDJJHIN. TRD_ASS1GN.TRD-JMP.TRD_CONST.TRD_SAME.TRD_NOP); {Массив строковых обозначений триад для вывода их нз экран! TTriadStr = array[TTriadType] of string; продолжение &
224 Приложение 3 • Тексты программных модулей для курсовой работы Листинг ПЗ.8 (продолжение) const TriadStr: TTriadStr =( ’if'.'or'.’xor’.'and'.'not'. ’:=’.' jmp'.'C.'same'.'nop'): { Множество триад, которые являются линейными операциями } TriadLineSet : set of TTriadType = [TRDOR. TRD_XOR. TRD_AND. TRD_NOT. TRD_ADD. TRD_SUB. TRD_LT. TRD_GT. TRD_EQ. TRD_NEQ. TRDJJMINJ: implementation end. Модуль вычисления значений триад при свертке объектного кода Листинг П3.9. Вычисление значений триад при свертке объектного кода unit TrdCalc: {!!! Зависит от входного языка !!!} interface { Модуль, вычисляющий значения триад при свертке операций } uses TrdType: { Функция вычисления триады по значениям двух операндов } function CalcTriadfTriad: TTriadType: i0pl.i0p2: integer): integer: implementation function CalcTriad(Triad: TTriadType: i0pl.i0p2: integer): integer; { Функция вычисления триады по значениям двух операндов } begin Result := 0: case Triad of TRD_OR: Result := (iOpl or i0p2) and 1; TRD_XOR: Result := CiOpl xor i0p2) and 1; TRD_AND: Result := (iOpl and i0p2) and 1; TRD_NOT: Result := (not iOpl) and 1: TRD_LT: if i0pl<i0p2 then Result := 1 else Result := 0; TRD_GT: if i0pl>i0p2 then Result := 1 else Result := 0: TRD_EQ: if i0pl=i0p2 then Result := 1 else Result := 0: TRD_NEQ: if i0pl<>i0p2 then Result := 1
^aiattaus,^. Модуль описания структур данных триад 225 else Result : = 0: TRD_ADD: Result := IOpl + Юр2: TRD_SUB: Result := IOpl - i0p2: TRDJJMIN: Result : = -i0p2; end; end; end. Модуль описания структур данных триад Листинг П3.10. Описание структур данных триад unit Triads; interface { Модуль, обеспечивающий работу с триадами и их списком } uses Classes. TblElem. LexElem. TrdType; type TTriad = class; { Предварительное описание класса триад } ТОрТуре = (OP_CONST. OP JAR. OP_LLNK); { Типы операндов; константа, переменная, ссылка на другую триаду } TOperand = record { Структура описания операнда в триадах } case ОрТугре; ТОрТуре о’' { Тип операнда } OP_CONST: (ConstVal: integer);{для констан! - значение} OPJ/AR: (VarLink; TVarlnfo!;{ для переменной - ссылка на элемент таблицы идентификаторов } OPLINK: CTriadNum: integer);{ для триады - номер ’ end; TOpArray = аггау[1..2] of 'Operand; {Массив из 2 операндов] TTriad = classCTCbjectl private { Структура данных для описания триада } TriadType: TTriadType; { Тип триады } Operands: TOpArray; { Массив операндов } public Info: Icmgint; { Дополнительная информация для оптимизирующих алгоритмов } IsLinkec: Boolean; { Слаг наличия ссылки на эту триаду } { Конструктор для создания триады } constructor CreateCTyp: 'TriadType: const Ops- ТОз^ггау); продолжение & К Зак_ 58
226 Приложение 3 • Тексты программных модулей для курсовой работу Листинг П3.10 (продолжение) { Функции для чтения и записи операндов } function GetOperanddldx: integer): TOperand: procedure SetOperandd Idx: integer; Op: TOperand): { Функции для чтения и записи ссылок на другие триады } function GetLinkdldx: integer): integer; procedure SetLinkdldx: integer; TrdN: integer); { Функции для чтения и записи типа операндов } function GetOpTypedIdx: integer): TOpType: procedure SetOpTypedldx: integer; OpT: TOpType); { Функции для чтения и записи значений констант } function GetConstValdIdx: integer): integer; procedure SetConstValdIdx: integer; iVal: integer); { Свойства триады, основанные на описанных функциях } property TrdType: TTriadType read TriadType; property Opers[ildx: integer]: TOperand read GetOperand write Setoperand; default: property Links[iIdx: integer]: integer read GetLink write SetLink; property OpTypes[iIdx: integer]: TOpType read GetOpType write SetOpType; property Valuestildx: integer]: integer read GetConstVal write SetConstVal: { Функция, проверяющая эквивалентность двух триад } function IsEqual(Trdl: TTriad): Boolean; { Функция, формирующая строковое представление триады } function MakeString(i; integer): string: end; TTriadList = class(TList) public { Класс для описания списка триад и работы с ним } procedure Clear; override; { Процедура очистки списка } destructor Destroy: override:{Деструктор удаления списка} { Процедура вывода списка триад в список строк для отображения списка триад } procedure WriteToList(1ist; TStrings); { Процедура удаления триады из списка } procedure DelTriaddldx: integer); { Функция получения триады из списка по ее номеру } function GetTriaddIdx: integer): TTriad: { Свойство списка триад для доступа по номеру триады } property TriadstiIdx: integer]: TTriad read GetTriad; default: end;
^ataHau^k Модуль описания структур данных триад 227 { Процедура удаления из списка триад заданного типа } procedure OelTriadTypesd istTnad: TTriadLlst; TrdType: TTriadType): implementation uses SysUtds. FncTree. LexType: constructor TTriad.CreateCTyp: TTriadType; const Ops: TOpArray); { Конструктор создания триады } var i: integer: begin inherited Create; {Вызываем конструктор базового класса)- TriadType : = Тур; { Запоминаем тип триады } { Запоминаем два операнда триады } for 1:=1 to 2 do OperandsEI] := Ops[i]; Info := 0; { Очищаем поле дополнительной информации -} IsLinked := False; { Очищаем поле внешней ссылки }• end; function TTriad.GetOperanddldx: integer): TOperand: { Функция получения данных об операнде по его номеру ] begin Result .= OperandsEIIdx]; end: procedure TTriad.SetOperanddIdx: integer; Op: TOperand); { Функция записи данных операнда триады по его номеру } begin OperandsEIIdx] : = Op; end; function TTriad.GetLinkOIdx: integer): integer; { Функция получения ссылки на другую триаду из операнда } begin Result := OperandsEIIdx].TrladNum: end: procedure TTriad.SetLinkdIdx: integer; TrdN. integer): { Функция записи номера ссылки на другую триаду } begin OperandsEIIdx],TrladNum := TrdN: end; function TTriad.GetOpTypeCiIdx: integer): TOpType; { Функция получения типа операнда по его номеру }• begin Result := OperandsElIdx].ОрТуре: end; function TTr-ad.GetConstVal[iIdx: integer): Integer; { Функция записи типа операнда по его номеру } begin Result := OperandsрIdx].ConstVal; end: продолжение &
228 Приложение 3 • Тексты программных модулей для курсовой работ Листинг П3.10 (продолжение) procedure TTriad.SetConstVaKildx: integer: iVal: integer); { Функция получения значения константы из операнда } begin Operands[iIdx].ConstVal := iVal: end: procedure TTriad.SetOpType(iIdx: integer; OpT; TOpType): { Функция записи значения константы в операнд } begin Operands[iIdx],OpType := OpT; end; function IsEqualOp(const 0pl.0p2: TOperand): Boolean; { Функция проверки совпадения двух операндов } begin { Операнды равны, если совпадают их типы } Result ;= (Opl.OpType = 0р2.OpType); if Result then { и значения в зависимости от типа } case Opl.OpType of OP_CONST: Result := (Opl.ConstVal = 0p2.ConstVal): OP_VAR; Result : = (Opl.VarLink = Op2.VarLink); OP_LINK: Result := (Opl.TriadNum = 0p2.TriadNum); end: end; function TTriad.IsEqual(Trdl: TTriad): Boolean: { Функция, проверяющая совпадение двух -триад } begin { Триады эквивалентны, если совпадают их типы } Result := (TriadType = Trdl.TriadType) { и оба операнда } and IsEqualOp(Operands[l].Trdl[l]) and IsEqual0p(0perands[2],Trdl[2]): end: function Get0perStr(0p: TOperand): string; { Функция формирования строки для отображения операнда } begin case Op.OpType of OP_CONST: Result := IntToStrCOp.ConstVal); OP_VAR: Result := Op.VarLink.VarName: OP_LINK; Result ;= '*'+ IntToStr(Op.TriadNum+l): end{case}; end; function TTriad.MakeStringCi: integer): string; begin Result := Format!’Xd;’#9’Xs (Xs. Xs)’. [i +1.TriadStr[Tri adType].
229 Модуль описания структур данных триад GetOperStr(Opers[l]).GetOperStr(Opers[2])]): end: destructor TTriadList.Destroy. { Деструктор для удаления списка триад } begin Clear; { Очищаем список триад } inherited Destroy: {Вызываем деструктор базового класса] end: procedure TTriadList.Cl ear: { Процедура очистки списка триад } var i: integer; begin { Освобождаем память для всех триад из списка } for i:=Count-l downto 0 do TTriad(Items[iJ).Free: inherited Clear; { Вызываем функцию базового класса } end; procedure TTriadList.DelTriad(iIdx: integer); { Функция удаления триады из списка триад } begin if ildx < Count-1 then { Если это не последняя триа/а. переставляем флаг ссылки на предыдущую (если флаг есть)} TTriad(ItemsEiIdx+1]).IsLinked := TTriad(ItemsE1Idx+l]).IsLinked or TTriad(Items[iIdx]).IsLinked: TTriad(Items[iIdx]).Free: { Освобождаем память триады } Deleted Idx): { Удаляем ссылку на триаду из списка end; function TTriadList.GetTriad(iIdx: integer); TTriad: { Функция выборки триады из списка по ее номеру } begin Result := TTriadCItemsEiIdx]); end: procedure TTriadList.WriteToListdist: ^Strings): { Процедура вывода списка триад в список строк для отображения списка триад } var i.iCnt: integer; begin list.Cl ear; { Очищаем список строк } iCnt := Count-1; for i;=0 to iCnt do { Для всех триад из списка триад } продолжение
230 Приложение 3 • Тексты программных модулей для курсовой работы Листинг ПЗ. 10 (продолжение) { Формируем строковое представление триады и добавляем его в список строк } Ii st.Add(TTri ad(Iterns[i]).MakeSt гi ng(i)); end; procedure DelTriadTypes(listTriad: TTriadList; TrdType: TTriadType); { Процедура удаления из списка триад заданного типа } var i.j.ICnt.iDel: integer; liStNum; TList; Trd: TTriad; { Список запоминания изменений индексов } begin IDel := 0; {В начале изменение индекса нулевое } iCnt := listTriad.Count-1: { Создаем список запоминания изменений индексов триад } listNum ;= TList.Create; try for i;=0 to iCnt do { Для всех триад списка выполняем } begin { запоминание изменений индекса } { Запоминаем изменение индекса данной триады } 1i stNum.AddCTObjectСiDel)); {Если триада удаляется, увеличиваем изменение индекса} if listTriad[i].TriadType = TrdType then Inc(iDel): end; for i:=iCnt downto 0 do { Для всех триад списка } begin { изменяем индексы ссылок } Trd := 1istTriad[i]; { Если эта триада удаляемого типа, то удаляем ее } if Trd.TriadType = TrdType then 1istTriad.Del Triad(i) else { Иначе для каждого операнда триады смотрим, не является ли он ссылкой } for j:=l to 2 do if Trd[j].OpType = OP_LINK then { Если операнд является ссылкой на триаду, уменьшаем ее индекс } Trd.Links[j] := Trd.Links[j] - integer(listNum[Trd.Links[j]]); end; finally 1istNum.Free; { Уничтожаем временный список } end: end; end.
Модуль, реализующий алгоритмы оптимизации списков триад 231 Модуль, реализующий алгоритмы оптимизации списков триад Листинг П3.11. Оптимизация списков триад unit TrdOpt: interface { Модуль, реализующий два алгоритма оптимизации: - оптимизация путем свертки объектного кода: - оптимизация за счет исключения лишних операций. } uses Classes. TblElem. LexElem. TrdType. Triads: type {Информационная структура для таблицы идентификаторов, предназначенная для алгоритма свертки объектного ксдм] TConstlnfo = cl ass(TAddVarlnfo) protected iConst: longint; { Поле для записи значения переменном } { Конструктор для создания структуры } constructor Created Info: longint): public { Функции для чтения и записи информации } function GetlnfoCiIdx: integer): longint: override: procedure Setlnfo(ildx: integer: 11nf: longint): override, end: {Информационная структура для таблицы идентификаторов.. предназначенная для алгоритма исключения лишних операций} TDepInfo = cl ass(TAddVarlnfo) protected iDep: longint: { Поле для записи числа зависимости j { Конструктор для создания структуры } constructor Create(ilnfo: longint): public { Функции для чтения и записи информации } function GetlnfodIdx: integer): longint: override: procedure Setlnfodldx: integer; ilnfo: longint); override; end: { Процедура оптимизации методом свертки объектного но да } procedure OptimizeConstdistTriad: TTriadList); { Процедура оптимизации путем исключения лишних огесаций } procedure OptimizeSamedistTriad: TTriadList): продолжение &
232 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.11 (продолжение) implementation uses Syslltils. FncTree. LexType. TrdCalc; constructor TConstInfo.Create(ilnfo; longint); { Создание структуры для свертки объектного кода } begin inherited Create: (Вызываем конструктор базового класса} iConst ;= ilnfo: { Запоминаем информацию } end: procedure TConstlnfo.SetlnfoUIdx: integer; iInf: longint): { Функция записи информации } begin iConst := ilnfo: end: function TConstlnfo.Getlnfodldx: integer): longint; { Функция чтения инфоримации } begin Result :- iConst; end: function TestOperConstlOp; TOperand; listTriad: TTriadList; var iConst: integer): Boolean; { Функция проверки того, что операнд является константой и получения его значения в переменную iConst } var plnfo: TConstlnfo; begin Result := False; case Op.OpType of { Выборка по типу операнда } OP_CONST: { Если оператор - константа, то все просто } begin iConst := Op.ConstVal; Result := True; end: OP-VAR: { Если оператор - переменная, } begin { тогда проверяем наличие у нее информационной структуры. } plnfo := TConstlnfo(Op.VarLink.Info); if plnfo <> nil then {и если такая структура есть.} begin {берем ее значение} iConst := plnfoEO]; Result := True; end; end; OP_LINK: { Если оператор - ссылка на триаду. begin { то он является константой. если триада имеет тип "CONST" } if 1 istTriad[Op.TriadNum].TrdType = TRD_CONST
233 Модуль, реализующий алгоритмы оптимизации списков триад then begin IConst := 1istTriad[Op.TriadNum][l].ConstVal; Result := True; end: end; end{case}; end; procedure OptimizeConsUlistTriad: TTriadList); { Процедура оптимизации методом свертки объектного кода } var 1.j.iCnt.i0pl.i0p2: integer; Ops: TOpArray; Trd: TTriad; begin { Очищаем информационные структуры таблицы идентификаторов } ClearTreelnfo; { Заполняем операнды триады типа "CONST" } Ops[1].OpType := OP_CONST; Ops[2].OpType := OP_CONST; Ops[2].ConstVal ;= 0; iCnt := listTriad.Count-1; for i:=0 to iCnt do { Для всех триад списка } begin { выполняем алгоритм } Trd := 1istTriad[i]; if Trd.TrdType in TriadLineSet then begin { Если любой операнд линейной триады ссылается на триаду "CONST", берем и запоминаем ее значение } for j;«l to 2 do if (Trd[j],OpType = OPJ.INK) and (listTriad[Trd.Links[j]].TrdType = TRD_CONST) then begin Trd.OpTypestj] : = OP_CONST; Trd.ValuesCj] ;= listTriad[Trd.Links[j]][l].ConstVal; end; end else ' if Trd.TrdType = TRD_IF then begin { Если первый операнд условной триады ссылается на триаду "CONST". берем и запоминаем ее значение } if (TrdtU.OpType = 0PJJNK) and ClistTriad[Trd.Links[1]].TrdType = TRD_CONST) then begin продолжение
234 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.11 (продолжение) Trd.OpTypes[1] : = OP_CONST; Trd.Values[l] := I istThadETrd. Li nks[l]][l]. ConstVal; end; end else . if Trd.TrdType = TRD_ASSIGN then begin { Если второй операнд триады присвоения ссылается на триаду "CONST", берем и запоминаем ее значение } if (Trd[2].ОрТуре = OP_LINK) and (listTriad[Trd.Links[2]].TrdType = TRD_CONST) then begin Trd.OpTypes[2J : = OP_CONST; Trd.ValuesE2] : = listTriadETrd.LinksE2]][l].ConstVal; end; end;{ Если триада помечена ссылкой, то линейный участок кода закончен - очищаем информационные структуры идентификаторов} if Trd.IsLinked then ClearTreelnfo: if Trd.TrdType = TRD_ASSIGN then { Если триада имеет } begin { тип "присвоение" } { и если ее второй операнд - константа. } if Test0perConst(TrdE2].1istTriad.i0p2) then (запоминаем его значение в информационной структуре переменной} TrdEl].VarLink.Info ;= TConstInfo.Create(i0p2); end else { Если триада - одна из линейных операций. } if Trd.TrdType in TriadLineSet then begin { и если оба ее операнда - константы. } if TestOperConst(Trd[l].1istTriad.iOpl) and Test0perConst(Trd[2].listTriad.i0p2) then begin { тогда вычисляем значение операции. } OpsЕ1].ConstVal := Cal cTriad(Trd.TrdType.iOpl.i0p2); { запоминаем его в триаде "CONST", которую записываем в список вместо прежней триады } 1istTriad.ItemsEi] ;= TTriad.Create(TRD_CONST.Ops): {Если на прежнюю триаду была ссылка, сохраняем ее} listTriad[i].IsLinked ;= Trd.IsLinked: Trd.Free; { Уничтожаем прежнюю триаду } end; end;
^alaHausiti' 235 Модуль, реализующий алгоритмы оптимизации списков триад end; end; constructor TDepInfo.CreateCilnfo: longint); { Создание информационной структуры для чисел зависимости } begin inherited Create; [Вызываем конструктор базового класса} IDep ;= ilnfo: { Запоминаем число зависимости } end: procedure TDepInfo.SetlnfofiIdx; integer; ilnfo; long-nt): { Функция записи числа зависимости } begin IDep : = ilnfo: end: function TDepInfo.GetInfoCiIdx: integer): longint: { Функция чтения числа зависимости } begin Result ;= Wep: end; function CalcDepOp(listTriad: TTriadList; Op: TOperand): longint; {Функция вычисления числа зависимости для операнда триады] begin Result ;= 0; case Op.ОрТуре of { Выборка по типу операнда } 0P_VAR: { Если это переменная - смотрим ее информационную структуру, и если она есть, берем число зависимости ] if Op.Varlink.Info <> nil then Result := Op.VarLink.Info.Info[0]; OP_LINK: { Если это ссылка на триаду, то берем число зависимости триады } Result .= 1istTriad[Op.TriadMim].Info: end{case}: end: function CalcDepClistTriad: TTriadList; Trd: TTrad): longint: { Функция вычисления числа зависимости триады } var IDepTmp: longint; begin Result := CalcDepOpClistTriad.Trd[l]); IDepTmp := CalcDepOpdistTriad.Trd[2]): { Число зависимости триады есть число на единицу большее, чем максимальное из чисел зазисимости ее операндов } продолжение &
236 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.11 (продолжение) if IDepTmp > Result then Result := iDepTmp+1 else Inc(Result); Trd.Info := Result; end: procedure OptimizeSame(listTriad: TTriadList); { Процедура оптимизации путем исключения лишних операций } var i,j.iStart.iCnt.iNum: integer; Ops; TOpArray; Trd; TTriad; begin { Начало линейного участка - начало списка триад } iStart := 0: ClearTreelnfo; { Очищаем информационные структуры таблицы идентификаторов } Ops[l].ОрТуре ;= OP_L1NK; { Заполняем операнды } 0ps[2]-ОрТуре ;= OPCONST; { для триады типа "SAME" } 0ps[2].ConstVal ;= 0: iCnt ;= listTriad.Count-1; for i;=0 to iCnt do { Для всех триад списка } begin { выполняем алгоритм } Trd ;= 1istTriad[i]: if Trd.IsLinked then {Если триада помечена ссылкой. } begin { то линейный участок кода закончен - очищаем } ClearTreelnfo; { информационные структуры идентификаторов и } iStart := i; { запоминаем начало линейного участка } end: for j:=l to 2 do { Если любой операнд триады ссылается if Trd[j].ОрТуре = OP LINK then { на триаду "SAME". } begin { то переставляем ссылку на предыдущую. } iNum := Trd[j].TriadNum;{ совпадающую с ней триаду } if listTriad[iNum],TrdType = TRD_SAME then Trd.Links[j] := 1istTriad[iNum].Links[1]; end; if Trd.TrdType = TRD_ASSIGN then { Если триада типа } begin { "присвоение" - запоминаем число зависимости связанной с нею переменной } Trd[l].VarLink.Info := TDepInfo.Create!i+1); end else { Если триада - одна из линейных операций } if Trd.TrdType in TriadLineSet then begin { Вычисляем число зависимости триады } Cal cDep( 1 istTriad.Trd):
^laiaHaus,^. Модуль создания списка триад 237 for j:=iStart to 1-1 do { На всем линейном участке begin { ищем совпадающую триаду с таким же } if Trd.IsEqual(1istTriad[j]) { числом зависимости } and (Trd.Info = 1istTriadEJ].Info) then begin { Если триада найдена, запоминаем ссылку } Dps[l].TriadNum : = j; { запоминаем ее в триаде типа "SAME", которую записываем в список вместо прежней триады } 1 istTriad.Items[i] : = TTriad.Create(TRD_SAME.Ops): 1 istTriad[i].IsLinked := Trd.IsLinked: { Если на прежнюю триаду была ссылка, сохраняем ее } Trd.Free: { Уничтожаем прежнюю триаду } Break: { Прерываем поиск } end; end: end{if}; end{for}; end: end. Модуль создания списка триад на основе дерева разбора Листинг П3.12. Создание списка триад на основе дерева разбора unit TrdMake: {!!! Зависит от входного языка !!!} interface { Модуль, обеспечивающий создание списка триад на осноэе структуры синтаксического разбора } uses LexElem. Triads. SyntSymb: function MakeTriadList(symbTop: TSymbol: listTriad: TTriadList): TLexem: { Функция создания списка триад начиная от корневого символа дерева синтаксического разбора. Функция возвращает nil при успешном выполнении, иначе она возвращает ссылку на лексему, где произошла ошибка } implementation uses LexType. TrdType: function GetLexem(symbOp: TSymbol): TLexem; { Функция, проверяющая, является ли операнд лексемой } продолжение
238 Приложение 3* Тексты программных модулей для курсовой работы Листинг ПЗ. 12 (продолжение) begin case symbOp.Rule of 0: Result := symbOp.Lexem; (Нет правил - это лексема!} 27.28: Result := symbOp[0].Lexem; { Если дочерний символ построен по правилу № 27 или 28. то это лексема } 19.26: Result := GetLexem(symbOp[l]) { Если это арифметические скобки, надо проверить. не является ли лексемой операнд в скобках } else Result := nil; { Иначе это не лексема } end; end: function MakeTriadListNOP(symbTop: TSymbol; listTriad; TTriadList): TLexem: { Функция создания списка триад начиная от корневого символа дерева синтаксического разбора (без добавления триады NOP в конец списка) } var Opens: TOpArray: { массив операндов триад } iInsl.iIns2.iIns3: integer: { переменные для запоминания индексов триад в списке } function MakeOperand( 10р{номер операнда}. iSymOp{порядковый номер символа в синтаксической конструкции}. 1М1п{минимальная позиция триады в списке}. 15угпЕгг{номер лексемы, на который позиционировать ошибку}: integer; van Uns; integer{индекс триады в списке}): TLexem; { Функция формирования ссылки на операнд } var lexTmp: TLexem; begin lexTmp := GetLexem(symbTop[iSymOp]); { Проверяем. } if lexTmp <> nil then { является ли операнд лексемой } with lexTmp do { Если да. то берем имя операнда } begin { в зависимости от типа лексемы } if LexType = LEX_VAR then begin if VarInfo.VarName = NAME_RESULT then begin{Убеждаемся, что переменная имеет допустимое имя} Result := lexTmp; Exit; end; { Если это переменная, то запоминаем ссылку на таблицу идентификаторов }
^alaHaus,^. Модуль создания списка триад 239 OpersCiOpJ.OpType ; = OP_VAR; Opers[iOp].VarLink := Varlnfo; end else if LexType = LEX_CONST then begin { Если это константа, то запоминаем ее значение } Opens[iOp].OpType := OP CONST; Cpers[iOp].ConstVal := ConstVal; end else begin { Иначе это ошибка, возвращаем лексему j Result := lexTmp; { как указатель на место ошибки } Exit; end; iIns := iMin; Result : = nil; end else { иначе это синтаксическая конструкция } begin {Вызываем рекурсивно функцию создания списка триад} Result := MakeTriadListNOP(symbTop[iSymOp]. 1 istTrie-d); if Result <> nil then Exit; {Ошибка - прерываем алгоритм} ilns ; = listTriad.Count: { Запоминаем индекс триады } if ilns <= iMin then {Ерли индекс меньше минимального -} begin { зто ошибка } Result ;= symbTop[iSymErr].Lexem; Exit; end: Opens[iOp].OpType := OP_LINK;{Запоминаем ссылку на} Opens[iOp],TriadNum := iIns-1: {предыдущую триаду } end; end; function MakeOperation! Trd; TTriadType{тип создаваемой триады}); TLexem; { Функция создания списка триад для линейных операиий } begin { Создаем ссылку на первый операнд } Result ;® MakeOperand(l{op}.0{sym}.listTriad.Cour.t. l{sym err}.ilnsl); if Result <> nil then Exit; {Ошибка - прерываем алгоритм} { Создаем ссылку на второй операнд } Result ;= Make0perand(2{op}.2{sym}.ilnsl. l{sym err}.ilns2): if Result <> nil then Exit; {Ошибка - прерываем алгоритм] { Создаем саму триаду с двумя ссылками на операнды } listTriad.AddCTTriad.Create(Tnd.Opens)): end;
240 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.12 (продолжение) begin { Тело главной функции } case symbTop.Rule of { Начинаем с выбора типа правила } 5:{'if(B)EelseE'} { Полный условный оператор } begin { Запоминаем ссылку на первый операнд (условие ”if(B)") } Result := MakeOperand(l{op}.2{sym}.listTriad.Count. l{sym err}.iInsl); { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit; Opens[2].ОрТуре := OP_LINK; { Второй операнд - } 0pers[2].TriadNum := 0; {ссылка на триаду, номер которой пока не известен} { Создаем триаду типа “IF" } 1i stTri ad.Add(TTriad.Create(TRD_1F.Opers)1; { Запоминаем ссылку на второй операнд (раздел "(В)Е") } Result Make0perand(2{op},4{sym}.1Insl. 3{sym err}.ilns2); { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit; Opers[l].ОрТуре ;= OP_CONST; {Заполняем операнды} Opens[U.ConstVal := 1; { для триады типа "JMP". которая должна быть в конце раздела "(В)Е"} Opens[2].ОрТуре ;= OP LINK; { Второй операнд - } 0pers[2].TriadNum ;= 0; {ссылка на триаду, номер которой пока не известен} { Создаем триаду типа "JMP" } 1 i stTri ad.Add(TTriad.Create(TRD_JMP.Opens)); { Для созданной ранее триады "IF" ставим ссылку в конец последовательности триад раздела "(В)Е“ } listTriad[iInsl],links[2] : = ilns2+l; { Запоминаем ссылку на третий операнд (раздел "elseE") } Result := Make0perand(2{op}.6{sym}.1Ins2. 5{sym err}.ilns3); { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit; { Для созданной ранее триады "JMP" ставим ссылку в конец последовательности триад раздела "elseE" } listTriad[iIns2].Links[2] ; = 1Ins3; end; 6;{'if(B)E'} { Неполный условный оператор } begin { Запоминаем ссылку на первый операнд (условие ”if(B)”) } Result := MakeOperand(l{op}.2{sym}.listTriad.Count.
^laiattaus,^. 241 Модуль создания списка триад l{sym err},1Insl): { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit: 0pers[2].ОрТуре := 0P_LINK; { Второй операнд - } 0pers[2].TriadNum := 0; {ссылка на триаду, номер которой пока не известен} { Создаем триаду типа "IF" } 1 i stTri ad.Add(TTri ad.Create(TRD_IF.Opers)); { Запоминаем ссылку на второй операнд (раздел ”(В)Е”) } Result := Make0perand(2{op}.4{sym}.ilnsl. 3{sym err}.iIns2): { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit; { Для созданной ранее триады "IF" ставим ссылку в конец последовательности триад раздела "(В)Е" } 1istTriadLiInsl].Links[2] := ilns2; end; 8:{’while!B)doE'} { Оператор цикла "while" } begin { Запоминаем ссылку на первый операнд (условие "while(B)") } 1Ins3 := listTriad.Count: Result := MakeOperand(l{op}.2{sym}.iIns3. l{sym err}.iInsl); { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit; 0pers[2].ОрТуре ;= OPLINK: { Второй операнд - } 0pers[2].TriadNum := Of {ссылка на триаду, номер которой пока не известен} { Создаем триаду типа "IF" } listTriad.Add(TTriad.Create(TRD_IF.Opers)): { Запоминаем ссылку на второй операнд (раздел "doE") } Result ;= Make0perand(2{op}.5{sym}.ilnsl, 4{sym err}.ilns2); { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit; 0pers[l].ОрТуре ;= 0P_C0NST; {Заполняем операнды} Opers[l].ConstVal ;= 1; { для триады типа "JMP". которая должна быть в конце раздела "doE" } { Второй операнд - ссылка на начало списка триад } 0pers[2].ОрТуре : = OP_LINK; 0pers[2].TriadNum ;= 1Ins3; { Создаем триаду типа "JMP” } listTriad.Add(TTri ad.Create(TRD_JMP.Opers)); продолжение
242 Приложение 3 * Тексты программных модулей для курсовой работы Листинг П3.12 (продолжение) { Для созданной ранее триады "IF" ставим ссылку в конец последовательности триад раздела "doE” } listTriad[iInsl].Links[2] := 1Ins2+1; end: 9:{'a:=E'} { Оператор присвоения } begin { Если первый операнд не является переменной, то это ошибка } if symbTop[0].Lexem.LexType <> LEX_VAR then begin Result symbTop[0].Lexem: Exit: end: { Если имя первого операнда совпадает с именем параметра, то это семантическая ошибка } if (symbTop[0].Lexem,VarName = NAME_INPVAR) or (symbTopEOLLexem.VarName = NAME_RESULT) then begin Result :- symbTop[0].Lexem; Exit: end: { Создаем ссылку на первый операнд - переменную } OpersCH. ОрТуре := OP_VAR: Operstl].VarLink := symbTop[0].Lexem.VarInfo: { Создаем ссылку на второй операнд } Result :- Make0perand(2{op}.2{sym},listTriad.Count. l{sym err}.ilnsl); { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit: { Создаем триаду типа “присваивание” } 1i stTri ad.Add(TTri ad.Create(TRD_ASSIGN.Opers)); end: { Генерация списка триад для линейных операций } 10:{'ВогВ'} Result := MakeOperation(TRD_OR): 11:{'BxorB'} Result := MakeOperation(TRD_XOR); 13:{'BandB'} Result := MakeOperation(TRD_AND); 15:{’E<E’} Result := MakeOperation(TRD_LT): 16:{'E>E’} Result := MakeOperation(TRD_GT); 17:{'E=E'} Result := MakeOperation(TRD_EQ): 18:{’EoE‘} Result := MakeOperation(TRD_NEQ); 21:{'E-E'} Result := MakeOperation(TRD_SUB): 22:{'E+E'} Result :- MakeOperation(TRD_ADD): 20:{not(B)} begin { Создаем ссылку на первый операнд } Result :- MakeOperand(l{op}.2{sym}.listTriad.Count, l{sym err}.ilnsl); { Если произошла ошибка, прерываем выполнение }
^aiattaus,^. 243 Модуль создания списка триад if Result <> nil then Exit: Opens[2].ОрТуре : = 0P_C0NST: {Второй операнд для} Opens[2].ConstVal := 0: { NOT не имеет значения } { Создаем триаду типа "NOT" } 1 i stTni ad.Add(TTni ad.Cneate(TRD_NOT,Opens)); end: 24:{uminE} begin { Создаем ссылку на второй операнд } Result := Make0penand(2{op}.l{sym}.listTniad.Count. 0{sym ennj.ilnsl): { Если произошла ошибка, прерываем выполнение } if Result <> nil then Exit: Opens[1].ОрТуре : = 0P_C0NST; {Первый операнд для} OpensLU.ConstVal := 0: { унарной операции должен быть 0 } { Создаем триаду типа "UMIN" } 1i stTniad.Add(TTniad.Cneate(TRD_UMIN.Opens)): end; { Для логических, арифметических или операторных скобок рекурсивно вызываем функцию для второго символа } 1.7.19.26:{'pnogEend.’.’beginEend'.‘(Е)'.'(В)’} Result := MakeTniadListNOP(symbTop[l].listTniad): 3:{Е;Е Для списка операторов нужно рекурсивно вызвать} begin { функцию два раза } Result := MakeTniadListN0P(symbTop[0].1istTniad): if Result <> nil then Exit: Result := MakeTniadListN0P(symbTop[2].listTniad); end: 27.28: Result := nil; { Для лексем ничего не нужно } { Во всех остальных случаях нужно рекурсивно вызвать функцию для первого символа } else Result := MakeTniadListN0P(symbTop[0],listTniad): end{case Rule}: end: function MakeTniadList(symbTop: TSymbol: listTniad: TTniadList): TLexem; { Функция создания списка триад начиная от корневого символа дерева синтаксического разбора } van i: integen; Opens: TOpAnnay: Tnd: TTniad; продолжение
244 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.12 (продолжение) begin { Создаем список триад } Result := MakeTriadListNOP(syrnbTop.listTriad); if Result = nil then {Если ошибка, прерываем выполнение} with listTriad do begin { Создаем пустую триаду "NOP" в конце списка } Opens[1].OpType := OP_CONST; Operstl].ConstVal := 0: 0pers[2].OpType := OP_CONST: Opens[2].ConstVal := 0; Add(TT ni ad.Cneate(TRD_N0P.Opens)): fon i:-Cpunt-l downto 0 do begin {Для всех триад в списке расставляем флаг ссылки} Tnd : = TniadsCi]; if Tnd.TndType in [TRD_IF.TRD_JMP] then begin { Если триада "переход" ("IF" или "JMP") ссылается на другую триаду.} if Tnd.OpTypes[2] = 0P_LINK then listTriad[Trd.Links[2]J.IsLinked := Tnue: { то ту триаду надо пометить } end: end; end; end; end.' Модуль построения ассемблерного кода по списку триад Листинг П3.13. Построение ассемблерного кода по списку триад unit TndAsm; { !!! Зависит от целевой вычислительной системы !!! } interface { Модуль распределения регистров и построения ассемблерного кода по списку триад } uses Classes. TndType, Triads: const { Префикс наименования временных переменных } TEMP_VARNAME .= ’_Tmp’: NUM_PROCREG = 6; { Количество доступных регистров } { Функция распределения регистров и временных переменных для хранения промежуточных результатов триад }
^lataHaus^. 245 Модуль построения ассемблерного кода function MakeRegistersdistTriad: TTriadList): Integer: { Функция построения ассемблерного кода по списку триад } function MakeAsmCodeC1istTriad: TTriadList; listCcde: TStrings; flagOpt: Boolean): integer: Implementation uses SysUtils; function MakeRegistersdistTriad: TlriadList): integer: { Функция распределения регистров и временных переменных для хранения промежуточных результатов триад. Результат: количество необходимых временных переменных } var 1.J.iR.dCnt.iNurn : integer:{Счетчики и переменные циклов} { Динамический массив для запоминания занятых регистров } listReg: TList: begin { Создаем массив для хранения занятых регис-ров } listReg := TList.Create: Result := 0: if listReg <> nil then try { Обнуляем информационное поле у всех триад } for i:=listTriad.Count-1 downto 0 do listTriad[i].Info := 0: { Цикл no всем триадам. Обязательно с конца списка! } for i:=1istTriad.Count-1 downto 0 do for j:»l to 2 do { Цикл по всем (2) операндам } { Если триада - линейная операция, или "IF" (первый операнд), или присвоение (второй операнд) } if ((listTriad[i].TrdType in TriadLineSet) or (listTriad[i],TrdType = TRD_IF) and (j = 1) or (1istTriad[i].TrdType = TRD_ASSIGN) and (j = 2)) { и операндом является ссылка на другую триаду } and distTriad[i][j].OpType = OP_LINK) then begin { Запоминаем номер триады, на которую направлена ссылка } iNum := 1istTriad[i][j].TriadNum: { Если триаде еще не назначен регистр и если это не предыдущая триада - надо ей назначить регистр } if (listTriad[iNum].Info = 0) and (iNum <> i-1) then begin { Количество назначенных регистров } iCnt := 1istReg.Count-1: for iR:=0 to iCnt do begin{ Цикл по массиву назначенных регистров }
246 Приложение 3 • Тексты программных модулей для курсовой работы Листинг ПЗ. 13 (продолжение) { Если область действия регистра за пределами текущей триады, тр его можно использовать } if longint(listReg[iR]) >= i then begin { Запоминаем область действия регистра } listRegfiR] := TObject(iNum): { Назначаем регистр триаде с номером iNum } 1istTriad[iNum].Info := iR+1: Break: { Прерываем цикл по массиву регистров } end: end: { Если ни один из использованных регистров не был назначен, надо брать новый регистр } if 1istTriad[iNum].Info = 0 then begin { Добавляем запись в массив регистров, указываем ей область действия iNum } 1 i s tReg.Add(TObj ect(i Num)): { Назначаем новый регистр триаде с номером iNum } listTriad[iNum].Info :- listReg.Count: end; end: end:{ Результат функции: количество записей в массиве регистров -1. за вычетом числа доступных регистров} Result := listReg.Count - (NUM_PROCREG-1): finally listReg.Free: end; end; function GetRegNameCilnfo: integer): string; { Функция наименования регистров процессора } begin case ilnfo of 0: Result := ’eax': 1: Result := 'ebx': 2: Result := 'ecx'; 3: Result :- 'edx'; 4: Result := 'esi': 5: Result 'edi': { Если это не один из регистров - значит. даем имя временной переменной } else Result := Format!'XsXd'.[TEMP_VARNAME.iInfo-NUM_PROCREG]): end{case}; end;
^aiattaus,^. 247 Модуль построения ассемблерного кода function GetOpNamed: integer: listTriad: TTriadList; iOp: integer): string: { Функция наименования операнда триады i - номер триады в списке; listTriad - список триад; Юр - номер операнда триады } var iNum: integer; {номенр триады по ссылке} Triad: TTriad: {текущая триада} begin Triad := 1istTriad[i]; { Запоминаем текущую триаду } { Выборка наименования операнда в зависимости от типа } case Triad[iOp].ОрТуре of { Если константа - значение константы } OP_CONST: Result := IntToStr(Tnad[iOp].ConstVal); { Если переменная - ее имя из таблицы идентификаторов } OP_VAR: begin Result := Triad[iOp].VarLink.VarName; { Если имя совпадает с именем функции. заменяем его на Result функции } if Result = NAME-FUNCT then Result := NAME_RESULT; end: { Иначе - это регистр } else { для временного хранения результатов триады } begin { Запоминаем номер триады } iNum : = Triad[iOp].TriadNum; { Если зто предыдущая триада, то операнд не нужен } if iNum = 1-1 then Result := ” else begin {Берем номер регистра, связанного с триадой} iNum := 1istTriad[iNum].Info: { Если регистра нет. то операнд не нужен } if iNum = 0 then Result := " { Иначе имя операнда - это имя регистра } else Result := GetRegNamedNum); end; end: end{case}; end; function NakeMoveCconst $Кед.{имя регистра} sPrev.{предыдущая команда} sVal{предыдущая величина в еах}: string; flagOpt: Воо1еап{флаг оптимизации}): string;
248 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.13 (продолжение) { Функция, генерящая код занесения значения в регистр еах } begin { Если операнд был только что выгружен из еах или необходимое значение уже есть в аккумуляторе., нет необходимости записывать его туда снова } if (Pos(Format(#9'mov'#9'Xs.еах'.[sReg]).sPrev) = 1) or (sVal = sReg) then begin Result := Exit: end; if flagOpt then { Если оптимизация команд включена } begin if sReg - 'O' then { Если требуемое значение = 0. } begin{ero можно получить из -1 и 1 с помощью INC и DEC} if sVal = '-Г then Result := #9'inc'#9'eax' else if sVal = then Result : = #9'dec'#9'eax’ else Result :» #9'xor'#9'eax.eax' end {иначе - с помощью XOR} else if sReg = 'Г then { Если требуемое значение = 1. } begin{ero можно получить из -1 и 0- с помощью NEG и INC} if sVal = '-Г then Result := #9'neg'#9'eax' else if sVal = 'O' then Result : = #9'inc'#9'eax‘ else Result := #9'xor'#9'eax.eax’#13#10#9'inc'#9'eax': end {иначе - двумя командами: XOR и INC } else if sReg = '-Г then { Если требуемое значение = -1. } begin{ero можно получить из 1 и 0 с помощью NEG и DEC} if sVal = '1' then Result :» #9'neg'#9’eax' else if sVal = 'O' then Result := #9'dec'#9'eax' else Result := #9'xor’#9'eax.eax'#13#10#9'dec'#9'eax'; end {иначе -/двумя командами: XOR и DEC } { Иначе заполняем еах командой MOV } else Result := Format(#9'mov'#9'еах.Xs'.[sReg]); end { Если оптимизация команд выключена, всегда заполняем еах командой MOV } else Result := Format(#9'mov'#9'eax.Xs'.[sReg]); end;
249 Модуль построения ассемблерного кода function MakeOpcodeti: integer;{номер текущей триады] 1 istTriad: TTriadList;{список триад] const sOp.sReg.[код операции и операнд] sPrev.{предыдущая команда] sVal{предыдущая величина в еах}: string: flagOpt: Воо1еап{флаг оптимизации}): string: { Функция, генерящая код линейных операций над еах } var Triad: TTriad;{текущая триада,} begin { Запоминаем текущую триаду } Triad := 1istTriad[i]: if flagOpt then { Если оптимизация команд включена } begin if sReg = ‘O' then { Если операнд = 0 } begin case Tnad.TrdType of TRD_AND: [ Для команды AND результат всегда = 0 } Result := MakeMoveC'О'.sPrev.sVal.flagOpt); { Для OR. "+" и ничего не надо делать } TRD_OR.TRO_ADD.TRD_SUB: Result := #9#9: { Иначе генерируем код выполняемой операции } else Result : = Format(#9rs'#9'eax.2s',[sOp.sReg]>: end{case}: end else if sReg • T then { Если операнд = 1 } begin case Triad.TrdType of TRD_OR: { Для команды OR результат всегда » 1 } Result := NakeMove('Г.sPrev.sVal.flagOpt); { Для АО ничего не надо делать } TRD_AND: Result := #9#9; { Для "+’ генерируем операцию INC } TRD_ADD: Result := #9’inc'#9'eax'; { Для генерируем операцию DEC } TRD_SIIB: Result := #9'dec’#9’eax'; { Иначе генерируем код выполняемой операции } else Result := Formatters'#9'еах ,3s' .[sOp.sReg])1; end{case]: end else if sReg = '-Г then { Если операнд = -1 ] begin case Triad.TrdType of продолжение &
. 250 Приложение 3 * Тексты программных модулей для курсовой работы Листинг П3.13 (продолжение) { Для ”+" генерируем операцию DEC } TRD-ADD: Result := #9,dec’#9,eax'; { Для генерируем операцию INC } TRD_SUB: Result := #9'inc'#9,eax': { Иначе генерируем код выполняемой операции } else Result : = Format(#9'Xs'#9'eax.Xs’.[sOp.sReg]): end{case}: end { Иначе генерируем код выполняемой операции } else Result : = Format(#9'Xs'#9'eax.Xs'.[sOp.sReg]): end { Если оптимизация команд выключена. всегда генерируем код выполняемой операции } else Result := Format(#9’Xs‘#9'еах.Xs'.[sOp.sReg]): { Добавляем к результату информацию о триаде в качестве комментария } Result := Result + Format(#9'{ Xs }'. [Triad.MakeString(i)]): end: function MakeAsmCodeC 11stTriad: TTriadList:{входной список триад} listCode: TStrings;{список строк результирующего кода} flagOpt: Воо1еап{флаг оптимизации}): integer; { Функция построения ассемблерного кода по списку триад } var i.iCnt: integer:{счетчик и переменная цикла} sR: string;{строка для имени регистра} sPrev.sVal: string; {строки для хранения предыдущей команды и значения еах} procedure ТаkеРrevAsm; { Процедура, выделяющая предыдущую команду и значение еах из списка результирующих команд } var j: integer; begin j := listCode.Count: if j > 0 then begin sPrev := 1istCodeCj-1]; sVal :- StrPas(PChar(listCode.Objects[j-l])): end else begin sPrev := ": sVal :- "; end; end:
^aiattaus,^. Модуль построения ассемблерного кода 251 procedure MakeOperl(const $Ор.{код операции} sAddOp: string:{код дополнительной операции} Юр: integer{HOMep операнда в триаде}): { Функция генерации кода для унарных операций } var sReg{CTpoKd для имени регистра}: String: begin TakePrevAsm: {Берем предыдущую команду и значение из еах} { Запоминаем имя операнда } sReg := GetOpName(i.1istTriad.iOp): if sReg <> '’ then { Если имя пустое, операнд уже есть в регистре еах от выполнения предыдущей триады.} begin { иначе его нужно занести в еах } { Вызываем функцию генерации кода занесения операнда } sReg := MakeMove(sReg.sPrev.sVal .flagOpt): if sReg <> ” then listCode.Add(sReg): end: { Генерируем непосредственно код операции } listCode.Add(Format(#9'Xs'#9'eax,#9'{ Xs }’. [sOp.1i stTri ad[i].MakeStri ng(i)])); if sAddOp <> " then { Если есть дополнительная операция, генерируем ее код } listCode. Add(Format(#9'Xs,#9’eax.Г.[sAddOp])): if 1istTriad[i].Info <> 0 then { Если триада связана с begin { регистром, запоминаем результат в этом регистре } sReg := GetRegName(1istTriad[i].Info): { При этом запоминаем, что сейчас находится в еах } 1 i stCode.AddObject(Format(#9'mov'#9'Xs.eax‘.[sReg]). TObj ect(PCha г(sReg))); end: end: procedure Make0per2(const $0р.{код операции} sAddOp: string{KOfl дополнительная операции}); { Функция генерации кода для бинарных арифметических и логических операций } var sRegl.sReg2{CTpOKM для имен регистров}: string; begin TakePrevAsm: {Берем предыдущую команду и значение из еах} { Запоминаем имена первого и второго операндов } sRegl := GetOpName(i. 1 istTriad.1): sReg2 : = GetOpName(i.listTriad.2); { Если имя первого операнда пустое, значит, он уме есть в регистре еах от выполнения предыдущей триады - вызываем функцию генерации кода для второго операнда } if (sRegl = ”) or (sRegl = sVal) then продолжение £
252 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.13 (продолжение) I istCode.AddfMakeOpCode(1.listTriad.s0p.sReg2. sPrey.sVal.flagOpt)) else { Если имя второго операнда пустое, значит он уже есть в регистре еах от выполнения предыдущей триады - вызываем функцию генерации кода для первого операнда } if (sReg2 - '') or (sReg2 = sVal) then begin 1i stCode.Add(MakeOpCodeC i.1i stTriad.sOp.sRegl. sPrev.sVal.flagOpt)); { Если есть дополнительная операция, генерируем ее код (когда операция несимметричная - например } i f sAddOp <> '' then 1 i stCode.Add(Format(#9’Xs'#9'ea x'.[sAddOp])): end else { Если оба операнда не пустые, то надо: - сначала загрузить в еах первый операнд; - сгенерировать код для обработки второго операнда.} begin sRegl := MakeMoveC sRegl.sPrev.sVal.fl agOpt); if sRegl <>. ” then 1istCode.Add(sRegl); 1 i stCode.Add(MakeOpCodeC i.1i stTri ad.sOp.sReg2. sPrev,sVal.flagOpt)); end: if 1istTriad[i],Info <> 0 then { Если триада связана с begin { регистром, запоминаем результат в этом регистре } sRegl := GetRegName(1istTriad[i],Info): { При этом запоминаем, что сейчас находится в еах } 1i stCode.AddObject(Format(#9'mov'#9‘Xs.ea х'.[sRegl]). TObjectC PChar(sRegl))): end: end: procedure MakeCompare(const sOp: string {флаг операции сравнения}): { Функция генерации кода для операций сравнения } var sRegl.$Ред2{строки для имен регистров}: string: begin TakePrevAsm; {Берем предыдущую команду и значение из еах} { Запоминаем имена первого и второго операндов } sRegl := GetOpNamed.listTriad.1); sReg2 := GetOpNamed.listTriad.2): { Если имя первого операнда пустое, значит он уже есть в регистре еах от выполнения предыдущей триады - сравниваем еах со вторым операндом }
^alaHaus,^. Модуль построения ассемблерного кода i f sRegl = '' then 1 istCode.Add(Formate#9'cmp'#9’eax.Xs'#9'{ Xs [sReg2.11stTri ad[1L MakeStr1 ng(1)])) else { Если имя второго операнда пустое, значит он уже есть в регистре еах от выполнения предыдущей триады - сравниваем еах с первым операндом в обратном порядке } 1f sReg2 - '’ then listCode.Add(Format(#9'cmp'#9*3s.eax'#9’{ [sRegl,1istTriad[i].MakeString(i)])) else { Если оба операнда не пустые, то надо: - сначала загрузить в еах первый операнд; - сравнить еах со вторым операндом. } begin sRegl := MakeMoveCsRegl.sPrev.sVai .flagOpt): if sRegl о " then listCode.Add(sRegl): listCode.Add(Format(#9'crop’#9’eax.Xs'#9‘{ Xs }'. [sReg2.11stTriad[i].MakeString(i)])): end: { Загружаем в младший бит еах 1 или О в зависимости от флага сравнения } listCode.Add(Format(#9'setXs’#9'al‘.[sOp])): listCode.Add(#9’and’#9,eax.1’): {очищаем остальные биты} if 11stTr1ad[1].Info <> 0 then { Если триада связана с begin { регистром, запоминаем результат в этом регистре } sRegl :» GetRegName(listTriad[i],Info): { При этом запоминаем, что сейчас находится в еах } 11stCode.AddObject(Format(#9'mov'#9’Xs.ea x'.[sReg1]). TObject(PChar(sRegl))); end; end: begin { Тело главной функции } ICnt := 11stTrlad.Count-1: { Количество триад в списке } for i:=0 to ICnt do begin { Цикл по всем триадам от начала списка } { Если триада помечена, создаем локальную метку в списке команд ассемблера } if 11stTrlad[i],IsLinked then listCode.Add(Format('@MXd:'.[1+1])): { Генерация кода в зависимости от типа триады } case 11stTriad[i].TrdType of { Код для триады IF } TRD_IF: { Если операнд - константа. } begin {(это возможно в результате оптимизации)} If 11stTr1ad[i][l].ОрТуре = 0P_C0NST then
254 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.13 (продолжение) begin { Условный переход превращается в безусловный, если константа = 0.} if listTriad[i][l].ConstVal - 0 then listCode.Add(Format(#9'jmp'#9'@MXd’#9'{ Xs }'. [ I i stTri ad[ i ] [2]. Tri adNum+1. 1istTriad[i].MakeString(i)])); end { а иначе вообще генерировать код не нужно.} else { Если операнд - не константа } begin { Берем имя первого операнда } sR := GetOpName(i.listTriad.l): { Если имя первого операнда пустое, значит он уже есть в регистре еах от выполнения предыдущей триады. } i f sR • ” then { тогда надо выставить флаг "Z". сравнив еах с ним самим, но учитывая, что предыдущая триада для IF - это либо сравнение, либо логическая операция, это можно опустить} else { иначе надо сравнить еах с операндом } 1 i stCode.Add(Format(#9'cmp'#9’Xs.0'.[sR])); {Переход по условию "NOT Z" на ближайшую метку} listCode.Add(Format(#9’jnz'#9’@FXd'#9'{ Xs }'. [i,listTriad[i].MakeString(i)])); { Переход по прямому условию на дальнюю метку } 1i stCode.Add(Format!#9'jmp’#9'@MXd'. [listTriad[i][2].TriadNunw-l])); { Метка для ближнего перехода } listCode.Add(Format(’@FXd;’.[i])); end; end; { Код для бинарных логических операций } TRD_0R; Make0per2(’or'."); TRD_XOR; MakeOper2('xor'.”); TRD_AND; Make0per2(‘and’."); { Код для операции NOT (так как NOT(O)=FFFFFFFF. то нужна еще операция: AND еах.1 } TRD_NOT: MakeOperl(‘not’,’and'.1); { Код для операций сравнения по их флагам } TRD_LT: MakeCompare('1‘); TRD GT; Mak eCompa re (' g ’); TRD-EQ: MakeCompare('e'); TRD_NEQ: MakeCompare('ne'); { Код для бинарных арифметических операций }
^alattausiffk 255 Модуль интерфейса с пользователем TRD_ADD: MakeOper2('add'.’'): TRD_SUB: Make0per2(-’ sub ’. ’ neg'); { Код для унарного минуса } TRDJJMIN: MakeOperK'neg'.''.2); TRD_ASS1GN: { Код для операции присвоения } begin {Берем предыдущую команду и значение из еах} TakePrevAsm: sR := GetOpNamed.listTriad.2): {Имя второго операнда} { Если имя второго операнда пустое, значит он уже есть в регистре еах от выполнения предыдущей триады} 1f sR <> ” then begin {иначе генерируем код загрузки второго операнда} sVal := MakeMove(sR.sPrev.sVal.flagOpt): if sVal <> ’' then 1istCode.Add(sVal): end; { Из еах записываем результат в переменную с именем первого операнда } sVal 11stTriad[i][1].VarLink.VarName: if sVal = NAMEJUNCT then sVal NAME_RESULT; sVal :- Format(#9,mov’#9'Xs.eax'#9’{ Xs }'. [sVal.listTriad[i].MakeString(i)]); { При этом запоминаем, что было в еах } 1 istCode.AddObject(sVa1.TObject(PCha r(sR))); end: { Код для операции безусловного перехода } TRDJMP: 11 stCode. Add( Format(#9'jmp'#9‘@MXd,#9'{ Xs }1. [11stTriad[i][2].TriadNum+1. listTriad[i].MakeString(i)])); { Код для операции NOP } TRD_N0P: listCode.Add(Format(#9'nop,#9#9‘{ Xs }’. [1istTriad[i],MakeString(i)])); end{case}: end{for}; Result := 1istCode.Count: end; end. Модуль интерфейса с пользователем Программный код Листинг П3.14. Реализация пользовательского интерфейса unit Formlab4; interface
256 Приложение 3 • Тексты программных модулей для курсовой работы Листинг ПЗ. 14 (продолжение) uses Windows. Messages. SysUtils. Classes., Graphics. Controls. Forms. Dialogs. StdCtrls. ComCtrls. Grids. ExtCtrls. LexElem. SyntSymb. Triads: type { Типы возможных ошибок компилятора: файловая, лексическая, синтаксическая, семантическая или ошибок нет} TErrType = (ERRJILE.ERR_LEX.ERR_SYNT.ERR TRIAD.ERR_NO); TCursovForm = class(TForm) { главная форма программы } PageControl1: TPageControl: SheetFile: TTabSheet: SheetLexems: TTabSheet; BtnExit: TButton; GroupText: TGroupBox: Li stIdents: TMemo: EditFile: TEdit: BtnFile: TButton: BtnLoad: TButton: FileOpenDlg: TOpenDialog; GridLex: TStringGrid: SheetSynt: TTabSheet-; TreeSynt: TTreeView; SheetTriad: TTabSheet; GroupTri adAl1: TGroupBox; Splitterl: TSplitter; GroupTriadSame: TGroupBox; Splitter2: TSplitter: GroupTriadConst: TGroupBox: ListTriadAll; TMemo: ListTriadConst: TMemo: ListTriadSame: TMemo: CheckDel_C: TCheckBox; CheckDelSame: TCheckBox: SheetAsm: TTabSheet: ListAsm: TMemo; CheckAsm: TCheckBox; procedure BtnLoadClick(Sender: TObject): procedure BtnFileClick(Sender: TObject); procedure EditFileChange(Sender: TObject): procedure BtnExitClick(Sender: TObject): procedure FormCreate(Sender: TObject): procedure FormClose(Sender; TObject;
257 ftatailausi Модуль интерфейса с пользователем var Action: TCloseAction): private listLex: TLexList: { Список лексем } symbStack: TSymbStack: { Синтаксический стек } listTriad: TTriadList; { Список триад } { Имена файлов: входного, результата и ошибок } sInpFT1е.sOutFi1е.sErrFi1е: string: • { Функция записи стартовых данных в файл ошибок } procedure Startlnfo(const sErrF: string): { Функция обработки командной строки } procedure ProcessParams( var flOptC.flOptSame.flOptAsn: Boolean); { Инициализация таблицы отображения списка лексем } procedure InitLexGrid: { Процедура отображения синтаксического дерева } procedure MakeTreelnodeTree TTreehode: symbSynt: TSymbol): { Процедура информации об оиибке } procedure Errlnfo(const sErrFsErr: string; iPos.iLen: integer): { Функция запуска компилятора } function CompRun(const slnF.sOutF,sErrF: string: var symbRes: TSymbol; flTrd.flDelC.flDelSame.flOptC. flOptSame.fl OptAsm: Boolean): TErrType: end: var CursovForm: TCursovForm. implementation ($R *.DFM} uses FncTree.LexType.LexAuLo.TrdType.TrdMake.TrdAsn.TrdOpt; procedure TCursovForm.InitLexGrid: {Процедура инициализации таблицы отображения списка лексем} begin with GridLex do begin RowCount : = 2: Cells[0.0] := '№ n/a': Cells[1.0] ;= 'Лексема': Cells[2.0j := 'Значение': Cellsto.l] := Cellstl.l] : = Cells[2.1] := end: end; 9 Зак 68 продолжение &
258 Приложение 3* Тексты программных модулей для курсовой работы Листинг ПЗ. 14 (продолжение) procedure TCursovForm.StartlnfoC const sErrF: $1г1пд{имя файла ошибок}): { Функция записи стартовых данных в файл ошибок } var 1.ICnt: integer:{счетчик параметров и переменная цикла} sT: string:{суммарная командная строка} begin sErrFile := sErrF: { Запоминаем имя файла ошибок } { Записываем в файл ошибок дату запуска компилятора } ErrInfo(sErrFile. FormatC--- £s ---' .[0ateTimeToStr(Now)]).0.0): iCnt := ParamCount; { Количество входных параметров } sT := ParamStr(O): { Обнуляем командную строку } • {Записываем в командную строку параметры последовательно} for i:=l to iCnt do sT := sT +’ ’+ ParamStr(i): { Записываем в файл ошибок суммарную командную строку } Errlnfo(sErrFile.sT.O.O); end; procedure TCursovForm.ProcessParamsC var flOptC.flOptSame.flOptAsm: Воо1еап{флаги}): { Функция обработки командной строки } var i.iCnt.iLen: integer: { переменная счетчиков } sTmp: string: { временная переменная } { Список для записи ошибок параметров } listErr: TStringlist; begin { Устанавливаем все флаги по умолчанию } flOptC := True: flOptSame := True: fl OptAsm :» True; { Создаем список для записи ошибок параметров } listErr := TStringList.Create: try { Берем количество входных параметров } iCnt := ParamCount; for i:=2 to ICnt do begin { Обрабатываем параметры начиная co второго } sTmp :- ParamStr(i): { Берем строку параметра } iLen := Length(sTmp): { Длина строки параметра } { Если параметр слишком короткий или не начинается со знака - это неправильный параметр } if (iLen < 3) or (sTmp[l] <> '-') then { Запоминаем ошибку в список } listErr.Add(Formate'Неверный параметр $d: "Xs"!'. [i.sTmp])) else { Иначе обрабатываем параметр в соответствии
259 Модуль интерфейса с пользователем с его типом (второй символ) } case sTmp[2] of { Флаг оптимизации ассемблера } 'a'.'A': flOptAsm := (sTmp[3] = ’Г): { Флаг оптимизации методом свертки } 'с'.’С : flOptC :» (sTmp[3] = ’.Г): { Флаг оптимизации исключением лишних операций } 's'.'S': flOptSame ; = (sTmp[3] = ’Г); { Имя выходного файла } 'о'.'О*: sOutFile := System.Copy(sTmp.3.iLen-2): { Имя файла ошибок } 'е'. 'Е*: Startlnfo(System.Copy(sTmp.3.iLen-2)); else { Параметр неизвестного типа } { Запоминаем ошибку в список } listErr.Add(Format('Неверный параметр £d: "£s"!'. [1 .sTmp])): end{case}; end{for}: { Ставим имена файлов по умолчанию, если они не были указаны в параметрах } If sOutFile = '' then sOutFile := ChangeFileExt(sinpFile.’.asm’); if sErrFile » '’ then StartInfo(ChangeFileExt(sInpFile.'.err')); ICnt 1istErr.Count-1: { Количество ошибок } { Запоминаем информацию обо всех ошибках } for i:=0 to ICnt do ErrInfo(sErrFile.listErr[i].O.Oi finally listErr.Free: { Уничтожаем список ошибок } end{try}: end: procedure TCursovForm.FormCreate(Sender: TObject); var f1OptC.f10ptSame.fl OptAsm: Boolean; symbRes: TSymbol: iErr: TErrType; begin symbRes ;= nil: sOutFile := '*: sErrFile := { В начале выполнения инициализируем список лексем, таблицу идентификаторов, синтаксический стек и список триад InitTreeVar: UstLex := TLexList.Create; symbStack := TSymbStack.Create; listTriad := TTriadList.Create; продолжение &
260 Приложение 3 • Тексты программных модулей для курсовой работы Листинг ПЗ. 14 (продолжение) { Если указан параметр - не надо открывать окно, надо запускать компилятор и обрабатывать входной файл } if ParamCount > 0 then begin { Берем имя входного файла из первого параметра } slnpFile := ParamStr(l); { Обрабатываем все остальные параметры } ProcessParams(flOptC.flOptSame.flOptAsm); iErr := CompRun( { Запускаем компилятор } slnpFile.sOutFile.sErrFiIе{входные файлы}. symbRes{ссылка на дерево разбора}. Ра15е{запоминать списки триад не надо}. Н0р1С{флаг удаления триад "С"}. flOptSame^nar удаления триад "SAME"}. Н0р1С{флаг свертки объектного кода }. Т10р1$ате{флаг исключения лишних операций}. Т10р1А5т{оптимизация команд ассемблера}); { Если нет файловых ошибок, то надо завершать работу } if iErr <> ERR_FILE then Self.Close; end; end; procedure TCursovForm.FormClose(Sender: TObject; var Action; TCloseAction): { В конце выполнения очищаем список лексем, таблицу идентификаторов, синтаксический стек и список триад } begin 1istTriad.Free; symbStack.Free; listLex.Free: ClearTreeVar: Appli cation.Termi nate: end; procedure TCursovForm.EditFileChange(Sender: TObject); begin { Можно читать файл, только когда его имя не пустое } BtnLoad.Enabled := (EditFi1e.Text <> '’); end; procedure TCursovForm.BtnFileClick(Sender; TObject); begin { Выбор имени файла с помощью стандартного диалога } if FileOpenDlg.Execute then begin EditFile.Text : = FileOpenDlg.FileName; BtnLoad.Enabled := (EditFile.Text <> "): end; end:
^latattaus,^. Модуль интерфейса с пользователем 261 procedure TCursovForm.ErrInfo(const sErrF.sErr: string; iPos.iLen: integer); { Процедура информации об ошибке } var fileErr: TextFile: { Файл записи информации об сь/оке } begin { Если имя файла ошибок не пустое } if sErrF <> '' then try { Записываем информацию об ошибке в файл } AssignFiletfileErr.sErrF); if FileExists(sErrF) then Append(fileErr) else Rewrite(fileErr): writeln(fileErr.sErr); CloseFile(fileErr); { и закрываем его } except { Если ошибка записи в файл, сообщаем об этсч } HessageDlgCFormate'Ошибка записи в файл "Xs"!'#13#10 к 'Ошибка компиляции: Xs!'.[sErrF.sErr]). mtError.[mb0k].0); end { Если имя файла ошибок пустое. } else { выводим информацию на экран } begin { Позиционируем список строк на место ошибки ] Listldents.SeiStart := iPos: Listldents.Sei Length : = iLen; MessageDlg(sErr.mtWarning.[mbOk].0):{Выводим сообщение} Listldents.SetFocus: { Выделяем ошибку в списке строи } end: end: function TCursovForm.CompRun({Функция запуска компилятора} const slnF.{имя входного файла} sOutF.{MMfl результирующего файла} sErrF{nMfl файла ошибок}:string: var symbRes: TSymbol;{корень дерева разбора} flTrd.^nar записи триад в списки} Л0е1С.{флаг удаления триад типа ''С”} fl Del Same,{флаг удаления триад типа "SAME”} flOptC.{флаг оптимизации методом свертки] flOptSame.{флаг исключения лишних операций} flOptAsm^nar оптимизации ассемблерного кода} : Boolean): TErrType: var i.iCnt.iErr: integer; { переменные счетчиков • lexTmp: TLexem; { временная лексема для инф. об оим'скэх } sVars.sAdd: string; { временные строки } asmList: TStringList; { список ассемблерных команд } begin{ Очищаем список лексем, синтаксический стек к critcor риад } продолжение &
262 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.14 (продолжение) listLex.Clear; symbStack.Clear; listTriad.Clear; try { Чтение файла в список строк } L i stldents.Lines.LoadF romF i1e(sInF); except { Если файловая ошибка - сообщаем об этом } Result := ERRJILE; MessageDlgC'Ошибка чтения файла!'.mtError.[mbOk].0); Exit; { Дальнейшая работа компилятора невозможна } end; { Анализ списка строк и заполнение списка лексем } iErr ;= MakeLexList(Listldents.Lines.listLex); if iErr<>0 then {Анализ неуспешный - сообщаем об ошибке} begin { Берем позицию ошибки из лексемы в начале списка } ErrInfo(sErrF. Formate'Неверная лексема "$s" в строке Жб!'. [1i stLex[0].LexInfoStr.i Err]). listLex[0].PosAll.listLex[0].PosNum); Result ERR_LEX; { Результат - лексическая ошибка }. end else { Добавляем в конец списка лексем } begin { информационную лексему "конец строки” } with Listldents do listLex.Add(TLexem.CreateInfo('Конец строки'. Length(Text),L i nes.Count-1.0)); { Выполняем синтаксический разбор и получаем ссылку на корень дерева разбора } symbRes ;= BuildSyntList(listLex.symbStack): { Если эта ссылка содержит лексические данные, значит, была ошибка в месте, указанном лексемой } if symbRes.SymbTyре = SYMB_LEX then begin { Берем позицию ошибки из лексемы по ссылке } ErrlnfoCsErrF. Formate'Синтаксическая ошибка в строке $d поз. М!'. [symbRes.Lexem.StrNum+1.symbRes.Lexem.PosNum]). symbRes.Lexem.PosAl1.0); symbRes.Free; { Освобождаем ссылку на лексему } symbRes := nil; Result := ERR_SYNT; { Это синтаксическая ошибка } end else { Иначе - ссылка указывает на корень синтаксического дерева } begin { Строим список триад по синтаксическому дереву } lexTmp ;= MakeTriadLisUsymbRes. 1 istTriad); { Если есть ссылка на лексему, значит, была семантическая ошибка }
263 Модуль интерфейса с пользователем if lexTmp <> nil then begin { Берем позицию ошибочной лексемы по ссылке } ErrlnfofsErrF. Formate'Семантическая ошибка в строке Xd поз. fcd!'. [lexTmp.Strhum+1.lexTmp.PosNumJ). lexTmp.PosAll.0): Result := ERR_TRIAD; { Это семантическая ошибка } end else { Если ссылка пуста, значит, триады построены } begin Result :• ERR_NO; { Результат - "ошибок нет” } { Если указан флаг, сохраняем общий список триад } if flTrd then 1istTriad.WriteToList(ListTriadAU.Lines): if flOptC then { Если указан флаг, выполняем } begin { оптимизацию путем свертки объектного кода } Optimi zeConstdi StTri ad): { Если указан флаг, удаляем триады типа "С" } if flDelС then DelTri adTypes(1i stTri ad.TRD_CONST): end: { Если указан флаг.} if flTrd then {сохраняем триады после оптимизации} listTriad. Hr i teToLi st(L i stTri adConst.Lines): if flOptSame then { Если указан флаг, выполняем Ьед1п{оптимизацию путем исключения лишних операций} Optimi zeSameC1i stTri ad): { Если указан флаг, удаляем триады типа "SAME" } if flDelSame then DelTri adTypes(1i stT ri ad.TRD_SAME); end: { Если указан флаг.) if flTrd then {сохраняем триады после оптимизации} 1 istTriad.WriteToList(ListTriadSame.Lines); { Распределяем регистры по списку триад } iCnt := MakeRegistersClistTriad); { Создаем и записываем список ассемблерных команд } asmList := TStringList.Create: try with asmList do begin Clear; { Очищаем список ассемблерных команд } { Пишем заголовок программы } AddCFormatC'program £s;’.[NAME_PROG])): { Запоминаем перечень всех идентификаторов } продолжение &
264 Приложение 3 • Тексты программных модулей для курсовой работы Листинг П3.14 (продолжение) sVars : = IdentLiSt('.'.NAMEJNPVAR. NAME J UNCT); if sVars <> ” then begin{Ecnn перечень идентификаторов не пустой.} Add("); { записываем его с указанием } Add('var'): { типа данных } Add(FormatC Xs: Xs:' .[sVars.NAMEJYPE])): end; AddC): { Пишем заголовок функции } Add(Format('function X0:s(Xl:s: X2:s): X2:s:' +' stdcall;’. [NAME JUNCT. NAMEJNPVAR. NAME JYPE])): if iCnt > 0 then [Если регистров для хранения} begin {промежуточных результатов не хватило} Add(’var'); {и нужны временные переменные.} sVars : = {то заполняем их список.} for i;=0 to iCnt do begin sAdd :- Formate'XsXd'.[TEMP.VARNAME.i]): if sVars - " then sVars := sAdd else sVars := sVars +'.'+ sAdd: end; Add(Format(' Xs: Xs:',[sVars.NAMEJYPE])): end; Add('begin'): { В тело функции записываем } Add(' asm'); { список команд ассемблера. } Add(#9'pushad'#9#9'{запоминаем регистры.}’): MakeAsmCodeC1i stTri ad.asmLi st.flOptAsm): Add(#9'popad'#9#9'{восстанавливаем регистры.}'); AddC end;'); AddCend;'); Add("); { Описываем одну входную переменную } Add(Formate'var Xs: Xs;'. [NAMEJNPVAR. NAMEJYPE])): AddC): Add('begin'); { Заполняем главную программу } Add(Format(' readln(Xs):'.[NAMEJNPVAR])); Add(Format(' writeln(Xs(Xs));', [NAMEJUNCT. NAMEJNPVAR])); AddC readin:'): AddCend.'); end{with}; {Если установлен флаг, записываем} if flTrd then {команды для отображения на экране}
Модуль интерфейса с пользователем ListAsm.Lines.AddStrings(asmL1st): if sOutF <> ” then { Если есть имя рез. файла.) try { записываем туда список всех команд } asmList.SaveToFile(sdutF): except Result : = ERRFILE; end: finally asmList.Free; {Уничтожаем список команд) end{try); {после его отображения и записи в файл} end; end: end: end: procedure TCursovForm.BtnLoadClickCSender: TObject): { Процедура чтения и анализа файла } var i.iCnt: integer; { переменные счетчиков } iRes: TErrType; { переменная для хранения результата } symbRes: TSymbol: { временная переменная корня дерева) nodeTree: TTreeNode: { переменная для узлов дерева ] begin symbRes := nil: { Корень дерева разбора вначале пустом } InitLexGrid; {Очищаем таблицу отображения списка лексем) TreeSynt.Items.Clear; { Очищаем синтаксическое дерево } iRes :• CompRun({ Вызываем функцию компиляции } EditFile. Text. .{задан только входной файл) 5утЬКе${указатель на дерево разбора). Тгие{Списки триад нужно запоминать). CheckDel_C.Checked {флаг удаления триад "С"). CheckDelSame.Checked {флаг удаления триад "SAME"). True {флаг оптимизации "свертка объектного кода"). True {флаг оптимизации исключения лишних операций). CheckAsm.Checked {оптимизация команд ассемблера}): if iRes > ERR_LEX then {Если не было лексической ошибки.) begin { заполняем список лексем } GridLex.RowCount : = listLex.Count+1: { Количество строк } iCnt :.= 1 istLex.Count-1: for i:=0 to iCnt do begin { Цикл по всем прочитанным лексемам } { Первая колонка - номер } GridLex.Cells[0.1+1] : = IntToStrli+1): { Вторая колонка - тип лексемы } GridLex.Cells[l.i+1] := LexTypeNameC1i stLex[1].LexType): 265 продолжение
. 266 Приложение 3 • Тексты программных модулей для курсовой работы Листинг ПЗ. 14 (продолжение) { Третья колонка - значение лексемы } GridLex.Cel Is[2.1+1] := listLex[i].LexlnfoStr; end: end; if (IRes > ERR_SYNT) and (symbRes <> nil) then { Если не было синтаксической ошибки.) begin { заполняем дерево синтаксического разбора } { Записываем данные в корень дерева } nodeTree := TreeSynt.Items.Add(n11.symbRes.SymbolStr); MakeTree(nodeTree.symbRes); { Строим дерево от корня } nodeTree.Expand(True); { Раскрываем все дерево } { Позиционируем указатель на корневой элемент } TreeSynt.Selected := nodeTree; end; if iRes > ERR-TRIAD then { Если не было семантической } begin { ошибки, то компиляция успешно завершена }' MessageDlg(’Компиляция успешно выполнена!’. mtlnformation.[mbOk].0); PageControll.ActivePagelndex := 4; end; end; procedure TCursovForm.MakeTree( { Процедура отображения синтаксического дерева } nodeTree: TTreeNode: {ссылка на корневой элемент отображаемой части дерева на экране} symbSynt: TSymbol {ссылка на синтаксический символ. связанный с корневым элементом этой части дерева}); var i.ICnt: integer; { переменные счетчиков } nodeTmp: TTreeNode: { текущий узел дерева } begin { Берем количество дочерних вершин для текущей } ICnt ;= symbSynt.Count-1; for i;=0 to ICnt do begin { Цикл по всем дочерним вершинам } { Добавляем к дереву на экране вершину и запоминаем ссылку на нее } nodeTmp := TreeSynt.Items.AddChild(nodeTree. symbSynt[i].Symbo1St r); { Если эта вершина связана с нетерминальным символом. рекурсивно вызываем процедуру построения дерева } if symbSynt[1].SymbType = SYMB_SYNT then MakeTree(nodeTmp.symbSynt[i]):
Модуль интерфейса с пользователем 267 ^alaiiaustik end: end: procedure TCursovForm.BtnExitClick(Sender: TObject): { Завершение работы с программой } begin Self.Close: end: end. Описание ресурсов пользовательского интерфейса Описание ресурсов пользовательского интерфейса можно найти в архиве, распо- ложенном на веб-сайте издательства, в файле FormLab4.dfm в подкаталоге CURSOV.
ПРИЛОЖЕНИЕ 4 Примеры входных и результирующих файлов для курсовой работы Пример 1. Вычисление факториала Листинг П4.1. Входной файл prog if ((InpVar > 31) or InpVar<0) CompileTest ;= 0 else if (InpVar=0) CompileTest := 1 else begin i := InpVar; Fact := 1; while (i<>--l) do begin j 1-1: Sum : = Fact; while (not (j=0)) do begin Sum := Sum + Fact;
‘Xalattaus,^. Пример 1. Вычисление факториала j := j-(-l): end: Fact := Sum; 1 : = i - 1; end: CompileTest := Fact: end: end. Листинг П4.2. Результирующий код program MyCurs: 269 var Fact.1.j.Sum: integer: function CompileTest(InpVar: integer): integer; stdcall: begin asm pushad {запоминаем регистры} mov eax.InpVar cmp eax.31 { 1: > (InpVar. 31) setg al and eax.l mov ebx.eax mov eax.InpVar cmp eax.0 { 2: < (InpVar. 0) } setl al and eax.l or eax.ebx { 3: or (*1. *2) } jnz @F3 { 4: if (*3. *7) } jmp @F3: @M7 xor eax.eax mov Result.eax { 5: := (CompileTest. 0) jmp @M7: @M31 . { 6: jmp (1. *31) } mov eax.InpVar cmp eax.O { 7: = (InpVar. 0) } sete al and eax.l jnz @F7 { 8: if (*7. *11) } jmp @F7: PMll xor eax.eax продолжение &
270 Приложение 4 • Примеры входных и результирующих файлов Листинг П4.2 (продолжение) inc eax mov Result.eax { 9: := (CompileTest. 1) jmp @M31 {10: jmp (1. *31) } @M11: mov eax.InpVar mov 1.eax {11: := (1. InpVar) } xor eax.eax inc eax mov Fact.eax { 12: := (Fact. 1) } @M13: mov eax. i cmp eax.l { 13: <> (1.1)} setne al and eax.l jnz @F13 {14: if (*13. *30) } jmp @F13: @M30 mov eax.i dec eax { 15: - i(i. 1) } mov j.eax { 16: := (j. *15) } mov eax.Fact mov @M18: Sum.eax {17: := (Sum. Fact) } mov eax.j cmp eax.O { 18: = (j. 0) } sete al and eax.l not eax { 19: not (*18. 0) } and eax.l jnz @F19 { 20: if (*19. *26) } jmp @F19: @M26 mov eax.Sum add eax.Fact { 21: + (Sum. Fact) } mov Sum.eax { 22: := (Sum. *21) } mov eax.j dec eax { 23: - (j. 1) } . mov j.eax { 24: : = (j. *23) } jmp @M26: 0M18 { 25: jmp (1. *18) } mov eax.Sum mov Fact.eax { 26: := (Fact. Sum) ; mov eax.i
^lalaHaus^. Пример 2. Иллюстрация работы функций оптимизации 271 dec eax { 27 : - (i. 1) } mov i.eax { 28 : := (i. *27) } jmp @M13 { 29: jmp (1. *13) } @M30: mov eax.Fact mov Result.eax { 30: := (CompileTest. Fact) @M31: nop { 31: nop (0. 0) } popad {восстанавливаем регистры} end: end: var InpVar: integer: begin readlnlInpVar); writeln(CompileTest(InpVar)): readln; end. Пример 2. Иллюстрация работы функций оптимизации Листинг П4.3. Входной файл prog D := 0; В := 1; С := 1; ' А := С + InpVar: D := С+В+234: С := А + В + С: D := ((C) +(А+В)) - (InpVar + 1) + (А+В): Е := (D - 22) - (А + В): CompileTest := 0; if (a<b or a<c xor( e=0 xor(d=0 and (b<c or (a<c and (c<>e or not(b=0)))).))) a:=0 else a:=l; if (InpVar > 0 or -1 о InpVar) while (a<l) do a:=a+(b+l) else CompileTest := InpVar+1: end.
272 Приложение 4 • Примеры входных и результирующих файлов Листинг П4.4. Результирующий код без учета оптимизации program MyCurs; var A.B.C.D.E: integer; function CompileTest!InpVar: integer): integer: stdcall; var _TrnpO._Tmpl: integer; begin asm pushad mov eax.0 {запоминаем регистры} mov mov D.eax eax.l { 1: := (D. 0) } mov B.eax { 2: := (В. 1) } mov C.eax { 3: := (С. 1) } add eax.InpVar 4: + (С. InpVar) mov mov A. eax eax.C { 5; := (А. *4) } add еах.В { 6: + (С. В) } add eax.234 { 7: + (*6. 234) } mov mov D.eax eax. A { 8: := (D. *7) } add еах. В { 9; + (А. В) } add eax.C { 10: + (*9. С) } mov mov C.eax eax. A { 11: : = (С. *10) } add еах. В { 12: + (А. В) } add mov mov eax.C ebx.eax eax.InpVar { 13: + (С. *12) } add eax. 1 { 14: + (InpVar. 1) sub neg mov mov eax.ebx eax ebx.eax eax.A 15: - (*13. *14) } add еах. В { 16: + (А. В) } add eax.ebx { 17: + (*15. *16) } mov D.eax { 18: := (D. *17) } sub. mov mov eax.22 ebx.eax eax.A 19 - (D. 22) } add еах. В { 20: + (А. В) }
^atattaus^. Пример 2. Иллюстрация работы функций оптимизации sub eax.ebx { 21: - (*19. *20) } neg eax mov E.eax { 22: := (E. *21) } mov eax.O mov Result.eax { 23: , := (CompileTest, mov eax.A cmp еах. В { 24: < (A. B) } setl al and eax.l mov ebx.eax mov eax.A cmp eax.C { 25: < (A. C) } setl al and eax.l or eax.ebx 1 [ 26: or (*24. *25) } mov ebx.eax mov eax.E cmp eax.O { 27; = (E. 0) } sete al and eax.l mov ecx.eax mov eax.D cmp eax.O { 28: = (D. 0) } sete al and eax.l mov edx.eax mov еах. В cmp eax.C { 29: < (B. C) } setl al and eax.l mov esi.eax mov eax.A cmp eax.C { 30: < (A. C) } setl al and eax.l mov edi.eax mov eax.C cmp eax.E { 31: <> (С. E) } setne al and eax.l mov _TmpO.eax mov еах. В cmp eax.O { 32: = (B. 0) } 0) }
274 Приложение 4 • Примеры входных и результирующих файлов Листинг П4.4 (продолжение) sete al and eax.l not eax { 33: not (*32. 0) } and eax.l or eax._TmpO {34: or (*31. *33) } and eax.edi { 35: and (*30. *34) } or eax.esi { 36: or (*29, *35) } and eax.edx { 37: and (*28. *36) } xor eax.ecx { 38: xor (*27. *37) } xor eax.ebx { 39: xor (*26. *38) } jnz @F39 {40: if (*39. *43) } jmp @M43 @F39: mov eax.O mov A.eax { 41: : = (A. 0) } jmp @M44 { 42: jmp- (1. *44) } @M43: mov eax.l mov A.eax { 43: := (A. 1) } @M44: mov eax.InpVar cmp eax.O {44: > (InpVar. 0) } setg al and eax.l mov ebx.eax mov eax.l neg eax { 45: - (0. 1) } cmp eax.InpVar { 46: <> (*45. InpVar) setne al and eax.l or eax.ebx {47: or (*44. *46) } jnz @F47 {48: if (*47. *56) jmp @M56 @F47: 0M49:. mov eax. A cmp eax.l { 49: < (A. 1) } setl al and eax.l jnz @F49 {50: if (*49. *55) jmp @M55 @F49: mov еах.В
^alattaus,^. Пример 2. Иллюстрация работы функций оптимизаций 275 add eax.l { 51: + (В. 1) } add еах.А { 52: + (А. *51) } mov А.еах {53: := (А. *52) } jmp @М49 { 54: jmp (1. *49) } @М55: jmp @М58 { 55: jmp (1. *58) } @М56: mov еах.InpVar add eax.l { 56: + (InpVar. 1) } mov Result.eax {57: := (CompileTest. *56) } @M58: nop { 58: nop (0. 0) } popad {восстанавливаем регистры} end; end: var InpVar: integer; begin readln(InpVar); writeln(Compil eTest(InpVar)); readin: end. Листинг П4.5. Результирующий код с учетом оптимизации program MyCurs; var A.B.C.D.E: integer: function CompileTest!InpVar: integer): integer: stdcall: begin asm pushad {запоминаем регистры} xor eax,eax mov D.eax { 1: ;= (D. 0) } inc eax mov B.eax { 2: := (В. 1) } mov C.eax { 3: := (С. 1) } add eax.InpVar { 4: + (C. InpVar) } mov A.eax { 5: := (А. *4) } mov eax.236 mov D.eax { 6: := (D. 236) } продолжение £
276 Приложение 4 • Примеры входных и результирующих файлов Листинг П4.5 (продолжение) mov eax. A add еах. В { 7: + (A. B) } mov ebx.eax add eax.C { 8: + (*7. C) } mov C.eax { 9: := (C. *8) } add eax.ebx { 10: + (C. *7) } mov ecx.eax mov eax.InpVar inc eax { 11 + (InpVar. 1) sub eax.ecx { 12: - (*10. *11) } neg eax add eax.ebx { 13: + (*12. *7) } mov D.eax { 14: := (D. *13) } mov eax.214 sub eax.ebx { 15: - (214. *7) } mov E.eax { 16: := (E. *15) } xor eax.eax mov Result.eax { 17: := (CompileTest mov eax.A cmp еах, В { 18: < (A. B) } setl al and eax.l mov ebx.eax mov eax.A cmp eax.C { 19: < (A. C) } setl al and eax.l mov edx.eax or eax.ebx { 20: or (*18. *19) } mov ebx.eax mov еах.E cmp eax.O {21: = (E. 0) } sete al and eax.l mov ecx.eax mov eax.C cmp eax.E { 22: <> (С. E) } setne al and eax.l xor eax.eax inc eax { 23: or (*22. 1) and eax.edx { 24: and (*19. *23) { 25: or (0. *24) }
^lataHaus,^. Пример 2. Иллюстрация работы функций оптимизации xor xor eax.eax eax.ecx { 26: { 27: and (0. *25) } xor (*21. *26) } xor eax.ebx { 28: xor (*20. *27) } jnz @F28 { 29: if (*28. *32) } jmp @M32 @F28: xor eax.eax mov A. eax { 30: := (A. 0) } jmp @M33 { 31: jmp (1, *33) } @M32: xor eax.eax inc eax mov A. eax { 32: := (A. 1) } @M33: mov eax.InpVar cmp eax.O { 33: > (InpVar. 0) } setg al and eax.l mov ebx.eax xor eax.eax dec eax cmp eax.InpVar { 34: <> (-1. InpVar) setne al and eax.l or eax.ebx { 35: or (*33. *34) } jnz PF 35 { 36: if (*35. *44) } jmp @M44 0F35: @M37: mov eax. A cmp eax.l { 37: < (A. 1) } setl al and eax.l jnz 0F37 {38: if (*37. *43) } jmp @M43 0F37: mov еах. В inc eax { 39: + (В. 1) } add eax.A { 40: + (A. *39) } mov A.eax { 41: := (A. *40) } jmp 0M37 { 42: jmp (1. *37) } @M43: jmp @M46 { 43: jmp (1. *46) }
278 Приложение 4 • Примеры входных и результирующих файлов Листинг П4.5 (продолжение) 0М44:. mov еах.InpVar inc еах { 44: + (InpVar. 1) } mov Result.еах { 45: := (CompileTest. *44) } @M46: nop { 46: nop (0. 0) } popad {восстанавливаем регистры} end; end: var InpVar: integer: begin readln(InpVar); writeln(CompileTestdnpVar)): readin: end.
^ataHaus,^. Литература Основная литература 1. Ахо А., Ульман Дж. Теория синтаксического анализа, перевода и компиляции. - М.: Мир, 1978. - Т. 1, 612 с. Т. 2,487 с. 2. Ахо А., Сети Р., Ульман Дж. Компиляторы: принципы, технологии и инстру- менты: Пер. с англ. — М.: Издательский дом «Вильямс», 2003. — 768 с. 3. Гордеев А. В., Молчанов А. Ю. Системное программное обеспечение. — СПб. Питер, 2002. — 734 с. 4. Компанией, Р. И., Маньков Е. В., Филатов Н. Е. Системное программирование Основы построения трансляторов: Учеб, пособие для высших и средних учеб- ных заведений. — СПб.: КОРОНА принт, 2000. — 256 с. 5. Гордеев А. В. Операционные системы: Учебник для вузов. 2-е изд. — СПб. Питер, 2004. — 416 с. 6. Олифер В. Г, Олифер Н. А. Сетевые операционные системы. — СПб.: Питер 2002. - 544 с. 7. Молчанов А. Ю. Системное программное обеспечение: Учебник для вузов. - СПб.: Питер, 2003. — 396 с. Дополнительная литература 8. Абрамова Н.А. и др. Новый математический аппарат для анализа внешней поведения и верификации программ. — М.: Институт проблем управление РАН, 1998. - 109 с. 9. Архангельский А. Я. и др. Русская справка (HELP) по Delphi 5 и Object Pas cal. - М.: БИНОМ, 2000. - 32 с.
280 Литература 10. Афанасьев А. Н. Формальные языки и грамматики: Учеб, пособие. — Улья- новск: УлГТУ, 1997. — 84 с. И. Карпова Т. С. Базы данных: модели, разработка, реализация. — СПб.: Питер, 2001.-304 с. 12. Бартеньев О. В. Фортран для студентов. — М.: Диалог-МИФИ, 1999. — 342 с. 13. Березин Б. И., Березин С. Б. Начальный курс С и C++. — М.: Диалог-МИФИ, 1996.-288 с. 14. Браун С. Операционная система UNIX. — М.: Мир, 1986. — 463 с. 15. Бржезовский А. В., Корсакова Н. В., Фильчаков В. В. Лексический и синтакси- ческий анализ. Формальные языки и грамматики. — Л.: ЛИАП, 1990. — 31 с. 16. Бржезовский А. В., Фильчаков В. В. Концептуальный анализ вычислительных систем. - СПб.: ЛИАП, 1991. - 78 с. 17. Волкова И. А., Руденко Т. В. Формальные языки и грамматики. Элементы тео- рии трансляции. — М.: Диалог-МГУ, 1999. — 62 с. 18. Грис Д. Конструирование компиляторов для цифровых вычислительных ма- шин. — М.: Мир, 1975. — 544 с. 19. Дворянкин А. И. Основы трансляции: Учеб, пособие. — Волгоград: ВолгГТУ, 1999.-80 с. 20. Дунаев С. UNIX System V. Release 4.2. Общее руководство. — М.: Диалог-МИФИ, 1995. - 287 с. 21. Евстигнеев В. А., Мирзуитова И. П. Анализ циклов: выбор кандидатов на рас- параллеливание. — Новосибирск: Ин-т систем информатики, 1999. — 48 с. 22. Жаков В. И., Коровинский В. В., Фильчаков В. В. Синтаксический анализ и ге- нерация кода. — СПб.: ГААП, 1993. — 26 с. 23. Калверт Ч. Delphi 4. Энциклопедия пользователя. — Киев: ДиаСофт, 1998. 24. Карпов Б. И. Delphi: Специальный справочник. — СПб.: Питер, 2001 — 684 с. 25. Карпов Б. И., Баранова Т. К. C++: Специальный справочник. — СПб.: Питер, 2002. - 480 с. 26. Карпов Ю. Г. Теория автоматов: Учебник для вузов. — СПб.: Питер, 2003. — 208 с. 27. Керниган Б., Пайк Р. UNIX — универсальная среда программирования. — М.: Финансы и статистика, 1992. — 420 с. 28. Кэнту М. Delphi 5 для профессионалов. — СПб.: Питер, 2001. 29. Льюис Ф. и др. Теоретические основы построения компиляторов. — М.: Мир, 1979.-483 с. 30. Мельников Б. Ф. Подклассы класса контекстно-свободных языков. — М.: Изд- во МГУ, 1995. - 174 с. 31. Немюгин С., ПерколабЛ. Изучаем Turbo Pascal. — СПб.: Питер, 2000. 32. Павловская Т. А. C/C++: Учебник. — СПб.: Питер, 2001. 33. Полетаева И. А Методы трансляции: Конспект лекций. — Новосибирск: Изд- во НГТУ, 1998. - Ч. 2. - 51 с.
Литература 281 34. Пратт Т., Зелковиц М. Языки программирования: разработка и реализация. — СПб.: Питер, 2001. 35. Рассел Ч., Кроуфорд Ш. UNIX и Linux: книга ответов. — СПб.: Питер, 1999. — 297 с. 36. Рейчард К., Фостер-Джонсон Э. UNIX: справочник. — СПб.: Питер, 2000. — ! 384 с. 37. Рудаков П. И., Федотов М. А. Основы языка Pascal: Учеб. курс. — М.: Радио и связь: Горячая линия — Телеком, 2000. — 205 с. 38. Серебряков В. И. Лекции по конструированию компиляторов. — М.: МГУ, 1997.- 171 с. 39. Страуструп Б. Язык программирования Си++. — М.: Радио и связь, 1991. — 348 с. 40. Федоров В. В. Основы построения трансляторов: Учеб, пособие. — Обнинск: ИАТЭ, 1995.- 105 с. 41. Финогенов К. Г. Основы языка ассемблера. — М.: Радио и связь, 1999. — 288 с. 42. Фомин В. В. Математические основы разработки трансляторов: Учеб, посо- бие. - СПб.: ИПЦ СПГУВК, 1996. - 65 с. 43. Чернышов А. В. Инструментальные средства программирования из состава ОС UNIX и их применение в повседневной практике. — М.: Изд-во МГУП, 1999. — 191 с. 44. Юров В. Assembler: Учебник. — СПб. и др.: Питер, 2000. — 622 с.
Алфавитный указатель Graphical User Interface (GUI) 36 A Автомат конечный /детерминированный 44 недетерминированный 44 конечный 43, 49, 59 Алгоритм бинарного поиска 16 метода цепочек 23 оптимизации 126 построения бинарного дерева 17 сдвиг-свертка 64-65, 75 Анализ лексический 43, 44, 49, 59, 85, 87, 92 синтаксический 13, 40, 61, 92 Анализатор лексический 39 Ассемблер 105 Б Бинарное дерево 17 Г Грамматика контекстно-свободная 67, 99 остовная 75, 89, 156 регулярная. 43, 49, 60-61 д ДМП-автомат 62, 65 К Коллизия 20 Компилятор 13, 40, 43-44, 61, 67, 96, 98-99, 102-104, 105, 122, 133, 136, 163, 173, 179 Л Лексема 49 Лексема (лексическая единица) 60 м Метод цепочек 23 МП-автомат 62-66, 74, 85 расширенный 69 МП-преобразователь 66 п Подготовка к генерации кода 13 Программа исходная 40, 61, 96, 98, 100, 103, 106 объектная 95, 104-105 результирующая 95, 98, 104-105, 138, 173, 179 Р Распознаватель 61-62 восходящий 64, 67 линейный 65 нисходящий 64, 67 табличный 65 Распределение памяти 13, 165 Рехэшпрование 21-22, 30, 34, 37 С Синтаксис 40 Сканер 39, 45 СУ-комниляция 98, 100 СУ-перевод 97-98, 101, 104, 161 т Таблица идентификаторов 14, 20, 24, 31, 54, 96, 98, 146 бинарное дерево 17 комбинированная 25, 32, 34, 37, 146 лексем 54, 61, 132
^laiaHaus,^. Алфавитный указатель 283 X Хэш-адресация 19-20, 27 Хэш-функция 19-20, 30, 38, 146 Хэширование 19 Я Язык ассемблера 99, 105, 166, 172, 179 программирования 106 C++" 41, 144 Object Pascal 30, 32 семантика 96, 103 синтаксис 40, 61 Фортран 41 регулярный 43 семантика 97, 171 синтаксис 97
Алексей Юрьевич Молчанов Системное программное обеспечение Лабораторный практикум Главный редактор Заведующий редакцией Руководитель проекта Литературный редактор Иллюстрации Художник Корректоры Верстка Е. Строганова А. Кривцов И. Шапошников Е. Бочкарева Г. Домрачева Е. Дьяченко Л. Ванькаева, Н. Лукина А. Зайцев Лицензия ИД № 05784 от 07.09.01. Подписано к печати 18.02.05. Формат 70x100/16. Усл. п. л. 23,22. Тираж 3000. Заказ 68 ООО «Питер Принт», 194044, Санкт-Петербург, пр. Б. Сампсониевский 29а. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. • Ипетатало с готовых диапозитивов в ОАО «Техническая книга» 190005, Санкт-Петербург, Измайловский пр., 29

СИСТЕМНОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ЛАБОРАТОРНЫЙ ПРАКТИКУМ Алексей Юрьевич Молчанов — выпуска Санкт-Петербургского университета аэрокосмического приборостроения (1993 год), кандидат технических науц, доцент. В настоящее время преподает на кафедре «Вычислительные комплексы системы и сети», а также^явл^ется директором по разработкам НПП «СпецТек». Лффоратрр^ый практикум является дополнением к учебном; пособию «Системное программное обеспечение». Эта книга позволит читателям полнее изучить принциг)Ь| создени^ системного программного обеспечения. Материал четко структурирован и состоит из лабораторных работ, что позволяет правильно выполнить рррпрактические задания и подготовить итоговое курсовое задание. разовый курс для студентов высших учебных заведений обучающихся по направлению «Информатика и вычислительная техника»^ ПИТЕР Заказ книг: 197198, Санкт-Петербург, а/я 619 тел.: (812) 103-73-74, postbook@piter.com 61093, Харьков-93, а/я 9130 тел.: (057) 712-27-05, piter@kharkov.piter.com www.piter.com — вся информация о книгах и веб-магазин