УЗБЕКСКОЕ АГЕНТСТВО
СВЯЗИ И ИНФОРМАТИЗАЦИИ
ТАШКЕНТСКИЙ УНИВЕРСИТЕТ
ИНФОРМАЦИОННЫХ ТЕХНОЛОГИЙ
Методическое пособие для лабораторных занятий
По предмету
Системное программное обеспечение
Ташкент 2008.
Л.Т.
Марышева, А.Эргашев «Системное программное обеспечение»
Данное
методическое пособие предназначено для студентов специальности информационные
технологии изучающих предмет «Системное программное обеспечение».
Цель
пособия – закрепить знания, полученные при изучении теоретической части курсов
и получить практические навыки разработки компилятора и лексического
анализатора входного языка транслятора.
Методическое пособие соответствует программе курса программное обеспечение», целью которой
является познакомить студента с основными приемами, необходимыми для решения
приведенных задач.
Темы |
часы |
Лабораторная работа №1 Организация таблиц идентификаторов |
4 |
Лабораторная работа №2 Проектирование лексического
анализатора |
4 |
Лабораторная работа №3 Построение простейшего дерева вывода |
4 |
Лабораторная работа №4 Генерация и оптимизация объектного кода |
4 |
Всего |
16 |
Рецензенты:
Краткое
содержание
Введение......................................................................................................................................... 10
Лабораторная работа № 1
Организация таблиц
идентификаторов ............................................................................... 13
Лабораторная работа № 2
Проектирование
лексического анализатора ....................................................................... 39
Лабораторная работа № 3
Построение простейшего
дерева вывода............................................................................... 60
Лабораторная работа № 4
Генерация и оптимизация объектного кода.......................................................................... 95
Курсовая работа ...................................................................................................................... 133
Приложение 1
Функция переходов конечного автомата
для лабораторной работы №
2 .............................................................................................. 181
Приложение
2
Функция
переходов конечного автомата
для курсовой работы................................................................................................................. 184
Приложение 3
Тексты
программных модулей для курсовой работы..................................................... 190
Приложение
4
Примеры
входных и результирующих файлов
для курсовой работы................................................................................................................. 268
Литература .............................................................................................................................. 279
Алфавитный
указатель……………………………………………………………..282
Содержание
Введение
................................................................................................... 10
От
издательства......................................................................................................................... 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
ЛАБОРАТОРНАЯ РАБОТА № 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
ЛАБОРАТОРНАЯ РАБОТА № 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
ПРИЛОЖЕНИЕ
1
Функция
переходов конечного автомата
для лабораторной работы № 2 .............................................................. isi
ПРИЛОЖЕНИЕ 2
Функция
переходов конечного автомата
для курсовой работы............................................................................... ^
ПРИЛОЖЕНИЕ
3
Тексты программных
модулей для курсовой работы .. 19С
Модуль
структуры данных для таблицы идентификаторов
............................................ 19(
Модуль
таблицы идентификаторов на основе хэш-адресации
в комбинации с бинарным деревом.................................................................................... 19J
Модуль описания всех
типов лексем ......................................................................... 19{
Модуль описания структуры
элементов таблицы лексем ....................................... 20(
Модуль
заполнения таблицы лексем по исходному
тексту программы ..................................................................................................... 20С
Модуль
описания матрицы предшествования и правил
исходной грамматики................................................................................................... 211
Модуль
описания структур данных синтаксического анализатора
и реализации алгоритма
«сдвиг-свертка» ......................................................................
Модуль описания допустимых типов триад................................................................. 22!
Модуль
вычисления значений триад при свертке
объектного кода........................................................................................................... 22'
Модуль
описания структур данных триад.......................................................................... 22!
Модуль, реализующий алгоритмы оптимизации
списков триад....................................... 23
Модуль создания списка триад на основе дерева
разбора......................................... 23'
Модуль построения ассемблерного кода по списку
триад ...................................... 24<
Модуль интерфейса с пользователем........................................................................... 25!
ПРИЛОЖЕНИЕ
4
Примеры
входных и результирующих файлов
для курсовой работы ............................................................................ 26
Пример
1. Вычисление факториала ................................................................................... 26
Пример
2. Иллюстрация работы функций оптимизации
.................................................. 27
Литература
.............................................................................................. 27
Основная
литература .......................................................................................................... 27
Дополнительная
литература ............................................................................................... 27
Алфавитный
указатель...................................................................................................... 28
Введение
Эта книга является логическим продолжением и дополнением
учебника «Системное программное обеспечение»1, вышедшего в свет в
2003 году. Главной целевой аудиторией книги «Системное программное
обеспечение» были студенты технических вузов, обучающиеся по специальности
«Вычислительные машины, комплексы, системы и сети» и родственным с ней
направлениям, поэтому материал книги был подобран исходя из требований
стандарта этой специальности для курса «Системное программное обеспечение».
Программа этого курса предусматривает практические занятия в виде лабораторных
работ, а'также выполнение курсовой работы по итогам курса. Поэтому автор
посчитал разумным добавить к сухим теоретическим выкладкам необходимый живой
практический материал, проиллюстрированный конкретными примерами реализации.
Некоторая часть материала, касающаяся базовых теоретических основ, в этой книге
перекликается с уже опубликованным материалом книги «Системное программное
обеспечение». Но автор посчитал необходимым кратко
привести здесь только те теоретические выкладки, без которых невозможно
построить логическое изложение материала. Подразумевается, что читатели уже
знакомы с основами курса «Системное программное обеспечение», поэтому в
соответствующих местах всегда даются ссылки на литературу — в основном на
базовые книги курса [1-3, 7], а также на книги по курсу «Операционные системы»
[3, 5, 6]. Поскольку оба курса («Системное программное обеспечение» и
«Операционные системы») тесно взаимосвязаны, читателям этой книги необходимо
знать их основы, чтобы понять и практически применять изложенный в книге
материал (совсем недавно, в старой редакции образовательного стандарта, оба
этих курса составляли единое целое [3]).
Книга может оказаться полезной не только студентам, но и
специалистам, чья деятельность напрямую связана с созданием средств обработки
текстов и структурированных текстовых команд. Некоторые практические приемы,
описанные в книге и проиллюстрированные в примерах программного кода, будут
полезны не только тем, кто создает или изучает трансляторы, компиляторы или
любые другие распознаватели для формальных языков, но и вообще всем
разработчикам программного обеспечения.
Для
понимания практических примеров необходимо знание языка программирования Object Pascal и хотя бы общее представление о системе
программирования Delphi,
а также знание языка ассемблера процессоров типа Intel 80x86. В ряде случаев для сравнения и
понимания примеров синтаксических конструкций рекомендуется знать язык
программирования С. Соответствующие сведения можно почерпнуть в дополнительной
литературе, приведенной в конце книги [13, 23-25,28,31,32,37,39,41,44].
Все практические примеры созданы автором в системе
программирования Delphi
5 на языке Object
Pascal с
использованием примитивных классов из библиотеки VCL. Но автор приложил все усилия, чтобы они
не были привязаны ни к версии системы программирования, ни к особенностям
исходного языка. Поэтому желающие без проблем могут перенести их под любую
версию Delphi, а при
необходимости переписать, например па C++, для чего требуются только самые
элементарные знания языка.
Программный код, приводимый в примерах, ни в коей мере не
претендует на высокую эффективность. При его создании автор в первую очередь
думал об иллюстративности кода, о его способности наглядно отражать те
теоретические посылки, которые есть в книге. И тем не
менее использованные методы и приемы, по мнению автора, могут служить не только
примером реализации элементов компилятора, но и иллюстрацией хорошего стиля
программирования — но пусть об этом лучше судят сами читатели. Возможности
дальнейшего совершенствования кода чаще всего специально заложены в примерах, а
в тексте книги указано, в чем эти возможности заключаются. При проведении
занятий преподаватели могут использовать эти моменты для дополнительных заданий
по теме книги. Структура книги проста: она содержит описания четырех
лабораторных работ и одной курсовой работы. Каждая лабораторная работа
снабжена краткими теоретическими выкладками, имеет перечень вариантов заданий,
рекомендации по выполнению и оформлению результатов работы, а также пример
выполнения. В каждой лабораторной работе автор обращает внимание на основные
сложности, связанные с ее выполнением, а также на возможные типичные ошибки и
недочеты, дает рекомендации по возможностям программной реализации, отличным
от кода, приводимого в примерах.
Все лабораторные работы связаны с реализацией составных
частей компилятора. Первая работа посвящена организации таблиц идентификаторов,
вторая — созданию лексического анализатора, третья — созданию синтаксического
анализатора и четвертая — генерации и оптимизации результирующего кода. Работы
имеют разную сложность выполнения: по мнению автора, первые две работы элементарно
просты, третья — более сложная и, наконец, четвертая имеет максимальную сложность.
Это следует учитывать преподавателям при планировании выполнения работ и
обучающимся при их выполнении. Кроме того, все четыре работы
взаимосвязаны — каждая последующая работа использует материал предыдущей,
поэтому для обучающихся желательно иметь один номер варианта на выполнение всех
работ (взаимосвязь работ и преимущества такого подхода наглядно
проиллюстрированы в примерах их выполнения).
Курсовая
работа предусматривает создание простейшего компилятора для заданного входного
языка. Она основана на том же теоретическом материале, что и лабораторные
работы, но требует комплексного подхода к освоению материала и к выполнению
задания. Кроме того, в курсовой работе обучающимся предоставляется большая
самостоятельность в выборе методов и приемов реализации задания, чем в
лабораторных работах.
Практическая направленность данной книги требует
использования значительного объема программного кода для реализации и иллюстрации
выполняемых примеров в лабораторных работах и курсовой работе. К сожалению,
из-за ограничений по объему нет возможности включить весь программный код в
книгу. Поэтому автор счел необходимым привести в книге только программный код,
связанный с курсовой работой (часть которого используется также и в лабораторных
работах). Остальной программный код можно найти на веб-сайте издательства
«Питер».
Приводимый в
книге практический материал не претендует на полноту охвата всего курса
«Системное программное обеспечение». Автор считает необходимым дополнить его
работами по программированию параллельных взаимодействующих процессов [3, 5],
а также методами разработки программного обеспечения в распределенных системах
(по технологиям построения систем «клиент-сервер» и многоуровневой архитектуре
[7]). Автор надеется, что ему удастся в ближайшее время подготовить
соответствующий материал.
ЛАБОРАТОРНАЯ РАБОТА № 1
Организация таблиц идентификаторов
Цель работы
Цель работы: изучить
основные методы организации таблиц идентификаторов, получить представление о
преимуществах и недостатках, присущих различным методам организации таблиц
идентификаторов.
Для
выполнения лабораторной работы требуется написать программу, которая получает
на входе набор идентификаторов, организует таблицы идентификаторов с помощью
заданных методов, позволяет осуществить многократный поиск произвольного
идентификатора в таблицах и сравнить эффективность методов организации таблиц.
Список идентификаторов считать заданным в виде текстового файла. Длина
идентификаторов ограничена 32 символами.
Краткие
теоретические сведения Назначение таблиц идентификаторов
При выполнении семантического анализа, генерации кода и
оптимизации результирующей программы компилятор должен оперировать
характеристиками основных элементов исходной программы — переменных, констант,
функций и других лексических единиц входного языка. Эти характеристики могут
быть получены компилятором на этапе синтаксического анализа входной программы
(чаще всего при анализе структуры блоков описаний переменных 'и констант), а
также дополнены на этапе подготовки к генерации кода (например
при распределении памяти).
Набор характеристик, соответствующий
каждому элементу исходной программы, зависит от типа этого элемента, от его
смысла (семантики) и, соответственно, от той роли, которую он исполняет в
исходной и результирующей программах. В каждом конкретном случае этот набор характеристик может
быть свой в зависимости от синтаксиса и семантики входного языка, от
архитектуры целевой вычислительной системы и от структуры компилятора. Но есть
типовые характеристики, которые чаще всего присущи тем или иным элементам
исходной программы. Например для переменной — это ее
тип и адрес ячейки памяти, для константы — ее значение, для функции —
количество и типы формальных аргументов, тип возвращаемого результата, адрес
вызова кода функции. Более подробную информацию о характеристиках элементов
исходной программы, их анализе и использовании можно найти в [1, 3, 7].
Главной
характеристикой любого элемента исходной программы является его имя. Именно с
именами переменных, констант, функций и других элементов входного языка
оперирует разработчик программы — поэтому и компилятор должен уметь
анализировать эти элементы по их именам.
Имя
каждого элемента должно быть уникальным. Многие современные языки
программирования допускают совпадения (не уникальность) имен переменных и
функций в зависимости от их области видимости и других условий исходной
программы. В этом случае уникальность имен должен обеспечивать сам компилятор
— о том, как решается эта проблема, можно узнать в [1-3, 7], здесь же будем
считать, что имена элементов исходной программы всегда являются уникальными.
Таким
образом, задача компилятора заключается в том, чтобы хранить некоторую
информацию, связанную с каждым элементом исходной программы, и иметь доступ к
этой информации по имени элемента. Для решения этой задачи компилятор
организует специальные хранилища данных, называемые таблицами идентификаторов,
или таблицами символов. Таблица идентификаторов состоит из набора
полей данных (записей), каждое из которых может соответствовать одному
элементу исходной программы. Запись содержит всю необходимую компилятору
информацию о данном элементе и может пополняться по мере работы компилятора.
Количество записей зависит от способа организации таблицы идентификаторов, но
в любом случае их не может быть меньше, чем элементов в исходной программе. В
принципе, компилятор может работать не с одной, а с несколькими таблицами
идентификаторов — их количество и структура зависят от реализации компилятора
[1, 2].
Принципы
организации таблиц идентификаторов
Компилятор
пополняет записи в таблице идентификаторов по мере анализа исходной программы
и обнаружения в ней новых элементов, требующих размещения в таблице. Поиск
информации в таблице выполняется всякий раз, когда компилятору необходимы
сведения о том или ином элементе программы. Причем следует заметить, что поиск
элемента в таблице будет выполняться компилятором существенно чаще, чем
помещение в нее новых элементов. Так происходит, потому, что описания новых
элементов в исходной программе, как правило, встречаются гораздо реже, чем эти
элементы используются. Кроме того, каждому добавлению элемента в таблицу
идентификаторов в любом случае будет предшествовать операция поиска — чтобы
убедиться, что такого элемента в таблице нет.
На
каждую операцию поиска элемента в таблице компилятор будет затрачивать время, и
поскольку количество элементов в исходной программе велико (от единиц до сотен
тысяч в зависимости от объема программы), это время будет сущеественно влиять
на общее время компиляции. Поэтому таблицы идентификаторов должны быть
организованы таким образом, чтобы компилятор имел возможность максимально быстро
выполнять поиск нужной ему записи таблицы по имени элемента, с которым связана
эта запись. Можно выделить следующие способы организации таблиц
идентификаторов:
□
простые
и упорядоченные списки;
□
бинарное
дерево;
а хэш-адресация с рехэшированием; Q
хэш-адресация по методу цепочек;
□ комбинация хэш-адресации со списком или
бинарным деревом.
Далее будет дано краткое описание всех вышеперечисленных
способов организации таблиц идентификаторов. Более подробную информацию можно
найти в [3,7].
Простейшие
методы построения таблиц идентификаторов
В простейшем случае таблица идентификаторов представляет
собой линейный неупорядоченный список, или массив, каждая ячейка которого
содержит данные о соответствующем элементе таблицы. Размещение новых элементов
в такой таблице выполняется путем записи информации в очередную ячейку массива
или списка по мере обнаружения новых элементов в исходной программе. Поиск
нужного элемента в таблице будет в этом случае выполняться путем последовательного
перебора всех элементов и сравнения их имени с именем искомого элемента, пока
не будет найден элемент с таким же именем. Тогда если за единицу времени
принять время, затрачиваемое компилятором на сравнение двух строк (в
современных вычислительных системах такое сравнение чаще всего выполняется
одной командой), то для таблицы, содержащей N элементов, в среднем будет
выполнено N/2
сравнений.
Время, требуемое на добавление нового элемента в таблицу
(Г), не зависит от числа элементов в таблице (N). Но если N велико, то поиск потребует значительных
затрат времени. Время поиска (Ги) в такой таблице можно оценить как Т
= O(iV). Поскольку именно поиск в таблице
идентификаторов является наиболее часто выполняемой компилятором операцией,
такой способ организации таблиц идентификаторов является неэффективным. Он
применим только для самых простых компиляторов, работающих с небольшими программами.
Поиск может быть выполнен более эффективно, если элементы
таблицы отсортированы (упорядочены) естественным образом. Поскольку поиск
осуществляется по имени, наиболее естественным решением будет расположить
элементы таблицы в прямом или обратном алфавитном порядке. Эффективным методом
поиска в упорядоченном списке из N элементов является бинарный, или логарифмический- поиск.
Алгоритм
логарифмического поиска заключается в следующем: искомый символ сравнивается с
элементом (N+
1)/2 в середине таблицы;
если этот элемент не является искомым, то мы должны просмотреть только блок
элементов, пронумерованных от 1 до (JV + 1)/2 - 1, или блок элементов от (N + 1)/2 + 1 до JVb зависимости от того, меньше или больше
искомый элемент того, с которым его сравнили. Затем процесс повторяется над
нужным блоком в два раза меньшего размера. Так продолжается до тех пор, пока
либо искомый элемент не будет найден, либо алгоритм не дойдет до очередного
блока, содержащего один или два элемента (с которыми можно выполнить прямое
сравнение искомого элемента).
Так как на каждом шаге число элементов, которые могут
содержать искомый элемент, сокращается в два раза, максимальное число сравнений
равно 1 + log2 N. Тогда время поиска элемента в
таблице идентификаторов можно оценить как ТП = 0(log2 N).
Для сравнения: при N
= 128 бинарный поиск требует самое большее 8 сравнений, а поиск в
неупорядоченной таблице — в среднем 64 сравнения. Метод называют «бинарным
поиском», поскольку на каждом шаге объем рассматриваемой информации сокращается
в два раза, а «логарифмическим» — поскольку время, затрачиваемое на поиск
нужного элемента в массиве, имеет логарифмическую зависимость от общего
количества элементов в нем. Недостатком логарифмического поиска является
требование упорядочивания таблицы идентификаторов. Так как массив информации, в
котором выполняется поиск, должен быть упорядочен, время его заполнения уже
будет зависеть от числа элементов в массиве. Таблица идентификаторов зачастую
просматривается компилятором еще до того, как она заполнена, поэтому требуется,
чтобы условие упорядоченности выполнялось на всех этапах обращения к ней.
Следовательно, для построения такой таблицы можно пользоваться только
алгоритмом прямого упорядоченного включения элементов.
Если пользоваться стандартными алгоритмами, применяемыми для
организации упорядоченных массивов данных, то среднее время, необходимое на
помещение всех элементов в таблицу, можно оценить следующим образом:
Гд = 0(Nlog2
N) + k-O(N).
Здесь k
— некоторый коэффициент,
отражающий соотношение между временами, затрачиваемыми компьютером на
выполнение операции сравнения и операции переноса данных.
При организации логарифмического поиска в таблице
идентификаторов обеспечивается существенное сокращение времени поиска нужного
элемента за счет увеличения времени на помещение нового элемента в таблицу.
Поскольку добавление новых элементов в таблицу идентификаторов происходит
существенно реже, чем обращение к ним, этот метод следует признать более
эффективным, чем метод организации неупорядоченной таблицы. Однако в реальных
компиляторах этот метод непосредственно также не используется, поскольку
существуют более эффективные методы.
Построение таблиц идентификаторов по
методу бинарного дерева
Можно
сократить время поиска искомого элемента в таблице идентификаторов, не
увеличивая значительно время, необходимое на ее заполнение. Для этого надо
отказаться от организации таблицы в виде непрерывного массива данных.
Существует
метод построения таблиц, при котором таблица имеет форму бинарного дерева.
Каждый узел дерева представляет собой элемент таблицы, причем корневым узлом
становится первый элемент, встреченный компилятором при заполнении таблицы. Дерево
называется бинарным, так как каждая вершина в нем может иметь не более двух
ветвей. Для определенности будем называть две ветви «правая» и«левая».
Рассмотрим алгоритм заполнения бинарного дерева. Будем
считать, что алгоритм работает с потоком входных данных, содержащим
идентификаторы. Первый идентификатор, как уже было сказано, помещается в
вершину дерева. Все дальнейшие идентификаторы попадают в дерево по следующему
алгоритму:
1.
Выбрать
очередной идентификатор из входного потока данных. Если очередного
идентификатора нет, то построение дерева закончено.
2.
Сделать
текущим узлом дерева корневую вершину.
3.
Сравнить
имя очередного идентификатора с именем идентификатора, содержащегося в текущем
узле дерева.
4.
Если
имя очередного идентификатора меньше, то перейти к шагу 5, если равно —
прекратить выполнение алгоритма (двух одинаковых идентификаторов быть не
должно!), иначе — перейти к шагу 7.
5.
Если
у текущего узла существует левая вершина, то сделать ее текущим узлом и
вернуться к шагу 3, иначе — перейти к шагу 6.
6.
Создать
новую вершину, поместить в нее информацию об очередном идентификаторе, сделать
эту новую вершину левой вершиной текущего узла и вернуться к шагу 1.
7.
Если
у текущего узла существует правая вершина, то сделать ее текущим узлом и
вернуться к шагу 3, иначе — перейти к шагу 8.
8.
Создать
новую вершину, поместить в нее информацию об очередном идентификаторе, сделать
эту новую вершину правой вершиной текущего узла и вер— v-нуться к шагу 1.
Рассмотрим
в качестве примера последовательность идф^фидатрдов, G*,, Щ, W22; КЫЙ
Е, А12, ВС, F.
На рис. 1.1 проиллюстрирован весь процесс тюстшщ^Я^ЩЩЩтцЦх,,,!уг
рева для этой последовательности идентификаторов. ' ' г Tii'Hl^e&e
Рис.
1.1. Заполнение бинарного дерева для последовательности идентификаторов
Ga, D1, М22, Е, А12, ВС, F
Поиск
элемента в дереве выполняется по алгоритму, схожему с алгоритмом заполнения
дерева:
1.
Сделать
текущим узлом дерева корневую вершину.
2.
Сравнить
имя искомого идентификатора с именем идентификатора, содержащимся в текущем
узле дерева.
3.
Если
имена совпадают, то искомый идентификатор найден, алгоритм завершается, иначе
надо перейти к шагу 4.
4.
Если
имя очередного идентификатора меньше, то перейти к шагу 5, иначе — перейти к
шагу 6.
5.
Если
у текущего узла существует левая вершина, то сделать ее текущим узлом и
вернуться к шагу 2, иначе — искомый идентификатор не найден, алгоритм
завершается.
6.
Если
у текущего узла существует правая вершина, то сделать ее текущим узлом и
вернуться к шагу 2, иначе — искомый идентификатор не найден, алгоритм завершается.
Для
данного метода число требуемых сравнений и форма получившегося дерева зависят
от того порядка, в котором поступают идентификаторы. Например, если в
рассмотренном выше примере вместо последовательности идентификаторов Ga, D1,
М22, Е, А12, ВС, F взять
последовательность А12, ВС, Dl, E,
F, Ga, M22,
то дерево выродится в
упорядоченный однонаправленный связный список. Эта особенность является
недостатком данного метода организации таблиц идентификаторов. Другими недостатками метода являются: необходимость хранить две
дополнительные ссылки на левую и правую ветви в каждом элементе дерева и
работа с динамическим выделением памяти при построении дерева.
Если
предположить, что последовательность идентификаторов в исходной программе
является статистически неупорядоченной (что в целом соответствует действительности),
то можно считать, что построенное бинарное дерево будет невырожденным. Тогда
среднее время на заполнение дерева (Т') и на поиск элемента в нем (Ти)
можно оценить следующим образом [3, 7]:
Гл = A/-0(log2JV);
r„
= 0(log2iV).
Несмотря на указанные недостатки, метод бинарного дерева
является довольно удачным механизмом для организации таблиц идентификаторов. Он
нашел свое применение в ряде компиляторов. Иногда компиляторы строят несколько
различных деревьев для идентификаторов разных типов и разной длины [1, 2, 3,
7].
Хэш-функции
и хэш-адресация
В реальных исходных программах количество идентификаторов
столь велико, что даже логарифмическую зависимость времени поиска от их числа
нельзя признать удовлетворительной. Необходимы более эффективные методы поиска
информации в таблице идентификаторов. Лучших результатов можно достичь, если
применить методы, связанные с использованием хэш-функций и хэш-адресации.
Хэш-функцией F называется некоторое отображение
множества входных элементов R
на множество целых неотрицательных чисел Z: F(r) = n,re R, n e Z. Сам термин «хэш-функция» происходит от
английского термина «hash
function» (hash — «мешать», «смешивать», «путать»).
Множество
допустимых входных элементов R
называется областью определения хэш-функции. Множеством значений хэш-функции F называется подмножество М из множества
целых неотрицательных чисел Z:
М с Z, содержащее все
возможные значения, возвращаемые функцией F: VrlR: F(r) е М и Mm
е М: Зге R: F(r) = m.
Процесс отображения
области определения хэш-функции на множество значений называется хэшированием.
При работе с таблицей идентификаторов хэш-функция должна
выполнять отображение имен идентификаторов на множество целых неотрицательных
чисел. Областью определения хэш-функции будет множество всех возможных имен
идентификаторов.
Хэш-адресация заключается
в использовании значения, возвращаемого хэш-функцией, в качестве адреса ячейки
из некоторого массива данных. Тогда размер массива данных должен
соответствовать области значений используемой хэш-функции. Следовательно, в
реальном компиляторе область значений хэш-функции никак не должна превышать
размер доступного адресного пространства компьютера. Метод организации таблиц
идентификаторов, основанный на использовании хэш-адресации, заключается в
помещении каждого элемента таблицы в ячейку, адрес которой возвращает
хэш-функция, вычисленная для этого элемента. Тогда в идеальном случае для
помещения любого элемента в таблицу идентификаторов достаточно только вычислить
его хэш-функцию и обратиться к нужной ячейке массива данных. Для
поиска элемента в таблице также необходимо вычислить хэш-функцию для искомого
элемента и проверить, не является ли заданная ею ячейка массива пустой (если
она не пуста — элемент найден, если пуста — не найден). Первоначально
таблица идентификаторов должна быть заполнена информацией, которая позволила бы
говорить о том, что все ее ячейки являются пустыми. Этот метод весьма
эффективен, поскольку как время размещения элемента в таблице, так и время его
поиска определяются только временем, затрачиваемым на вычисление хэш-функции,
которое в общем случае несопоставимо меньше времени, необходимого для
многократных сравнений элементов таблицы. Метод имеет два очевидных недостатка.
Первый из них — неэффективное использование объема памяти под таблицу
идентификаторов: размер массива для ее хранения должен соответствовать всей
области значений хэш-функции, в то время как реально хранимых в таблице
идентификаторов может быть существенно меньше. Второй недостаток —
необходимость соответствующего разумного выбора хэш-функции. Этот недостаток
является настолько существенным, что не позволяет непосредственно использовать
хэш-адресацию для организации таблиц идентификаторов.
Проблема
выбора хэш-функции не имеет универсального решения. Хэширование обычно
происходит за счет выполнения над цепочкой символов некоторых простых
арифметических и логических операций. Самой простой хэш-функцией для символа
является код внутреннего представления в компьютере литеры символа. Эту
хэш-функцию можно использовать и для цепочки символов, выбирая первый символ в
цепочке.
Очевидно, что такая примитивная хэш-функция будет
неудовлетворительной: при ее использовании возникнет проблема — двум различным
идентификаторам, начинающимся с одной и той же буквы, будет соответствовать
одно и то же значение хэш-функции. Тогда при хэш-адресации в одну и ту же
ячейку таблицы идентификаторов должны быть помещены два различных
идентификатора, что явно невозможно. Такая ситуация, когда двум или более
идентификаторам соответствует одно и то же значение хэш-функции, называется коллизией.
Естественно, что хэш-функция, допускающая коллизии, не может быть использована
для хэш-адресации в таблице идентификаторов. Причем достаточно получить хотя
бы один случай коллизии на всем множестве идентификаторов, чтобы такой
хэш-функцией нельзя было пользоваться. Но возможно ли построить хэш-функцию,
которая бы полностью исключала возникновение коллизий? Для полного исключения
коллизий хэш-функция должна быть взаимно однозначной: каждому элементу из
области определения хэш-функции должно соответствовать одно значение из ее
множества значений, и наоборот — каждому значению из множества значений этой
функции должен соответствовать только один элемент из ее области определения.
Тогда любым двум произвольным элементам из области определения хэш-функции
будут всегда соответствовать два различных ее значения. Теоретически для
идентификаторов такую хэш-функцию построить можно, так как и область
определения хэш-функции (все возможные имена идентификаторов), и область ее
значений (целые неотрицательные числа) являются бесконечными счетными
множествами, поэтому можно организовать взаимно однозначное отображение одного
множества на другое.
Но
на практике существует ограничение, делающее создание взаимно однозначной
хэш-функции для идентификаторов невозможным. Дело в том, что в реальности
область значений любой хэш-функции ограничена размером доступного адресного
пространства компьютера. Множество адресов любого компьютера с традиционной
архитектурой может быть велико, но всегда конечно, то есть ограничено.
Организовать взаимно однозначное отображение бесконечного множества на конечное даже теоретически невозможно. Можно, конечно,
учесть, что длина принимаемой во внимание части имени идентификатора в реальных
компиляторах на практике также ограничена — обычно она лежит в пределах от 32
до 128 символов (то есть и область определения хэш-функции конечна). Но и тогда
количество элементов в конечном множестве, составляющем область определения
хэш-функции, будет превышать их количество в конечном множестве области ее
значений (количество всех возможных идентификаторов больше количества
допустимых адресов в современных компьютерах). Таким образом, создать взаимно
однозначную хэш-функцию на практике невозможно. Следовательно, невозможно
избежать возникновения коллизий.
Поэтому
нельзя организовать таблицу идентификаторов непосредственно на основе одной
только хэш-адресации. Но существуют методы, позволяющие использовать
хэш-функции для организации таблиц идентификаторов даже при наличии коллизий.
Хэш-адресация с
рехэшированием
Для
решения проблемы коллизии можно использовать много способов. Одним из них
является метод рехэширования (или расстановки). Согласно этому методу,
если для элемента А адрес п0 = h(A), вычисленный с помощью хэш-функции h, указывает па уже занятую ячейку, то необходимо вычислить
значение функции п\ = ЬХ(А) и
проверить занятость ячейки по адресу пх. Если и она занята,
то вычисляется значение h2(A), и так до тех пор, пока либо не будет найдена свободная
ячейка, либо очередное значение h:(A) не совпадет с h(A). В последнем случае считается, что таблица идентификаторов заполнена и места в ней больше нет — выдается информация об
ошибке размещения идентификатора в таблице. Тогда поиск элемента А в таблице идентификаторов, организованной
таким образом, будет выполняться по следующему алгоритму:
1.
Вычислить
значение хэш-функции п = h(A) для искомого элемента А.
Если ячейка по адресу п пустая,
то элемент не найден, алгоритм завершен, иначе необходимо сравнить имя элемента
в ячейке п с именем искомого элемента Л. Если они совпадают, то элемент
найден и алгоритм завершен, иначе i := 1 и перейти к шагу 3. 3. Вычислить п. = h;(A). Если ячейка по адресу и. пустая или п = я., то элемент не найден и алгоритм
завершен, иначе — сравнить имя элемента в ячейке п. с именем искомого
элемента Л. Если они совпадают, то элемент найден и алгоритм завершен, иначе i := i + 1 и повторить шаг3.
Алгоритмы размещения и поиска элемента схожи по выполняемым
операциям. Поэтому они будут иметь одинаковые оценки времени, необходимого для
их выполнения.
При
такой организации таблиц идентификаторов в случае возникновения коллизии
алгоритм помещает элементы в пустые ячейки таблицы, выбирая их определенным
образом. При этом элементы могут попадать в ячейки с адресами, которые потом
будут совпадать со значениями хэш-функции, что приведет к возникновению новых,
дополнительных коллизий. Таким образом, количество операций, необходимых для
поиска или размещения в таблице элемента, зависит от заполненности таблицы.
Для
организации таблицы идентификаторов по методу рехэширования необходимо
определить все хэш-функции hi для всех г. Чаще всего функции ht определяют как некоторые модификации
хэш-функции h.
Например, самым простым
методом вычисления функции hs(A)
является ее организация
в виде /г,.(Л) = (h(A) + р;)
mod JVm, гдер; — некоторое вычисляемое целое число,
а Nm — максимальное значение из области значений хэш-функции h. В свою очередь, самым простым подходом здесь будет
положить;?, = i.
Тогда получаем формулу
/г,(Л) = (h(A) + i) mod Nm. В этом случае при совпадении значений хэш-функции для
каких-либо элементов поиск свободной ячейки в таблице начинается
последовательно от текущей позиции, заданной хэш-функцией h(A).
Этот
способ нельзя признать особенно удачным: при совпадении хэш-адресов элементы в
таблице начинают группироваться вокруг них, что увеличивает число необходимых
сравнений при поиске и размещении. Но даже такой примитивный метод
рехэширования является достаточно эффективным средством организации таблиц
идентификаторов при неполном заполнении таблицы. Среднее время на помещение
одного элемента в таблицу и на поиск элемента в таблице можно снизить, если
применить более совершенный метод рехэширования. Одним из таких методов
является использование в качестве р. для функции /г;(Л)
= (h(A) + p) mod Nm последовательности псевдослучайных целых
чисел pvpit ...,рк. При хорошем выборе генератора
псевдослучайных чисел длина последовательности k - Nm.
Существуют и другие методы организации функций рехэширования
ht(A), основанные на квадратичных вычислениях или, например, на
вычислении произведения по формуле: /г,(Л) = (h(A)N-i) mod N'm, тц,еЫ'т — ближайшее простое число, меньшее Nm. В целом рехэширование позволяет добиться неплохих
результатов для эффективного поиска элемента в таблице (лучших, чем бинарный
поиск и бинарное дерево), но эффективность метода сильно зависит от
заполненности таблицы идентификаторов и качества используемой хэш-фуикции —
чем реже возникают коллизии, тем выше эффективность метода. Требование
неполного заполнения таблицы ведет к неэффективному использованию объема
доступной памяти.
Оценки
времени размещения и поиска элемента в таблицах идентификаторов при
использовании различных методов рехэширования можно найти в [1, 3, 7].
Хэш-адресация
с использованием метода цепочек
Неполное заполнение таблицы идентификаторов при применении
рехэширования ведет к неэффективному использованию всего объема памяти,
доступного компилятору. Причем объем неиспользуемой памяти будет тем выше, чем
больше информации хранится для каждого идентификатора. Этого недостатка можно
избежать, если дополнить таблицу идентификаторов некоторой
промежуточной хэш-таблицей.
В ячейках хэш-таблицы может храниться либо пустое значение,
либо значение указателя на некоторую область памяти из основной таблицы
идентификаторов. Тогда хэш-функция вычисляет адрес, по которому происходит
обращение сначала к хэш-таблице, а потом уже через нее по найденному адресу —
к самой таблице идентификаторов. Если соответствующая ячейка таблицы
идентификаторов пуста, то ячейка хэш-таблицы будет содержать пустое значение.
Тогда вовсе не обязательно иметь в самой таблице идентификаторов ячейку для каждого
возможного значения хэш-функции — таблицу можно сделать динамической, так
чтобы ее объем рос по мере заполнения (первоначально таблица идентификаторов не
содержит ни одной ячейки, а все ячейки хэш-таблицы имеют пустое значение).
Такой подход позволяет добиться двух положительных результатов: во-первых, нет
необходимости заполнять пустыми значениями таблицу идентификаторов — это можно
сделать только для хэш-таблицы; во-вторых, каждому идентификатору будет
соответствовать строго одна ячейка в таблице идентификаторов. Пустые ячейки в
таком случае будут только в хэш-таблице, и объем неиспользуемой памяти не будет
зависеть от объема информации, хранимой для каждого идентификатора, — для
каждого значения хэш-функции будет расходоваться только память, необходимая
для хранения одного указателя на основную таблицу идентификаторов.
На
основе этой схемы можно реализовать еще один способ организации таблиц
идентификаторов с помощью хэш-функции, называемый методом цепочек. В
этом случае в таблицу идентификаторов для каждого элемента добавляется еще одно
поле, в котором может содержаться ссылка на любой элемент таблицы. Первоначально
это поле всегда пустое (никуда не указывает). Также необходимо иметь одну
специальную переменную, которая всегда указывает на первую свободную ячейку
основной таблицы идентификаторов (первоначально она указывает на начало
таблицы). Метод цепочек работает по следующему алгоритму:
1. Во все ячейки хэш-таблицы поместить
пустое значение, таблица идентификаторов пуста, переменная FreePtr (указатель первой свободной ячейки) указывает
на начало таблицы идентификаторов.
2.
Вычислить
значение хэш-функции п для нового
элемента А. Если ячейка хэш-таблицы по адресу п пустая, то
поместить в нее значение переменной FreePtr и перейти к шагу 5; иначе перейти к шагу
3.
3.
Выбрать
из хэш-таблицы адрес ячейки таблицы идентификаторов т и перейти к шагу
4.
4.
Для
ячейки таблицы идентификаторов по адресу т проверить значение поля
ссылки. Если оно пустое, то записать в него адрес из переменной FreePtr и перейти к шагу 5; иначе выбрать из
поля ссылки новый адрес т и повторить шаг 4.
5.
Добавить
в таблицу идентификаторов новую ячейку, записать в нее информа-
' цию для элемента Л (поле ссылки должно быть пустым), в переменную FreePtr поместить адрес за концом добавленной
ячейки. Если больше нет идентификаторов, которые надо поместить в таблицу, то
выполнение алгоритма закончено, иначе перейти к шагу 2.
Поиск элемента в таблице идентификаторов, организованной
таким образом, будет выполняться по следующему алгоритму:
1.
Вычислить
значение хэш-функции п для искомого
элемента А. Если ячейка хэш-таблицы по адресу п пустая, то
элемент не найден и алгоритм завершен, иначе выбрать из хэш-таблицы адрес
ячейки таблицы идентификаторов т.
2.
Сравнить
имя элемента в ячейке таблицы идентификаторов по адресу т
с именем искомого элемента А. Если они
совпадают, то искомый элемент найден и алгоритм завершен, иначе перейти к шагу
3.
3.
Проверить
значение поля ссылки в ячейке таблицы идентификаторов по адресу т. Если
оно пустое, то искомый элемент не найден и алгоритм завершен; иначе выбрать из
поля ссылки адрес т и перейти к шагу 2.
При
такой организации таблиц идентификаторов в случае возникновения коллизии
алгоритм помещает элементы в ячейки таблицы, связывая
их друг с другом последовательно через
поле ссылки. При этом элементы не могут попадать в ячейки с адресами, которые
потом будут совпадать со значениями хэш-функции. Таким образом, дополнительные
коллизии не возникают. В итоге в таблице возникают своеобразные цепочки
связанных элементов, откудаи происходит название данного метода — «метод
цепочек».
На рис. 1.2 проиллюстрировано заполнение хэш-таблицы и
таблицы идентифи-каторов для ряда идентификаторов: AvA2iAyA4,A5 при условии, что А(Л,) = h(A2) = = h(A5) = щ, /г(Л3) = п2; h(A4) = nA. После размещения в таблице для поиска идентификатора А, потребуется одно сравнение, для А2
— два сравнения, для А3 — одно
сравнение, для Л4 — одно сравнение и для А5 — три
сравнения (попробуйте сравнить эти данные с результатами, полученными с
использованием простого рехэ-ширования для тех же идентификаторов).
Метод цепочек является очень эффективным средством
организации таблиц идентификаторов. Среднее время на размещение одного
элемента и на поиск элемента в таблице для него зависит только от среднего
числа коллизий, возникающих при вычислении хэш-функции. Накладные расходы
памяти, связанные с необходимостью иметь одно дополнительное поле указателя в
таблице идентификаторов на каждый ее элемент, можно признать вполне
оправданными, так как возникает экономия используемой памяти за счет
промежуточной хэш-таблицы. Этот метод позволяет более экономно использовать
память, но требует организации работы с динамическими массивами данных.
Комбинированные
способы построения таблиц идентификаторов
Кроме
рехэширования и метода цепочек можно использовать комбинированные методы для
организации таблиц идентификаторов .с помощью
хэш-адресации. В этом случае для исключения коллизий хэш-адресация сочетается с
одним из ранее рассмотренных методов — простым списком, упорядоченным списком
или бинарным деревом, который используется как дополнительный метод упорядочивания
идентификаторов, для которых возникают коллизии. Причем, поскольку при
качественном выборе хэш-функции количество коллизий обычно невелико (единицы
или десятки случаев), даже простой список может быть вполне удовлетворительным
решением при использовании комбинированного метода.
При таком
подходе возможны два варианта: в первом случае, как и для метода цепочек, в
таблице идентификаторов организуется специальное дополнительное поле ссылки. Но
в отличие от метода цепочек оно имеет несколько иное значение: при отсутствии
коллизий для выборки информации из таблицы используется хэш-функция, поле
ссылки остается пустым. Если же возникает коллизия, то через поле ссылки
организуется поиск идентификаторов, для которых значения хэш-функции совпадают
— это поле должно указывать на структуру данных для дополнительного метода:
начало списка, первый элемент динамического массива или корневой элемент
дерева.
Во втором
случае используется хэш-таблица, аналогичная хэш-таблице для метода цепочек.
Если по данному адресу хэш-функции идентификатор отсутствует, то ячейка
хэш-таблицы пустая. Когда появляется идентификатор с данным значением
хэш-функции, то создается соответствующая структура для дополнительного метода,
в хэш-таблицу записывается ссылка на эту структуру, а идентификатор помещается
в созданную структуру по правилам выбранного дополнительного метода. В первом
варианте при отсутствии коллизий поиск выполняется быстрее, но второй вариант
предпочтительнее, так как за счет использования промежуточной хэш-таблицы
обеспечивается более эффективное использование памяти. Как и для метода
цепочек, для комбинированных методов время размещения и время поиска элемента в
таблице идентификаторов зависит только от среднего числа коллизий, возникающих
при вычислении хэш-функции. Накладные расходы памяти при использовании
промежуточной хэш-таблицы минимальны. Очевидно, что если в качестве
дополнительного метода использовать простой список, то получится алгоритм,
полностью аналогичный методу цепочек. Если же использовать упорядоченный список
или бинарное дерево, то метод цепочек и комбинированные методы будут иметь
примерно равную эффективность при незначительном числе коллизий (единичные
случаи), но с ростом количества коллизий эффективность комбинированных методов
по сравнению с методом цепочек будет возрастать.
Недостатком
комбинированных методов является более сложная организация алгоритмов поиска и
размещения идентификаторов, необходимость работы с динамически распределяемыми
областями памяти, а также большие затраты времени на размещение нового
элемента в таблице идентификаторов по сравнению с методом цепочек.
То, какой
конкретно метод применяется в компиляторе для организации таблиц
идентификаторов, зависит от реализации компилятора. Один и тот же компилятор
может иметь даже несколько разных таблиц идентификаторов, организованных на
основе различных методов. Как правило, применяются комбинированные методы.
Создание эффективной хэш-функции — это отдельная задача разработчиков компиляторов,
и полученные результаты, как правило, держатся в секрете. Хорошая хэш-функция
распределяет поступающие на ее вход идентификаторы равномерно на все имеющиеся
в распоряжении адреса, чтобы свести к минимуму количество коллизий. В
настоящее время существует множество хэш-функций, но, как было показано выше,
идеального хэширования достичь невозможно.
Хэш-адресация — это метод, который применяется не только для
организации таблиц идентификаторов в компиляторах. Данный метод нашел свое
применение и в операционных системах, и в системах управления базами данных [5,
6, 11].
Требования
к выполнению работы Порядок выполнения
работы
Во
всех вариантах задания требуется разработать программу, которая может обеспечить
сравнение двух способов организации таблицы идентификаторов с помощью
хэш-адресации. Для сравнения предлагаются способы, основанные на использовании
рехэширования или комбинированных методов. Программа должна считывать
идентификаторы из входного файла, размещать их в таблицах с помощью заданных
методов и выполнять поиск указанных идентификаторов по требованию
пользователя. В процессе размещения и поиска идентификаторов в таблицах
программа должна подсчитывать среднее число выполненных операций сравнения для
сопоставления эффективности используемых методов.
Для
организации таблиц предлагается использовать простейшую хэш-функцию, которую
разработчик программы должен выбрать самостоятельно. Хэш-функция должна
обеспечивать работу не менее чем с 200 идентификаторами, допустимая длина
идентификатора должна быть не менее 32 символов. Запрещается использовать в
работе хэш-функции, взятые из примера выполнения работы.
Лабораторная работа должна выполняться в
следующем порядке:
1.
Получить
вариант задания у преподавателя.
2.
Выбрать
и описать хэш-функцию.
3.
Описать
структуры данных, используемые для заданных методов организации таблиц
идентификаторов.
4.
Подготовить
и защитить отчет.
5.
Написать
и отладить программу на ЭВМ.
6.
Сдать
работающую программу преподавателю.
Требования
к оформлению отчета
Отчет по лабораторной работе должен
содержать следующие разделы:
□ задание по лабораторной работе;
□ описание выбранной хэш-функции;
□
схемы
организации таблиц идентификаторов (в соответствии с вариантом задания);
□
описание
алгоритмов поиска в таблицах идентификаторов (в соответствии с вариантом
задания);
□ текст программы
(оформляется после выполнения программы на ЭВМ);
□
результаты обработки заданного набора идентификаторов (входного файла) с
помощью методов организации таблиц идентификаторов, указанных в варианте задания;
а
анализ эффективности используемых методов организации таблиц
идентификаторов и выводы по проделанной
работе.
Основные
контрольные вопросы
а
Что такое таблица символов и для чего она предназначена? Какая информация может храниться в
таблице символов?
□ Какие
цели преследуются при организации таблицы символов?
□ Какими
характеристиками могут обладать лексические элементы исходной программы?
Какие характеристики являются обязательными?
□ Какие
существуют способы организации таблиц символов?
□ В
чем заключается алгоритм логарифмического поиска? Какие преимущества он
дает по сравнению с простым перебором и какие он имеет недостатки?
□ Расскажите
о древовидной организации таблиц идентификаторов. В чем ее преимущества и
недостатки?
□ Что такое хэш-функции и для чего они используются?
В чем суть хэш-адресации?
а
Что такое коллизия? Почему она происходит? Можно ли полностью избежать
коллизий?
Q Что такое рехэширование? Какие
методы рехэширования существуют?
а Расскажите о преимуществах и недостатках организации таблиц
идентификаторов
с помощью хэш-адресации и рехэширования.
□ В чем
заключается метод цепочек?
□ Расскажите
о преимуществах и недостатках организации таблиц идентификаторов с помощью хэш-адресации
и метода цепочек.
□ Как
могут быть скомбинированы различные методы организации хеш-таблиц?
□ Расскажите
о преимуществах и недостатках организации таблиц идентификаторов с помощью
комбинированных методов.
Варианты заданий
В табл. 1.1 перечислены методы организации таблиц
идентификаторов, используемые
в заданиях.
Таблица 1.1. Методы организации таблиц
идентификаторов
№ метода Способ разрешения коллизий
1
Простое рехэширование
2
Рехэширование с использованием псевдослучайных чисел
Ч"» y/'-'.v- ■,-,--------------------------------------------------------------- :------------------------------------------
**>**-*w^*~^—-........................................................................................................ -- -■■■ ■ '-■..
.. ■■■.J-.a^,-. ...
-.и| ' .
№ метода |
Способ разрешения
коллизий
Рехэширование
с помощью произведения Метод цепочек Простой список
Упорядоченный список Бинарное дерево
В табл. 1.2 даны варианты заданий на основе методов
организации таблиц идентификаторов,
перечисленных в табл. 1.1.
Таблица 1.2. Варианты заданий
Второй
метод организации таблицы |
№ варианта |
Первый
метод организации таблицы
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Пример
выполнения работы Задание для примера
В качестве примера
выполнения лабораторной работы возьмем сопоставление двух
методов: хэш-адресации с рехэшироваиием на основе псевдослучайных чисел
и комбинации хэш-адресации с бинарным деревом. Если обратиться к приведенной
выше табл. 1.1, то такой вариант задания будет соответствовать комбинации
методов 2 и 7 (в табл. 1.2 среди вариантов заданий
такая комбинация отсутствует).
Выбор и описание
хэш-функции
Для
хэш-адресации с рехэшированием в качестве хэш-функции возьмем функцию, которая
будет получать на входе строку, а в результате выдавать сумму кодов первого,
среднего и последнего элементов строки. Причем если строка содержит менее трех
символов, то один и тот же символ будет взят и в качестве первого, и в
качестве среднего, и в качестве последнего.
Будем считать, что прописные и строчные
буквы в идентификаторах различны1. В качестве кодов
символов возьмем коды таблицы ASCII, которая
используется в вычислительных
системах на базе ОС типа Microsoft Windows.
Тогда, если положить, что строка из области определения хэш-функции содержит
только цифры и буквы английского алфавита, то минимальным значением
хэш-функции будет сумма трех кодов цифры «О», а максимальным значением — сумма
трех кодов литеры «z».
Таким образом, область значений выбранной хэш-функции в терминах языка Object Pascal может быть описана как: (Ord(,0')+0rd(,0,)+0rd('0,))..(0rd(,z,)+0rd(,z')+0rd(,z'))
Диапазон области значений составляет 223 элемента, что
удовлетворяет требованиям'задания (не менее 200 элементов). Длина входных
идентификаторов в данном случае ничем не ограничена. Для удобства пользования
опишем две константы, задающие границы области значений хэш-функции:
HASHJ-1IN = Ord('0')+0rd('0')+0rdC'
0'); HASH^MAX = OrdCz'VordCz'HOrdCz').
Сама хэш-функция без учета рехэширования будет вычислять
следующее выражение:
Ord(sName[l]) + Ord(sName[(hength(sName)+l) div 2]) + Ord(sName[Length(sName)] здесь sName — это входная строка (аргумент
хэш-функции). Для рехэширования возьмем простейший генератор последовательности
псевдослучайных чисел, построенный на основе формулы F= г'Я, mod Я2,
где Я, и Я2 — простые числа, выбранные таким образом, чтобы Я, было
в диапазоне от Я2/2 до Я2. Причем, чтобы этот генератор
выдавал максимально длинную последовательность
во всем диапазоне от HASHMIN до HASH_MAX, Я2
должно быть максимально близко к величине Н ASH_MAX - Н ASH_M IN + 1. В данном случае диапазон содержит 223 элемента, и поскольку 223 — простое число,
то возьмем Я, = 223 (если бы размер диапазона не был простым числом, то в
качестве Я, нужно было бы взять ближайшее к нему меньшее простое число). В
качестве Я, возьмем 127: Я, = 127. Опишем соответствующие константы:
REHASH1 =
127; REHASH2 =
223;
' Программные модули,
реализующие таблицы символов, построены таким образом, что в зависимости от
условий компиляции они могут либо различать, либо не различать прописные и
строчные буквы. Условие компиляции реализовано через макрокоманды компилятора Delphi
5 в функции Upper в модуле TblElem (листинг П3.1,
приложение 3). О принципах, на основе которых выполняются макрокоманды и
условная компиляция, можно подробно узнать в [7, 13, 23, 25, 28, 32].
Тогда
хэш-функция с учетом рехэширования будет иметь следующий вид:
function VarHashtconst
sName;string; iNum:integer);longint:
begin Result:=(Ord(sName[l])+Ord(sName[(hength(sName)+l) div 2]) +
Ord(sName[tength(sName)]) - HASH_MIN + iNum*REHASHl mod REHASH2) mod
(HASH_MAX-HASH_MIN+1) + HASH_MIN; if Result <HASH_MIN then Result :=
HASHJIIN; end;
Входные параметры этой функции: sName — имя хэшируемого идентификатора, iNum— индекс рехэшированиея (если iNum = 0, то рехэширование отсутствует);. Строка проверки величины результата (Resul t < HASHMIN) добавлена, чтобы исключить
ошибки в тех случаях, когда на вход функции подается строка, содержащая символы
вне диапазона ' 0'..' z'
(поскольку контроль входных идентификаторов отсутствует, это имеет смысл).
Для
комбинации хэш-адресации и бинарного дерева можно использовать более простую
хэш-функцию — сумму кодов первого и среднего символов входной стро>,'. ки.
Диапазон значений такой хэш-функции в терминах языка Object Pascal будет выглядеть так:
(Ord(,0')+0rd(,0,))..(0rd('z,)+0rd('z,:>)
Этот
диапазон содержит менее 200 элементов, однако функция будет удовлетзо^;1 рять
требованиям задания, так как в комбинации с бинарным деревом она будет
обеспечивать обработку неограниченного количества идентификаторов (максимальное
количество идентификаторов будет ограничено только объемом доступ^.:' ной
оперативной памяти компьютера).
Без применения рехэширования эта хэш-функция будет выглядеть
значительно проще, чем описанная выше хэш-функция с учетом рехэширования:
function
VarHash(const sName: string): longint: begin
Result:=(Ord(sName[l])+Ord(sName[(hength(sName)+l) div
2]) - HASH_MIN) mod
(HASH_MAX-HASH_MIN+1) + HASH_MIN;
if Result < HASH_MIN then Result := HASH_MIN; end.
Описание
структур данных таблиц идентификаторов
В первую очередь
необходимо описать структуру данных, которая будет использована для хранения
информации об идентификаторах в таблицах идентификаторов. Для обеих таблиц (с
рехэшированием на основе генератора псевдослучайных чисел и в комбинации с
бинарным деревом) будем использовать одну и ту же структуру. В этом случае в
таблицах будут храниться неиспользуемые данные, но программный код будет
проще. В качестве учебного примера такой подход оправдан.
Структура
данных таблицы идентификаторов (назовем ее TVarlnfo) должна содержать в обязательном
порядке поле имени идентификатора (поле sName: string), а также поля дополнительной информации
об идентификаторе по усмотрению разработчиков компилятора. В
лабораторной работе не предусмотрено хранение какой-либо дополнительной
информации об идентификаторах, поэтому в качестве иллюстрации информационного
поля включим в структуру TVarlnfo
дополнительную информационную структуру TAddVarlnfo (поле plnfo:
TAddVarlnfo). Поскольку в языке Object Pascal для полей и
переменных, описанных как cl ass,
хранятся только ссылки на соответствующую структуру, такой подход не приведет
к значительным расходам памяти, но позволит в будущем хранить любую информацию,
связанную с идентификатором, в отдельной структуре данных (поскольку
предполагается использовать создаваемые программные модули в последующих
лабораторных работах). В данном случае другой подход невозможен, так как заранее не известно, какие данные необходимо будет
хранить в таблицах идентификаторов. Но разработчик реального компилятора, как
правило, знает, какую информацию требуется хранить, и может использовать другой
подход — непосредственно включить все необходимые поля в структуру данных
таблицы идентификаторов (в данном случае — в структуру TVarlnfo) без использования промежуточных
структур данных и ссылок.
Первый подход, реализованный в данном примере, обеспечивает
более экономное использование оперативной памяти, но является более сложным и
требует работы с динамическими структурами, второй подход более прост в
реализации, но менее экономно использует память. Какой из двух подходов
выбрать, решает разработчик компилятора в каждом конкретном случае (второй
подход будет проиллюстрирован позже в примере к лабораторной работе № 4). Для
работы со структурой данных TVarlnfo потребуются
следующие функции:
□
функции
создания структуры данных и освобождения занимаемой памяти — реализованы как constructor Create и destructor Destroy;
□
функции
доступа к дополнительной информации — в данной реализации это procedure Setlnfo и procedure Clearlnfo.
Эти функции будут общими для таблицы идентификаторов с
рехэшированием и для комбинированной таблицы Идентификаторов.
Однако
для комбинированной таблицы идентификаторов в структуру данных TVarlnfo потребуется также включить
дополнительные поля данных и функции, обеспечивающие организацию бинарного
дерева:
□ ссылки
на левую («меньшую») и правую («большую») ветвь дерева — реали
зованы как поля данных minEl,
maxEl: TVarlnfo;
U функции добавления элемента в дерево — function AddElCnt и function AddElem;
□
функции
поиска элемента в дереве — function FindElCnt
и function FindElem;
□
функция очистки информационных полей во всем дереве — procedure Cl earAl 1 Info;
а функция вывода
содержимого бинарного дерева в одну строку (для получения списка всех
идентификаторов) — function GetElList.
Функции поиска и размещения элемента в дереве реализованы в
двух экземплярах, так как одна из них выполняет подсчет количества сравнений, а
другая — нет. Поскольку на функции и процедуры не расходуется оперативная
память, в результате получилось, что при использовании одной и той же
структуры данных для разных таблиц идентификаторов в таблице с рехэшированием
будет расходоваться неиспользуемая память только на хранение двух лишних ссылок
(minEl и maxEl). Полностью вся структура данных TVarlnfo и связанные с ней процедуры и функции
описаны в программном модуле TblElem.
Полный текст этого программного модуля приведен в листинге П3.1 в приложении 3.
Надо обратить внимание на один важный момент в реализации
функции поиска идентификатора в дереве (function TVarlnfo.FindElCnt). Если выполнять сравнение двух строк
(в данном случае — имени искомого идентификатора 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
else
if i > 0 then
begin
end else
В
таком варианте дважды может быть выполнено только сравнение целого числа с
нулем, а сравнение строк всегда выполняется только один раз, что существенно
увеличивает эффективность процедуры поиска.
Организация таблиц
идентификаторов
Таблицы идентификаторов реализованы в виде статических
массивов размером HASHMIN.. HASH_MAX, элементами которых являются структуры
данных типа TVar Info. В
языке Object Pascal, как было сказано выше, для структур таких
типов хранятся ссылки. Поэтому для обозначения пустых ячеек в таблицах
идентификаторов будет использоваться пустая ссылка — nil.
Поскольку в памяти хранятся ссылки, описанные массивы будут
играть роль хэш-таблиц, ссылки из которых указывают непосредственно на
информацию в таблицах идентификаторов.
На рис. 1.3 показаны условные схемы, наглядно иллюстрирующие
организацию таблиц идентификаторов. Схема 1 иллюстрирует таблицу
идентификаторов с ре-хэшированием на основе генератора псевдослучайных чисел,
схема 2 — таблицу идентификаторов на основе комбинации хэш-адресации с бинарным
деревом. Ячейки с надписью «nil»
соответствуют незаполненным ячейкам хэш-таблицы.
Схема 1 |
Ai |
|
|
Ai |
nil |
•— |
Схема
2
Ak |
nil |
nil |
nil
Н2(А|) = Н2(Ак),Л>Ак; Н2(А)) = Н2(Л).А)>А| |
Поля ссылок в
элементах таблицы не используются, а потому не имеют значения
ЬЦ и Н2 -
соответствующие хэш-функции Рис. 1.3. Схемы
организации таблиц идентификаторов Для каждой таблицы идентификаторов реализованы
следующие функции:
- функции
начальной инициализации хэш-таблицы — InitTreeVar и InitHashVar;
- функции освобождения памяти хэш-таблицы
— ClearTreeVar и ClearHashVar;
-
функции удаления дополнительной информации в таблице — ClearTreelnfo и ClearHashlnfo;
- функции добавления элемента в таблицу
идентификаторов — AddTreeVar и Add-Ha$hVar;
- функции
поиска элемента в таблице идентификаторов — GetTreeVar и GetHashVar;
- функции, возвращающие количество выполненных
операций сравнения при
размещении или поиске элемента в таблице — GetTreeCount и GetHashCount.
Алгоритмы поиска и размещения идентификаторов для двух
данных методов организации таблиц были описаны выше в разделе «Краткие
теоретические сведения», поэтому приводить их здесь повторно нет смысла. Они
реализованы в виде четырех перечисленных
выше функций (AddTreeVar и AddHashVar — для размещения элемента; GetTreeVar
и GetHashVar — для
поиска элемента). Функции поиска и размещения элементов в таблице в качестве
результата возвращают ссылку на элемент таблицы (структура которого описана в
модуле TblElem) в случае
успешного выполнения и нулевую ссылку — в противном случае.
Надо
отметить, что функции размещения идентификатора в таблице организованы таким
образом, что если на момент помещения нового идентификатора в таблице уже есть
идентификатор с таким же именем, то функция не добавляет новый идентификатор в
таблицу, а возвращает в качестве результата ссылку на ранее помещенный в
таблицу идентификатор. Таким образом, в таблице не может быть двух и более
идентификаторов с одинаковым именем. При этом наличие одинаковых- идентификаторов во входном файле не воспринимается как
ошибка — это допустимо, так как в задании не предусмотрено ограничение на
наличие совпадающих имен идентификаторов.
Все перечисленные функции описаны в двух программных
модулях: FncHash
— для таблицы идентификаторов, построенной на оенбве рехэширования с использованием
генератора псевдослучайных чисел, и FncTree — для таблицы идентификаторов,
построенной на основе комбинации хэш-адресации и бинарного дерева. Кроме массивов
данных для организации таблиц идентификаторов и функций работы с н и-ми эти
модули содержат также описание переменных, используемых для подсчета количества
выполненных операций сравнения при размещении и поиске идентификатора в
таблицах.
Полные тексты обоих модулей (FncHash и FncTree) можно найти на веб-сайте издательства,
в файлах FncHash.pas и FncTree.pas. Кроме того, текст модуля FncTree приведен в листинге П3.2 в приложении 3.
Хочется обратить внимание на то, что в разделах инициализации
(initialization) обоих
модулей вызывается функция начального заполнения таблицы идентификаторов, а в
разделах завершения (final
ization) обоих
модулей — функция освобождения памяти. Это гарантирует корректную работу
модулей при любом порядке вызова остальных функций, поскольку Object Pascal сам обеспечивает своевременный вызов
программного кода в разделах инициализации и завершения модулей.
Текст
программы
Кроме
перечисленных выше модулей необходим еще модуль, обеспечивающий интерфейс с
пользователем. Этот модуль (FormLabl)
реализует графическое окно TLablForm
на основе класса TForm
библиотеки VCL.
Он обеспечивает интерфейс
средствами
Graphical User Interface (GUI) в ОС типа Windows на основе стандартных органов
управления из системных библиотек данной ОС. Кроме программного кода (файл FormLabl.pas) модуль включает в себя описание
ресурсов пользовательского интерфейса (файл FormLabl.dfm). Более подробно принципы организации
пользовательского интерфейса на основе GUI и работа систем программирования с
ресурсами интерфейса описаны в [3, 5, 6, 7]. Кроме описания интерфейсной формы
и ее органов управления модуль FormLabl содержит три переменные (iCountNum, iCountHash, iCountTree), служащие для накопления статистических результатов по мере выполнения размещения
и поиска идентификаторов в таблицах, а также функцию (procedure ViewStati stiс) для отображения накопленной
статистической информации на экране. Интерфейсная форма, описанная в модуле,
содержит следующие основные органы управления:
□ поле ввода имени файла (EditFi Ie), кнопка выбора имени файла из каталогов
файловой системы (BtnFile),
кнопка чтения файла (BtnLoad);
□ многострочное поле для отображения
прочитанного файла (Li
stldents);
□ поле ввода имени искомого идентификатора
(EditSearch);
□ кнопка для поиска введенного
идентификатора (BtnSearch)
— этой кнопкой однократно вызывается процедура поиска (procedure SearchStr);
□ кнопка автоматического поиска всех
идентификаторов (BtnAllSearch)
— этой кнопкой процедура поиска идентификатора (procedure SearchStr) вызывается циклически для всех
считанных из файла идентификаторов (для всех, перечисленных в поле Li stldents);
Q кнопка сброса накопленной статистической
информации (BtnReset);
Q поля для отображения статистической
информации;
□ кнопка завершения работы с программой (BtnExit).
Внешний вид этой формы приведен на рис.
1.4.
Функция чтения содержимого файла с
идентификаторами (procedure TLablForm. BtnLoadCl i ck) вызывается щелчком по кнопке BtnLoad. Она организована таким образом, что сначала содержимое файла
читается в многострочное поле Li
stldents, а затем все
прочитанные идентификаторы записываются в две таблицы идентификаторов. Каждая
строка файла считается отдельным идентификатором, пробелы в начале и в конце
строки игнорируются. При ошибке размещения идентификатора в одной из таблиц
выдается предупреждающее сообщение (например, еслг будет считано более 223
различных идентификаторов, то рехэширование станс невозможным и будет выдано
сообщение об ошибке).
Функция поиска идентификатора (procedure TLablForm.SearchStr) вызывается од нократно щелчком по кнопке BtnSearch (процедура procedure TLablForm. BtnSearchCl ick или многократно щелчком по кнопке BtnAll Search (процедура procedure TLablForm BtnAllSearchClick). Поиск идет сразу в двух таблицах, результаты
поиска и накоп ленная статистическая информация
отображаются в соответствующих полях. :; И сходные данные':;
stall
end
bbadsh
Еыорать Файл |
sdfasd
ZXCV
агрчзитьФаил s |
czxv
sdag
asdv
ж идентификатора-' |
asd
asdgfasdgter
afsdgfsdhhgjhjk
dsthghnxcvnxcv
В сего поиск: 1841
Найти все Копичестео выполнявшихся операций поиска
иденти
'им'.......... mm.V.iMn.i.r.iu; i.fil......................................................................... .......................
Бинарное
дерево-~ Сравнений
15 Всего
сравнений: 2 |
"Рехэширование ~....... -
Идентификатор найдег Сравнений: 13 Всего
сравнений: 184
В среднем сравнений: 1.02 В
средне
зыаОД из программы
Рис.
1.4. Внешний вид интерфейсной формы для лабораторной работы № 1
Полный
текст программного кода модуля интерфейса с
пользователем и описание ресурсов пользовательского интерфейса находятся в
архиве, располагающемся на веб-сайте издательства, в файлах FormLabl.pas и FormLabl.dfm соответственно.
Полный
текст всех программных модулей, реализующих
рассмотренный пример для лабораторной работы № 1, можно найти в архиве,
располагающемся на вебсайте, в
подкаталогах LABS и COMMON (в подкаталог COMMON вынесены те программные модули, исходный текст которых
не зависит от входного языка и задания по лабораторной работе). Главным файлом
проекта является файл LAB1
.DPR в подкаталоге LABS. Кроме того, текст модуля FncTree приведен в листинге П3.1 в приложении 3.
Выводы
по проделанной работе
В
результате выполнения написанного программного кода для ряда тестовых файлов
было установлено, что при заполнении таблицы идентификаторов до 20% (до 45
идентификаторов) для поиска и размещения идентификатора с использованием
рехэширования на основе генератора псевдослучайных чисел в среднем требуется
меньшее число сравнений, чем при использовании хэш-адресации в комбинации с
бинарным деревом. При заполнении таблицы от 20% до 40% (примерно 45-90
идентификаторов) оба метода имеют примерно равные показатели, но при заполнении
таблицы более, чем на 40% (90-223 идентификаторов),
эффективность комбинированного метода по сравнению с методом рехэширования
резко возрастает. Если на входе имеется более 223 идентификаторов,
рехэширование полностью перестает работать.
Таким образом, установлено, что комбинированный метод
работоспособен даже при наличии простейшей хэш-функции и дает неплохие
результаты (в среднем 3-5 сравнений на входных файлах, содержащих 500-700
идентификаторов), в то время как метод на основе рехэширования для реальной
работы требует более сложной хэш-функции с диапазоном значений в несколько тысяч или десятков тысяч.
Лабораторная
работа №2
Проектирование
лексического анализатора.
Цель
работы
Цель работы: изучение
основных понятий теории регулярных грамматик, ознакомление с назначением и
принципами работы лексических анализаторов (сканеров), получение практических
навыков построения сканера на примере заданного простейшего входного языка.
Краткие
теоретические сведения Назначение
лексического анализатора
Лексический анализатор (или сканер) — это часть компилятора, которая читает литеры
программы на исходном языке и строит из них слова (лексемы) исходного языка.
На вход лексического анализатора поступает текст исходной программы, а
выходная информация передается для дальнейшей обработки компилятором на этапе
синтаксического анализа и разбора.
Лексема (лексическая единица языка) — это
структурная единица языка, которая состоит из элементарных символов языка и не
содержит в своем составе других структурных единиц языка. Лексемами языков
программирования являются идентификаторы, константы, ключевые слова языка,
знаки операций и т. п. Состав возможных лексем каждого конкретного языка
программирования определяется синтаксисом этого языка.
С
теоретической точки зрения лексический анализатор не является обязательной,
необходимой частью компилятора. Его функции могут выполняться на этапе
синтаксического анализа. Однако существует несколько причин, исходя
из которых в состав практически всех компиляторов включают лексический
анализ. Это следующие причины:
□
упрощается
работа с текстом исходной программы на этапе синтаксического разбора и
сокращается объем обрабатываемой информации, так как лексический анализатор
структурирует поступающий на вход исходный текст программы
и удаляет всю незначащую информацию;
□
для
выделения в тексте и разбора лексем возможно применять
простую, эффективную и хорошо проработанную теоретически технику анализа, в то
время как на этапе синтаксического анализа конструкций исходного языка используются
достаточно сложные алгоритмы разбора;
□
лексический
анализатор отделяет сложный по конструкции синтаксический анализатор от работы
непосредственно с текстом исходной программы, структура которого может
варьироваться в зависимости от версии входного языка — при такой конструкции
компилятора при переходе от одной версии языка к другой достаточно только
перестроить относительно простой лексический анализатор.
Функции, выполняемые лексическим анализатором, и состав
лексем, которые он выделяет в тексте исходной программы, могут меняться в
зависимости от версии компилятора. В основном лексические анализаторы выполняют
исключение из текста исходной программы комментариев и незначащих пробелов, а
также выделение лексем следующих типов: идентификаторов, строковых, символьных
и числовых констант, знаков операций, разделителей и ключевых (служебных) слов
входного языка.
В большинстве компиляторов лексический и синтаксический анализаторы
— это взаимосвязанные части. Где провести границу между лексическим и синтаксическим
анализом, какие конструкции анализировать сканером, а какие — синтаксическим
распознавателем, решает разработчик компилятора. Как правило, любой анализ
стремятся выполнить на этапе лексического разбора входной программы, если он
может быть там выполнен. Возможности лексического анализатора ограничены по
сравнению с синтаксическим анализатором, так как в его основе лежат более
простые механизмы. Более подробно о роли лексического анализатора в
компиляторе и о его взаимодействии с синтаксическим анализатором можно узнать
в [1-4, 7].
Проблема определения
границ лексем
В
простейшем случае фазы лексического и синтаксического анализа могут выполняться
компилятором последовательно. Но для многих языков программирования информации
на этапе лексического анализа может быть недостаточно для однозначного
определения типа и границ очередной лексемы.
иллюстрацией такого случая может служить пример оператора
программы на языке Фортран, когда по части текста DO 10 1=1... невозможно определить тип оператора
(а соответственно, и границы лексем). В случае DO 10 1=1.15 это будет присвоение
вещественной переменной D010I значения константы 1.15 (пробелы з
Фортране игнорируются), а в случае DO 10 1=1,15 это цикл с перечислением от 1 до 15 по
целочисленной переменной I
до метки 10.
Другая иллюстрация из более современного
языка программирования C++ — оператор присваивания k=i+++++j;, который имеет только одну верную
интерпретацию (если операции разделить пробелами): k = i++ + ++j;.
Если
невозможно определить границы лексем, то лексический анализ исходного текста
должен выполняться поэтапно. Тогда лексический и синтаксический анализаторы
должны функционировать параллельно, поочередно обращаясь
друг к другу. Лексический анализатор, найдя очередную лексему, передает ее
синтаксическому анализатору, тот пытается выполнить анализ считанной части
исходной программы и может либо запросить у лексического анализатора следующую
лексему, либо потребовать от него вернуться на
несколько шагов назад и попробовать выделить лексемы
с другими границами. При этом он может сообщить информацию о том, какую
лексему следует ожидать. Более подробно такая схема взаимодействия лексического
и синтаксического анализаторов описана в [3, 7].
Параллельная
работа лексического и синтаксического анализаторов, очевидно, более сложна в
реализации, чем их последовательное выполнение. Кроме того, такой подход
требует больше вычислительных ресурсов и в общем случае большего времени на
анализ исходной программы, так как допускает возврат назад и повторный анализ
уже прочитанной части исходного кода. Тем не менее
сложность синтаксиса некоторых языков программирования требует именно такого
подхода — рассмотренный ранее пример программы на языке Фортран не может быть
проанализирован иначе.
Чтобы избежать параллельной работы лексического и
синтаксического анализаторов, разработчики компиляторов и языков
программирования часто идут на разумные ограничения синтаксиса входного языка.
Например, для языка С^ принято соглашение, что при
возникновении проблем с определением границ лек семы всегда выбирается лексема
максимально возможной длины. В рассмотренном выше примере для оператора k=i+++++j; это приведет к тому что при чтении
четвертого знака + из двух вариантов лексем (+ — знак сложение в C++, а ++ —
оператор инкремента) лексический анализатор выберет самую длин
ную — ++ (оператор инкремента) — и в целом весь оператор будет разобран на
k = i++ -н- +j; (знаки операций разделены пробелами),
что неверно, так как семантика языка C++ запрещает два оператора инкремента
подряд. Конечно, неверны] анализ операторов, аналогичных приведенному в примере
(желающие могут убедиться в этом на любом доступном компиляторе языка C++), —
незначительна, плата за увеличение эффективности работы компилятора и не
ограничивает возможности языка (тот же самый оператор может быть записан в виде
k=i++ + ++j что исключит любые неоднозначности в его
анализе). Однако таким же путем дл языка Фортран пойти нельзя — разница между
оператором присваивания и оператором цикла слишком велика, чтобы ею можно было
пренебречь.
В дальнейшем будем исходить из предположения, что все
лексемы могут быть однозначно выделены сканером на этапе лексического анализа.
Для всех современных языков программирования это действительно так, поскольку
их синтаксис разрабатывался с учетом возможностей компиляторов.
Таблица
лексем и содержащаяся в ней информация
Результатом работы лексического анализатора является
перечень всех найденных в тексте исходной программы лексем с учетом
характеристик каждой лексемы. Этот перечень лексем можно представить в виде
таблицы, называемой таблицей лексем. Каждой лексеме в таблице лексем
соответствует некий уникальный условный код, зависящий от типа лексемы, и
дополнительная служебная информация. Таблица лексем в каждой строке должна
содержать информацию о виде лексемы, ее типе и, возможно, значении. Обычно
структуры данных, служащие для организации такой таблицы, имеют два поля:
первое — тип лексемы, второе — указатель на информацию о лексеме.
Кроме того, информация о некоторых типах лексем, найденных в
исходной программе, должна помещаться в таблицу идентификаторов (или в одну из
таблиц идентификаторов, если компилятор предусматривает различные таблицы идентификаторов
для различных типов лексем).
ВНИМАНИЕ-------------------------------------------------------------------------------------------------------
Не следует путать таблицу лексем и таблицу
идентификаторов — это две принципиально разные таблицы, обрабатываемые
лексическим анализатором.
Таблица лексем фактически содержит весь текст исходной программы,
обработанный лексическим анализатором. В нее входят все возможные типы лексем,
кроме того, любая лексема может встречаться в ней любое количество раз. Таблица
идентификаторов содержит только определенные типы лексем — идентификаторы и
константы. В нее не попадают такие лексемы, как ключевые (служебные) слова
входного языка, знаки операций и разделители. Кроме того, каждая лексема
(идентификатор или константа) может встречаться в таблице идентификаторов
только один раз. Также можно отметить, что лексемы в таблице лексем обязательно
располагаются в том же порядке, что и в исходной программе (порядок лексем в
ней не меняется), а в таблице идентификаторов лексемы располагаются в любом
порядке так, чтобы обеспечить удобство поиска.
В
качестве примера можно рассмотреть некоторый фрагмент исходного кода на языке Object Pascal и соответствующую ему таблицу лексем,
представленную в табл. 2.1:
' begin
for i:=1 to N do fg := fg * 0.5
Таблица 2.1. Лексемы
фрагмента программы на языке Pascal
Лексема |
Тип лексемы |
|
Значение |
begin |
Ключевое слово |
|
х, |
for |
Ключевое слово |
|
Х2 |
i |
Идентификатор |
|
i :
1 |
■ |
Знак присваивания |
|
s, |
1 |
Целочисленная константа |
1 |
|
to |
Ключевое слово |
1 |
Х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,8,g(l,F), где:
Q.
— конечное множество состояний автомата;
£
— конечное множество допустимых входных символов (входной алфавит КА); 8 —
заданное отображение множества Q-E во множество подмножеств P(Q) 8: Q-E —> P(Q) (иногда 8 называют функцией переходов
автомата);
q0 e Q, — начальное состояние автомата;
F с Q — множество заключительных состояний
автомата.
Другим способом описания КА является граф переходов — графическое
представление множества состояний и функции переходов КА. Граф переходов КА —
это нагруженный однонаправленный граф, в котором вершины представляют состояния
КА, дуги отображают переходы из одного состояния в другое, а символы нагрузки
(пометки) дуг соответствуют функции перехода КА. Если функция перехода КА
предусматривает переход из состояния q в q 'по нескольким символам, то между ними
строится одна дуга, которая помечается всеми символами, по которым происходит
переход из q в q'.
Недетерминированный
КА неудобен для анализа цепочек, так как в нем могут встречаться состояния,
допускающие неоднозначность, то есть такие, из которых выходит две или более
дуги, помеченные одним и тем же символом. Очевидно, что программирование работы
такого КА — нетривиальная задача. Для простого программирования
функционирования КА M(Q,£,8//(),F) он должен быть детерминированным — в
каждом из возможных состояний этого КА для любого входного символа функция
перехода должна содержать не более одного состояния: ValV, VglQj либо b{a,q) = {г} г е Q,
либо b(a,q) = 0.
Доказано, что любой недетерминированный КА может быть
преобразован в детерминированный КА так, чтобы их языки совпадали [3, 7, 26]
(говорят, что эти К А эквивалентны).
Кроме преобразования в детерминированный КА любой КА может
быть минимизирован — для него может быть построен эквивалентный ему
детерминированный КАс минимально возможным количеством состояний. Алгоритмы
преобразования КА в детерминированный КА и минимизации
КА подробно описаны в [3, 7, 26].
Можно написать функцию, отражающую функционирование любого
детерминированного КА. Чтобы запрограммировать такую функцию, достаточно иметь
переменную, которая бы отображала текущее состояние КА, а переходы из одного
состояния в другое на основе символов входной цепочки могут быть построены с
помощью операторов выбора. Работа функции должна продолжаться до тех пор, пока
не будет достигнут конец входной цепочки. Для вычисления результата функции
необходимо по ее завершении проанализировать состояние КА. Если это одно из
конечных состояний, то функция выполнена успешно и входная цепочка принимается,
если нет, то входная цепочка не принадлежит заданному языку.
Однако в общем случае задача лексического анализатора шире,
чем просто проверка цепочки символов лексемы на соответствие ее входному
языку. Он должен правильно определить конец лексемы (об этом было сказано выше)
и выполнить те или иные действия по запоминанию распознанной лексемы (занесение
ее в таблицу лексем). Набор выполняемых действий определяется реализацией
компилятора. Обычно эти действия выполняются сразу же при обнаружении конца
распознаваемой лексемы.
Во входном тексте лексемы не ограничены специальными
символами. Определение границ лексем — это выделение тех строк в общем потоке
входных символов,
для которых надо выполнять
распознавание. Если границы лексем всегда определяются (а выше было принято
именно такое соглашение), то их можно определить по заданным терминальным
символам и по символам начала следующей лексемы. Терминальные символы — это
пробелы, знаки операций, символы комментариев, а также разделители (запятые,
точки с запятой и др.). Набор таких терминальных символов может варьироваться в
зависимости от входного языка. Важно отметить, что знаки операций сами также
являются лексемами и необходимо не пропустить их при распознавании текста.
Таким образом, алгоритм работы простейшего сканера можно описать так:
□
просматривается
входной поток символов программы на исходном языке до обнаружения очередного
символа, ограничивающего лексему;
□
для
выбранной части входного потока выполняется функция распознавания лексемы;
Q при успешном распознавании информация о
выделенной лексеме заносится в таблицу лексем, и алгоритм возвращается к
первому этапу;
Q при неуспешном распознавании выдается
сообщение об ошибке, а дальнейшие действия зависят от реализации сканера: либо
его выполнение прекращается, либо делается попытка распознать следующую лексему
(идет возврат к первому этапу алгоритма).
Работа программы-сканера продолжается до тех пор, пока не
будут просмотрены все символы программы на исходном языке из входного потока.
Требования
к выполнению работы Порядок выполнения
работы
Для выполнения лабораторной работы требуется написать
программу, которая выполняет лексический анализ входного текста в соответствии
с заданием и порождает таблицу лексем с указанием их типов и значений. Текст
на входном языке задается в виде символьного (текстового) файла. Программа
должна выдавать сообщения о наличии во входном тексте ошибок, которые могут
быть обнаружены на этапе лексического анализа.
Длину идентификаторов и строковых
констант можно считать ограниченной 32 символами. Программа должна допускать наличие комментариев неограниченной длины во входном файле. Форму
организации комментариев предлагается выбрать самостоятельно.
Лабораторная работа должна выполняться в
следующем порядке:
1.
Получить
вариант задания у преподавателя.
2.
Построить
описание КА, лежащего в основе лексического анализатора (в виде набора множеств
и функции переходов или в виде графа переходов).
Подготовить и
защитить отчет.
4.
Написать
и отладить программу на ЭВМ.
5.
Сдать
работающую программу преподавателю.
Требования
к оформлению отчета
Отчет
должен содержать следующие разделы:
Q Задание по
лабораторной работе.
□
Описание
КС-грамматики входного языка в форме Бэкуса—Наура.
□
Описание
алгоритма работы сканера или граф переходов КА для распознавания цепочек (в
соответствии с вариантом задания).
□
Текст программы (оформляется после выполнения программы на
ЭВМ).
□
Выводы
по проделанной работе.
Основные контрольные
вопросы
□
Что
такое трансляция, компиляция, транслятор, компилятор?
□
Из
каких процессов состоит компиляция? Расскажите об общей структуре компилятора.
□
Какую
роль выполняет лексический анализ в процессе компиляции?
□
Что
такое лексема? Расскажите, какие типы лексем существуют в языках программирования.
а Как могут быть связаны между собой
лексический и синтаксический анализ?
□ Какие
проблемы могут возникать при определении границ лексем в процессе
лексического анализа? Как решаются эти проблемы?
□ Что такое таблица лексем? Какая информация
хранится в таблице лексем?
Q В чем разница между таблицей лексем и
таблицей идентификаторов?
□
Что
такое грамматика? Дайте определения грамматики. Как выглядит описание
грамматики в форме Бэкуса—Наура.
□
Какие
классы грамматик существуют? Что такое регулярные грамматики?
□
Что
такое конечный автомат? Дайте определение детерминированного и недетерминированного
конечных автоматов.
□
Опишите
алгоритм преобразования недетерминированного конечного автомата в детерминированный.
□
Какие
проблемы необходимо решить при построении сканера на основе конечного
автомата?
□
Объясните
общий алгоритм функционирования лексического анализатора.
Варианты заданий
1. Входной язык
содержит арифметические выражения, разделенные символом ;
(точка с запятой). Арифметические выражения состоят, из идентификаторов,
десятичных чисел с плавающей точкой (в обычной и
логарифмической форме), знака присваивания (:=), знаков
операций +, -, *, / и круглых скобок.
2.
Входной
язык содержит логические выражения, разделенные символом; (точка с запятой).
Логические выражения состоят из идентификаторов, констант true и false, знака присваивания
(:=), знаков операций or,
not, and, not и круглых скобок.
3.
Входной
язык содержит операторы условия типа if ... then
... else и if... then,
разделенные
символом ; (точка с запятой). Операторы условия
содержат идентификаторы, знаки сравнения <, >, =, десятичные числа с
плавающей точкой (в обычной и логарифмической форме), знак присваивания (:=).
4.
Входной
язык содержит операторы цикла типа for (...; ...; ...) do, разделенные символом ; (точка с
запятой). Операторы цикла содержат идентификаторы, знаки сравнения <, >,
=, десятичные числа с плавающей точкой (в обычной и логарифмической форме),
знак присваивания (:-).
5.
Входной
язык содержит арифметические выражения, разделенные символом
; (точка с запятой). Арифметические выражения состоят из
идентификаторов, римских чисел, знака присваивания (:=), знаков
операций +, -, *, / и круглых скобок.
6.
Входной
язык содержит логические выражения, разделенные символом ;
(точка с запятой). Логические выражения состоят из идентификаторов, констант О и 1, знака присваивания (: = ),
знаков операций or,
not, and, not и круглых скобок.
7.
Входной
язык содержит операторы условия типа if... then
... else и if... then, разделенные символом
; (точка с запятой). Операторы условия содержат идентификаторы, знаки
сравнения <, >, =, римские числа, знак присваивания
(:=).
8.
Входной
язык содержит операторы цикла типа for (...; ...; ...) do, разделенные символом ; (точка с
запятой). Операторы цикла содержат идентификаторы, знаки сравнения <, >,
=, римские числа, знак присваивания (:=).
9.
Входной
язык содержит арифметические выражения, разделенные символом
; (точка с запятой). Арифметические выражения состоят из
идентификаторов, шестнадцатеричных чисел, знака присваивания
(:=), знаков операций +, -, *, / и круглых скобок.
10. Входной язык содержит логические
выражения, разделенные символом; (точка с запятой). Логические выражения
состоят из идентификаторов, шестнадцатеричных чисел, знака присваивания (:=), знаков операций or, not, and, not и круглых скобок.
11. Входной язык содержит операторы условия типа
if... then ... else и if... then,
разделенные символом; (точка с занято]!). Операторы условия содержат идентификаторы,
знаки сравнения <, >, =, шестнадцатеричные числа, знак присваивания (:=).
12. Входной язык содержит операторы никла
типа for (...; ...; ...) do, разделенные символом
; (точка с запятой). Операторы цикла содержат идентификаторы, знаки
сравнения <, >, =, шестнадцатеричные числа, знак присваивания (: = ).
13.
Входной
язык содержит арифметические выражения, разделенные символом
; (точка с запятой). Арифметические выражения состоят из
идентификаторов, символьных констант (один символ в одинарных кавычках), знака
присваивания (:=), знаков операций +, -, *, / и
круглых скобок.
14.
Входной
язык содержит логические выражения, разделенные символом; (точка с запятой).
Логические выражения состоят из идентификаторов, символьных констант Т и 'F', знака присваивания
(: = ), знаков операций or,
xor, and, not и круглых скобок.
15.
Входной
язык содержит операторы условия типа if... then
... else и if... then, разделенные символом
; (точка с запятой). Операторы условия содержат идентификаторы, знаки
сравнения <, >, =, строковые константы (последовательность символов в
двойных кавычках), знак присваивания (: = ).
16.
Входной
язык содержит операторы цикла типа for (...; ...; ...) do, разделенные символом ; (точка с
запятой). Операторы цикла содержат идентификаторы, знаки сравнения <, >,
=, строковые константы (последовательность символов в двойных кавычках), знак
присваивания (: = ).
ПРИМЕЧАНИЕ------------------------------------------------------------------------------------------------------
■
Римскими
числами считать последовательности заглавных латинских букв X, V и I;
■
шестнадцатеричными
числами считать последовательность цифр и символов «а», «Ь», «с», «d, «e» n«f»,
начинающуюся с цифры (например: 89, 45ас9, 0abc4);
■
задание
по лабораторной работе № 2 взаимосвязано с заданием по лабораторной работе №
3, для уточнения состава входного языка можно посмотреть грамматику, заданную в
работе № 3 по соответствующему варианту.
Пример
выполнения работы Задание для примера
В качестве задания для примера возьмем входной язык, который
содержит набор условных операторов условия типа if... then ... else и if... then, разделенных символом
; (точка с запятой). Эти операторы в качестве условия содержат
логические выражения, построенные с помощью операций or, xor и and, операндами которых являются идентификаторы и целые
десятичные константы без знака. В исполнительной части эти операторы содержат
или оператор присваивания переменной логического выражения
(:=), или другой условный оператор. Комментарий будет организован в виде
последовательности символов, начинающейся с открывающей фигурной скобки ({) и заканчивающейся закрывающей фигурной скобкой (}).
Комментарий может содержать любые алфавитно-цифровые символы, в том числе и
символы национальных алфавитов.
Грамматика входного
языка
Описанный
выше входной язык может быть построен с помощью КС-грамматики
G({if,then,else,a,:=,or, not, and,(,),;},{5,J-'',£',L),C},P,5) с правилами Р:
5->F;
F->if fthen Telse F| if E then F|
a := E
T —> if E then Telse T|
a := E
E->EorD\ExorD\D
D -> D and С | С
C~>a\ (£)
Описание
грамматики построено в форме Бэкуса—Наура. Жирным шрифтом в грамматике и в
правилах выделены терминальные символы.
Выбранный в качестве примера язык и задающая его грамматика
не совпадают ни с одним из предложенных выше вариантов. С другой стороны, на
этом примере можно проиллюстрировать многие особенности построения
лексического, а впоследствии — и синтаксического распознавателя, присущие
различным вариантам. Он содержит как условные операторы, связанные с передачей
управления в то или иное место исходной программы, так и линейные операции в
форме вычисления логических выражений. Поэтому данный пример выбран в качестве
иллюстрации для лабораторной работы № 2, а позже будет использоваться также в
лабораторных работах № 3 и 4.
Описание конечного
автомата
для распознавания лексем
входного языка
Задача лексического анализатора для описанного выше языка
заключается в том, чтобы распознавать и выделять в исходном тексте программы
все лексемы этого языка. Лексемами данного языка являются:
- шесть ключевых слов языка (if, then, else, or, xor и and);
- разделители: открывающая и закрывающая
круглые скобки, точка с запятой;
- знак операции присваивания;
- идентификаторы;
- целые десятичные константы без знака.
Кроме перечисленных лексем распознаватель должен уметь
определять и исключать из входного текста комментарии, принцип построения
которых описан выше. Для выделения комментариев ключевыми символами должны быть
открывающая и закрывающая фигурные скобки.
Для
перечисленных типов лексем и комментария можно построить регулярную грамматику,
а затем на ее основе создать КА. Однако построенная таким образом грамматика, с
одной стороны, будет элементарно простой, с другой стороны — громоздкой и
малоинформативной. Поэтому можно пойти путем построения КА непосредственно по
описанию лексем. Для этого не хватает только описания идентификаторов и целых
десятичных констант без знака:
- идентификатор — это произвольная
последовательность малых и прописных
букв латинского алфавита (от А до Z и от а до г), цифр (от 0 до 9) и знака
подчеркивания (_), начинающаяся с буквы или со знака
подчеркивания;
-
целое десятичное число без знака — это произвольная последовательность
цифр
(от 0 до 9), начинающаяся
с любой цифры. Границами лексем для данного распознавателя будут служить
пробел, знак табуляции, знаки перевода строки и возврата каретки, а также
круглые скобки, открывающая фигурная скобка, точка с запятой и знак двоеточия.
При этом следует помнить, что круглые скобки и точка с запятой сами по себе
являются лексемами, открывающая фигурная скобка начинает комментарий, а знак
двоеточия, являясь границей лексемы, в то же время является и началом другой
лексемы — операции присваивания.
В данном языке лексический анализатор всегда может
однозначно определить границы лексемы, поэтому нет необходимости в его
взаимодействии с синтаксическим анализатором и другими элементами компилятора.
П
(.): IF (а)
Рис. 2.1. Фрагмент графа
переходов КА для распознавания всех лексем, кроме ключевых слов
Полный граф переходов КА будет очень громоздким и неудобным
для просмотра, поэтому проиллюстрируем его несколькими фрагментами. На рис.
2.1 изображен фрагмент графа переходов КА, отвечающий за распознавание
разделителей, комментариев, знака присваивания,
переменных и констант (всех лексем входного языка, кроме ключевых слов).
На рис. 2.2 изображен фрагмент графа переходов КА,
отвечающий за распознавание ключевых слов if и then (этот фрагмент имеет ссылки на
состояния, изображенные на рис. 2.1). Аналогичные фрагменты можно построить и
для других ключевых слов.
Рис. 2.2. Фрагмент графа
переходов К А для ключевых слов if и then
На
фрагментах графа переходов КА, изображенных на рис. 2.1 и 2.2, приняты следующие
обозначения: *
□
А — любой алфавитно-цифровой символ;
□
А(*) — любой алфавитно-цифровой символ, кроме
перечисленных в скобках;
□ П — любой незначащий
символ (пробел, знак табуляции, перевод строки, возврат каретки);
□ Б — любая буква английского алфавита (прописная или
строчная) или символ подчеркивания(_);
□
Б(*) — любая буква английского алфавита (прописная или
строчная) или символ подчеркивания (_), кроме перечисленных в скобках;
□
Ц — любая цифра от 0 до 9;
□
F — функция обработки таблицы лексем,
вызываемая при переходе КА из одного состояния в другое. Обозначения ее аргументов:
■
v
— переменная,
запомненная при работе КА; ш d — константа, запомненная при работе КА;
■
а
— текущий входной символ
КА.
С учетом этих обозначений, полностью КА
можно описать следующим образом:
M(Q,£,8,<
Q= {Н, С, G, V, D, II, 12, Т1, Т2, ТЗ, Т4, Е1, Е2, ЕЗ, Е4, 01,
02, XI, Х2, ХЗ, Al, A2,
A3, J}
£ = А (все допустимые
алфавитно-цифровые символы);
<?о= н;
F = {F).
Функция переходов (8) для этого КА
приведена в приложении 2.
Из начального состояния КА литеры «i», «t», «e», «о», «х» и «а» ведут в начало
цепочек состояний, каждая из которых
соответствует ключевому слову:
□ состояния II, 12 — ключевому слову if;
□ состояния Т1, Т2,
ТЗ, Т4 — ключевому слову then;
□ состояния Е1,
Е2, ЕЗ, Е4 — ключевому слову else;
□
состояния
01, 02 — ключевому слову or;
□
состояния
XI, Х2, ХЗ — ключевому слову хог;
□
состояния
Al, A2, A3 — ключевому слову and.
Остальные литеры ведут к состоянию, соответствующему
переменной (идентификатору), — V. Если в какой-то из цепочек встречается литера, не
соответствующая ключевому слову, или цифра, то К А
также переходит в состояние V,
а если встречается граница лексемы — запоминает уже прочитанную часть ключевого
слова как переменную (чтобы правильно выделять такие идентификаторы, как «1»
или «els», которые
совпадают с началом ключевых слов). Цифры ведут в состояние, соответствующее
входной константе, — D.
Открывающая фигурная скобка ведет в состояние С,
которое соответствует обнаружению комментария — из этого состояния КА выходит,
только если получит на вход закрывающую фигурную скобку. Еще одно состояние — G — соответствует лексеме «знак
присваивания». В него КА переходит, получив на вход двоеточие, и ожидает в этом
состоянии символа «равенство».
Состояние
Н — начальное состояние КА, а состояние F — его конечное состояние. Поскольку КА работает с непрерывным потоком лексем, перейдя в
конечное состояние, он тут же должен возвращаться в начальное, чтобы
распознавать очередную лексему. Поэтому в моделирующей программе эти
два состояния можно объединить.
На
графе и при описании функции переходов не обозначено состояние «ошибка», чтобы
не загромождать и без того сложный граф и функцию. В это состояние КА переходит
всегда, когда получает на вход символ, по которому нет переходов из текущего
состояния.
Функция
F, которой помечены дуги КА на графе и
переходы в функции переходов, соответствует выполнению записи данных в таблицу
лексем. Аргументы функции зависят от текущего состояния КА. В реализации
программы, моделирующей функционирование КА, этой функции должны
соответствовать несколько функций, вызываемые в зависимости от текущего
состояния и входного символа.
Надо
отметить, что для корректной записи переменных и констант в таблицу лексем КА
должен запоминать соответствующие им цепочки символов. Проще всего это делать,
запоминая позицию считывающей головки КА всякий раз, когда ои находится в
состоянии Н.
Можно
заметить, что функция переходов КА получилась довольно громоздкой, хотя и
простой по своей сути (для всех ключевых слов она работает однотипно). В
реализации функционирования КА проще было бы не выделять отдельные состояния
для ключевых слов, а переходить всегда по обнаружению буквы на входе КА в
состояние V.
Тогда проверку того, является ли считанная строка ключевым словом или же
идентификатором, можно было бы выполнять на момент ее записи в таблицу лексем с
помощью стандартных операций сравнения строк. Граф переходов КА в таком
варианте был бы намного компактнее — он выглядел бы точно так же, как фрагмент,
представленный на рис. 2.1. Его можно назвать «сокращенным» графом переходов К А (или «сокращенным КА»).
Но следует отметить, что, несмотря на большую наглядность и
простоту реализации, сокращенный КА будет менее эффективным, поскольку в
момент записи лексемы в таблицу он должен будет выполнять ее сравнение со
всеми известными ключевыми словами (в данном случае надо определять шесть
ключевых слов — следовательно, будет выполняться шесть сравнений строк). То
есть такой КА будет повторно просматривать уже прочитанную часть входной
цепочки, да еще и несколько раз! И хотя в явном виде в реализации сокращенного
КА эта операция не присутствует, она все равно будет выполняться в вызове
библиотечной функции сравнения строк.
Итак,
хотя сокращенный КА меньше по количеству состояний и проще в реализации, он
является менее эффективным, чем полный КА, построенный на анализе всех входных
лексем. Тем не менее оба варианта реализации КА
обеспечивают построение требуемого лексического анализатора. Какой из них
выбрать, решает разработчик компилятора.
Реализация
лексического анализатора
Разбиение
на модули
Модули, реализующие лексический
анализатор, разделены на две группы:
□
модули,
программный код которых не зависит от входного языка;
модули,
программный код которых зависит от входного языка.
В первую группу входят модули:
□
LexEl em — описывает структуру данных элемента
таблицы лексем;
□ Forml_ab2 — описывает интерфейс с пользователем.
Во вторую группу входят модули:
□ LexType - описывает типы входных лексем,
связанные с ними наименования и текстовую информацию;
□ LexAuto
— реализует функционирование
КА.
Такое разбиение на модули позволяет использовать те же самые
структуры данных для организации лексического распознавателя при изменении
входного языка. Кроме этих модулей для реализации лабораторной работы № 2
используются также программные модули (TblElem и FncTree), позволяющие работать с комбинированной
таблицей идентификаторов, которые были созданы при выполнении лабораторной
работы № 1. Эти два модуля, очевидно, также не зависят от входного языка.
Кратко опишем содержание программных модулей, используемых
для организации лексического анализатора.
Модуль типов лексем
Модуль LexType
в детальных комментариях не нуждается. В нем перечислены все допустимые типы
лексем (тип данных TLexType),
каждой из которых соответствует наименование и обозначение лексемы. Вывод
наименований лексем обеспечивает функция LexTypeName, а вывод обозначений — функция LexTypelnfo. Следует отметить,
что кроме перечисленных в задании лексем используется еще одна дополнительная
информационная лексема (LEXSTART),
обозначающая конец строки. Модуль LexEl em описывает структуры данных элемента
таблицы лексем (TLexem)
и самой таблицы лексем (TLexList),
а также все, что с ними связано.
Модуль структур данных
таблицы идентификаторов
Структура данных таблицы лексем содержит информацию о
лексеме (поле Lexlnfo).
В этом поле содержится тип лексемы (LexType), а также следующие данные:
□ Varlnfo — ссылку на элемент таблицы
идентификаторов для лексем типа «переменная»; Q ConstVal — целочисленное значение для лексем типа
«константа»; □ szlnfo — произвольная строка для информационной
лексемы. Для лексем других типов не требуется никакой дополнительной
информации. Следует отметить, что для лексем типа «переменная» хранится именно
ссылка на таблицу идентификаторов, а не имя переменной. Именно для этого в
данной лабораторной работе используются модули из лабораторной работы № 1. Для
самого лексического анализатора не имеет значения, что хранить в таблице
лексем — ссылку на таблицу идентификаторов со всей информацией о переменной или
же
только
имя переменной. Но реализация лексического анализатора, при которой хранится
именно ссылка на таблицу идентификаторов, чрезвычайно удобна для дальнейшей
обработки данных, что будет очевидно в последующих работах (лабораторных
работах № 3 и № 4). Поскольку лексический анализатор интересен не сам по себе,
а в составе компилятора, такой подход принципиально важен.
Кроме
этого в структуре данных элемента таблицы лексем хранится информация' о позиции
лексемы в тексте входной программы:
О iStr — номер строки, где встретилась лексема;
a iPos — позиция лексемы в строке;
а iA11Р — позиция лексемы относительно начала входного файла.
Эта
информация будет полезна, в частности, при информировании пользователя об
ошибках.
Кроме
этих данных структура содержит также:
Q четыре конструктора для создания лексем
четырех разных типов:
■
CreateVar
— для создания лексем типа «переменная»;
■
CreateConst
— для создания лексем типа «константа»;
■
Createlnfo
— для создания информационных лексем;
■
CreateKey
— для создания лексем других типов;
□
деструктор
Destroy для
освобождения памяти, занятой лексемой (важен для информационных лексем);
□ свойства и функции для доступа к
информации о лексеме.
Хранить в структуре строку самой лексемы нет никакой
необходимости (для переменных строка хранится в таблице идентификаторов, для
других типов лексем она просто не нужна).
Сама
таблица лексем (тип данных TLexList)
построена на основе динамического массива TList из библиотеки VCL (модуль Classes) системы программирования Delphi 5.
Динамический массив типа TList обеспечивает все функции и данные,
необходимые для хранения в памяти произвольного количества лексем
(максимальное количество лексем ограничено только объемом доступной
оперативной памяти). Для таблицы лексем TLexList дополнительно реализованы функции
очистки таблицы, которые освобождают память, занятую лексемами, при их удалении
из таблицы (функция Clear и деструктор Destroy), а также функция GetLexem и свойство Lexem, обеспечивающие удобный доступ к любой
лексеме в таблице по се индексу (порядковому номеру).
Модуль моделирования
работы КА
Модуль
LexAuto, моделирующий
работу КА, на основе которого построен лексический распознаватель, — самый
значительный по объему программного кода. Однако по содержанию программного
кода он предельно прост. Этот модуль обеспечивает функционирование полного КА,
фрагменты графа переходов которого были изображены на рис. 2.1 и 2.2, а функция
переходов была построена выше. Главной составляющей этого программного модуля
является функция Маке-LexList, которая непосредственно моделирует
работу КА. На вход функции подается входная программа в виде списка строк
(формальный параметр 1istFile) и таблица лексем, куда должны помещаться
найденные лексемы (формальный параметр listLex). Результатом работы функции является 0,
если лексический анализ выполнен без ошибок, а если ошибка обнаружена — номер
строки в исходном файле, в которой она присутствует. Для более подробной
информации об обнаруженной ошибке функция создает информационную лексему и помещает
ее в конец таблицы лексем. Сама информационная лексема кроме текстовой
информации об ошибке содержит еще дополнительную информацию о
ее местонахождении в исходной программе (смещение от начала файла и
длина ошибочной лексемы).
В типе данных TAutoPos перечислены все возможные состояния КА.
Перечень состояний полностью соответствует функции переходов КА.
Реализация функции MakeLexLi st, несмотря на большой объем программного
кода, предельно проста. Она построена на основе двух вложенных циклов (первый —
по строкам входного списка, второй — по символам в текущей.строке),
внутри которых находятся два уровня вложенных оператора выбора типа case — типичный подход к моделированию
функционирования КА. Внешний оператор case выполняется по всем возможным состояниям автомата, a case второго уровня — по допустимым входным
символам в каждом состоянии.
Можно обратить внимание на шесть
вспомогательных функций:
□ AddVarToList — добавление лексемы типа «переменная» в
таблицу лексем;
Q AddVarKeyToLi st — добавление лексем типа «переменная» и
типа «разделитель» в таблицу лексем;
□ AddConstToList — добавление лексемы типа «константа» в
таблицу лексем;
□
AddConstKeyToLi st — добавление лексем типа «константа» и типа
«разделитель» в таблицу
лексем;
□
AddKeyToLi st
— добавление лексемы типа «ключевое слово» или «разделитель» в таблицу лексем;
□
Add2KeysToList — добавление лексем типа «ключевое слово»
и «разделитель» в таблицу лексем подряд.
Эти функции, по сути, являются реализацией функции, которая
на графе переходов КА была обозначена F.
Еще две вспомогательные функции служат для упрощения кода.
Они выполняют часто повторяющиеся действия в состояниях автомата, которые
связаны со средними символами ключевых слов (в функции переходов эти состояния
обозначены Т2, ТЗ, Е2, ЕЗ, Х2 и А2) и завершающими
символами ключевых слов (в функции переходов эти состояния обозначены 12, Т4,
Е4, 02, ХЗ и A3).
Построенный лексический анализатор
обнаруживает три типа ошибок:
□
неверный
символ в лексеме (например, сочетания «2а» или «:6» будут признаны неверными
символами в лексемах);
□
незакрытый
комментарий (присутствует открывающая фигурная скобка, но отсутствует соответствующая
ей закрывающая);
а незавершенная лексема (в данном
входном' языке это может быть только символ «:» в
конце входной программы, который будет воспринят как начало незавершенной
лексемы «:=»).
Остальные ошибки входного языка должен обнаруживать
синтаксический анализатор.
В качестве еще одной особенности реализации можно отметить,
что переход с одной строки входного списка на другую должен восприниматься как
граница текущей лексемы, так как одна лексема не может быть разбита на две
строки — именно это и реализовано в конце цикла по символам текущей строки.
Текст программы распознавателя
Кроме
перечисленных выше модулей необходим еще модуль, обеспечивающий интерфейс с
пользователем. Как и в лабораторной работе № 1, этот модуль (Form-Lab2) реализует графическое окно TLab2Form на основе класса TForm библиотеки VCL и включает в себя две составляющие:.
□
файл
программного кода (файл FormLab2.pas);
□
файл
описания ресурсов пользовательского интерфейса (файл FormLab2.dfm).
Кроме описания интерфейсной формы и ее органов управления
модуль FormLab2 содержит
переменную (1 i
stLex), в которую
записывается ссылка на таблицу лексем.
Интерфейсная
форма, описанная в модуле, содержит следующие основные органы управления:
□
многостраничную
вкладку (PageControll)
с двумя закладками (SheetFile
и Sheetlexems) под
названиями «Исходный файл» и «Таблица лексем» соответственно;
□
на закладке SheetFi 1 е:
■
поле
ввода имени файла (EditFile),
кнопка выбора имени файла из каталогов файловой системы (BtnFile), кнопка чтения файла (BtnLoad);
■
многострочное
поле для отображения прочитанного файла (Li stldents);
□ на закладке SheetLexems:
■ сетка
(GridLex) с тремя
колонками для отображения данных о прочитанных
лексемах;
□ кнопка
завершения работы с программой (BtnExit).
Лабораторная
работа h»2
Исходный
Файл .Табода лексем.
:
сводные данные
закладки интерфейсной формы для
лабораторной работы № 2
h?j |
Лабораторная работа тг
Таблша
лек£вм 1
1 j Ключевое слово
2 i Переменная
3 I Ключевое слово
4 |
; Константа |
22 |
5 |
: Ключевое слово |
ithen |
6
7 |
\ Ключевое слово |
• s* |
j Переменная |
ia |
|
|
I Ключевое
слово |
;and |
10
11 |
;
Ключевое слово Переменная |
■then |
JJJ |
Рис. 2.4. Внешний вид второй закладки
интерфейсной формы для лабораторной работы № 2
Чтение содержимого входного файла организовано точно так же,
как в лабораторной работе № 1.
После чтения файла создается таблица лексем (ссылка на нее
запоминается в переменной 1 istLex)
и вызывается функция MakeLexList,
результат работы которой помещается во временную переменную пЕгг.
Если
обнаружена ошибка, пользователю выдается сообщение об этом и указатель в списке
строк позиционируется на место, где обнаружена ошибка.
Если ошибок не обнаружено, то на основании считанной таблицы
лексем 1 istLex
заполняется сетка GridLex,
которая очень удобна для наглядного представления таблицы лексем:
□
первая
колонка — порядковый помер лексемы;
□
вторая
колонка — тип лексемы (ее внешний вид); Q
третья колонка — информация о лексеме.
Полный
текст программного кода модуля интерфейса с пользователем приведен в листинге П2.4 в приложении 2, а описание ресурсов пользовательского
интерфейса — в листинге П2.5 в приложении 2.
Полный текст всех программных
модулей, реализующих рассмотренный пример для лабораторной работы № 2, приведен
в приложении 2.
Выводы
по проделанной работе
В
результате лабораторной работы № 2 построен лексический анализатор на основе
конечного автомата. Построенный лексический анализатор позволяет выделять в
тексте исходной программы лексемы следующих типов:
Q ключевые слова (if, then, else,
or, xor и and);
□
идентификаторы
(при этом в именах идентификаторов различаются строчные и прописные английские
буквы);
□
знак
операции присваивания;
□
целые
десятичные константы без знака;
□
разделители
(круглые скобки и точка с запятой).
Лексический анализатор игнорирует в тексте входной программы
пробелы, знаки табуляции и переводы строки, а также комментарии, выделенные
фигурными скобками.
В
случае обнаружения неверной лексемы (например числа,
содержащего букву), незакрытого комментария или незавершенной лексемы (такой
лексемой может быть только символ «:») лексический анализатор выдает сообщение
об ошибке и прекращает дальнейший анализ. При наличии нескольких неверных
лексем анализатор обнаруживает только первую из них.
Результатом
выполнения лексического анализа является структура данных, которая
представляет таблицу лексем. Построенный лексический анализатор предназначен
для подготовки данных, необходимых для выполнения следующих лабораторных
работ, связанных с синтаксическим анализом и генерацией кода.
ЛАБОРАТОРНАЯ
РАБОТА № 3
Построение простейшего дерева
вывода
Цель
работы
Цель
работы изучение основных
понятий теории грамматик простого и опера-?„Z™—*, ознакомление с алгоритмами «*™£^_ за (разбора) для
некоторых классов КС-грамматик, получение практических на Z^fcZlZ простейшего синтаксического анализатора
для заданной грамматики операторного предшествования.
Краткие теоретические сведения Назначение
синтаксического анализатора
По
иерархии грамматик Хомского выделяют четыре основные JW™™»» (и описывающих их грамматик) [1, 3,
4, 7]. При этом наибольший интерес представляют регулярные и
контекстно-свободные (КС) грамматики и ™^Ohh используются
при описании синтаксиса языков программирования. С помощью регулярных грамматик
можно описать лексемы языка - идентификаторы, константы служебные слова и
прочие. На основе КС-грамматик строятся более крупные синтаксические
конструкции: описания типов и переменных, арифметические и логические
выражения, управляющие операторы и, наконец, полностью вся программа на входном
языке.
Входные цепочки регулярных языков распознаются с помощью
конечных автоматов (КА). Они лежат в основе сканеров, выполняющих лексический
анализ и выделение слов в тексте программы на входном языке. Результатом работы
сканера является преобразование исходной программы в список или таблицу лексем.
Дальнейшую ее обработку выполняет другая часть
компилятора — синтаксический анализатор. Его работа основана на использовании
правил КС-грамматики, описывающих конструкции исходного языка.
Синтаксический анализатор (синтаксический разборщик) — это часть
компилятора, которая отвечает за выявление и проверку синтаксических
конструкций входного языка. В задачу синтаксического анализатора входит:
□
найти и выделить синтаксические конструкции в тексте исходной программы;
□ установить тип и проверить правильность
каждой синтаксической конструкции;
□ представить
синтаксические конструкции в виде, удобном для дальнейшей генерации текста
результирующей программы.-
Синтаксический анализатор — это основная часть компилятора
на этапе анализа. Без выполнения синтаксического разбора работа компилятора
бессмысленна, в то время как лексический разбор, в принципе, не является
обязательной фазой компиляции. Все задачи по проверке синтаксиса входного
языка могут быть решены на этапе синтаксического разбора. Лексический
анализатор только позволяет избавить сложный по структуре синтаксический
анализатор от решения примитивных задач по выявлению и запоминанию лексем
исходной программы.
Выходом лексического анализатора является таблица лексем.
Эта таблица образует вход синтаксического анализатора, который исследует
только один компонент каждой лексемы — ее тип. Остальная информация о лексемах
используется на более поздних фазах компиляции при семантическом анализе,
подготовке к генерации и генерации кода результирующей программы.
Синтаксический анализатор воспринимает выход лексического
анализатора и разбирает его в соответствии с грамматикой входного языка.
Однако в грамматике входного языка программирования обычно не уточняется, какие
конструкции следует считать лексемами. Примерами конструкций, которые обычно
распознаются во время лексического анализа, служат ключевые слова, константы и
идентификаторы. Но эти же конструкции могут распознаваться и синтаксическим
анализатором. На практике не существует жесткого правила, определяющего, какие
конструкции должны распознаваться на лексическом уровне, а какие надо оставлять
синтаксическому анализатору. Обычно это определяет разработчик компилятора
исходя из технологических аспектов программирования, а также синтаксиса и
семантики входного языка. Принципы взаимодействия лексического и синтаксического
анализаторов были рассмотрены в лабораторной работе № 2.
В
основе синтаксического анализатора лежит распознаватель текста исходной
программы, построенный на основе грамматики входного языка. Как правило,
синтаксические конструкции языков программирования могут быть описаны с помощью
КС-грамматик; реже встречаются языки, которые могут быть описаны с помощью
регулярных грамматик.
Главную роль в том,
как функционирует синтаксический анализатор и какой алгоритм
лежит в его основе, играют принципы построения распознавателей для КС-языков.
Без применения этих принципов невозможно выполнить эффективный синтаксический
разбор предложении входного языка.
Проблема
распознавания цепочек КС-языков
Взаимодействие лексического
и синтаксического анализаторов рассматривалось в предыдущей
лабораторной работе, здесь же будут рассмотрены алгоритмы, лежащие
в основе синтаксического анализа. Перед синтаксическим анализатором стоят
две основные задачи: проверить правильность конструкций программы, которая
представляется в виде уже выделенных слов входного языка, и преобразовать
ее в вид, удобный для дальнейшей семантической (смысловой) обработки и
генерации кода. Одним из способов такого представления является дерево синтаксического разбора.
Основой для
построения распознавателей КС-языков являются автоматы с магазинной
памятью — МП-автоматы — односторонние недетерминированные распознаватели
с линейно-ограниченной магазинной памятью (полная классификация распознавателей
приведена в [1, 4, 3, 7]). Поэтому важно рассмотреть, как функционирует
МП-автомат и как для КС-языков решается задача разбора — построение
распознавателя языка на основе заданной грамматики. Далее рассмотрены технические
аспекты, связанные с реализацией синтаксических анализаторов.
МП-автомат в
отличие от обычного КА имеет стек (магазин), в который можно помещать
специальные «магазинные» символы (обычно это терминальные и нетерминальные
символы грамматики языка). Переход МП-автомата из одного состояния в другое
зависит не только от входного символа, но и от одного или нескольких верхних
символов стека. Таким образом, конфигурация автомата определяется тремя
параметрами: состоянием автомата, текущим символом входной цепочки
(положением указателя в цепочке) и содержимым стека.
При выполнении
перехода МП-автомата из одной конфигурации в другую из стека удаляются
верхние символы, соответствующие условию перехода, и добавляется цепочка,
соответствующая правилу перехода. Первый символ цепочки становится
верхушкой стека. Допускаются переходы, при которых входной символ игнорируется
(и тем самым он будет входным символом при следующем переходе). Эти
переходы называются ^.-переходами. Если при окончании цепочки автомат находится
в одном из заданных конечных состояний, а стек пуст, цепочка считается
принятой (после окончания цепочки могут быть сделаны А-переходы).
Иначе
цепочка символов не принимается.
МП-автомат называется недетерминированным., если при одной и той же его конфигурации возможен
более чем один переход. В противном случае (если из любой конфигурации
МП-автомата по любому входному символу возможно не более одного
перехода в следующую конфигурацию) МП-автомат считается детерминированным
(ДМП-автоматом). ДМП-автоматы задают класс детерминированных КС-языков, для которых
существуют однозначные КС-грамматики. Именно этот класс языков
лежит в основе синтаксических конструкций всех языков программйрова ния, так
как любая синтаксическая конструкция языка программирования должна допускать только
однозначную трактовку [1-4, 7].
По произвольной
КС-грамматике G(VN,VT,P,5), V = VTuVN всегда можно построить
недетерминированный МП-автомат, который допускает
цепочки языка, заданного этой грамматикой [1-3, 7]. А на основе этого
МП-автомата можно создать
распознаватель для заданного языка.
Однако при алгоритмической
реализации функционирования такого распознавателя могут возникнуть проблемы.
Дело в том, что построенный МП-автомат будет, как правило,
недетерминированным, а для МП-автоматов, в отличие от обычных
КА, не существует алгоритма, который позволял бы преобразовать произвольный
МП-автомат в ДМП-автомат. Поэтому программирование функционирования МП-автомата
— нетривиальная задача. Если моделировать его функционирование по шагам с
перебором всех возможных состояний, то может оказаться, что
построенный для тривиального МП-автомата алгоритм никогда не завершится
на конечной входной цепочке символов при определенных условиях. Примеры таких МП-автоматов можно найти в [1, 3, 7].
Поэтому для построения
распознавателя для языка, заданного КС-грамматикой, рекомендуется
воспользоваться соответствующим математическим аппаратом и одним из существующих
алгоритмов.
Виды
распознавателей для КС-языков
Существуют несложные
преобразования КС-грамматик, выполнение которых гарантирует, что
построенный на основе преобразованной грамматики МП-автомат можно будет
промоделировать за конечное время на основе конечных вычислительных
ресурсов. Описание сути и алгоритмов этих преобразований можно найти в [1, 3, 7].
Эти преобразования
позволяют строить два основных типа простейших распознавателей:
□ распознаватель
с подбором альтернатив;
□ распознаватель
на основе алгоритма «сдвиг-свертка».
Работу распознавателя с
подбором альтернатив можно неформально описать следующим
образом: если на верхушке стека МП-автомата находится нетерминальный
символ А, то его можно заменить на
цепочку символов а при условии, что в грамматике языка есть
правило А —> а, не сдвигая при этом считывающую головку автомата
(этот шаг работы называется «подбор альтернативы»); если же на верхушке
стека находится терминальный символ а, который совпадает с текущим символом
входной цепочки, то этот символ можно выбросить из стека и передвинуть
считывающую головку на одну позицию вправо (этот шаг работы называется
«выброс»). Данный МП-автомат может быть недетерминированным, поскольку
при подборе альтернативы в грамматике языка может оказаться более одного правила
вида Л -^ а, тогда функция 8(<уД,Л) будет содержать
более одного следующего состояния — у МП-автомата будет несколько
альтернатив.
Решение
о том, выполнять ли на каждом шаге работы МП-автомата выброс или подбор
альтернативы, принимается однозначно. Моделирующий алгоритм должен
обеспечивать выбор одной из возможных альтернатив и хранение информации о том,
какие альтернативы на каком шаге уже были выбраны,
чтобы иметь возможность вернуться к этому шагу и подобрать другие альтернативы.
Распознаватель с подбором альтернатив является нисходящим распознавателем: он
читает входную цепочку символов слева направо и строит левосторонний вывод.
Название «нисходящий» дано ему потому, что дерево вывода в этом случае следует
строить сверху вниз, от корня к концевым вершинам («листьям»)1.
Работу распознавателя на основе алгоритма «сдвиг-свертка» можно описать
так: если на верхушке стека МП-автомата находится цепочка символов у, то ее
можно заменить на нетерминальный символ А при
условии, что в грамматике языка существует правило вида Л —> у, не сдвигая
при этом считывающую головку автомата (этот шаг работы называется «свертка»);
с другой стороны, если считывающая головка автомата обозревает некоторый символ
входной цепочки а, то его можно поместить в стек, сдвинув при этом
головку на одну позицию вправо (этот шаг работы называется «сдвиг» или
«перенос»).
Этот распознаватель потенциально имеет больше
неоднозначностей, чем рассмотренный выше распознаватель, основанный на
алгоритме подбора альтернатив. На каждом шаге работы автомата надо решать
следующие вопросы:
Q что необходимо выполнять: сдвиг или свертку;
□
если
выполнять свертку, то какую цепочку у выбрать для поиска правил (цепочка у должна встречаться в правой
части правил грамматики);
□
какое
правило выбрать для свертки, если окажется, что существует несколько правил
вида А —> у (несколько правил с
одинаковой правой частью).
Для моделирования работы этого расширенного МП-автомата надо
на каждом шаге запоминать все предпринятые действия, чтобы иметь возможность
вернуться к уже сделанному шагу и выполнить
эти же действия по-другому. Этот процесс должен повторяться до тех пор,
пока не будут перебраны все возможные варианты. Распознаватель на основе
алгоритма «сдвиг-свертка» является восходящим распознавателем: он читает
входную цепочку символов слева направо и строит правосторонний вывод. Название
«восходящий» дано ему потому, что дерево вывода в этом случае следует строить
снизу вверх, от концевых вершин к корню. Функционирование обоих рассмотренных
распознавателей реализуется достаточно простыми алгоритмами, которые можно
найти в [3, 7]. Однако оба они имеют один существенный недостаток — время их
функционирования экспоненциально зависит от длины входной цепочки п = |а|, что недопустимо для компиляторов, где
длина входных программ составляет от десятков до сотен тысяч символов. Так
происходит потому, что оба алгоритма выполняют разбор входной цепочки символов методом простого перебора, подбирая правила
грамматики произвольным образом, а в случае неудачи возвращаются к уже
прочитанной части входной цепочки и пытаются подобрать другие правила.
Существуют
более эффективные табличные распознаватели, построенные на основе алгоритмов
Эрли и Кока—Янгера—Касами [1,3]. Они обеспечивают полиномиальную зависимость
времени функционирования от длины входной цепочки (я3 для
произвольного МП-автомата и п2 для
ДМП-автомата). Это самые эффективные из универсальных распознавателей для
КС-языков. Но и полиномиальную зависимость времени разбора от длины входной
цепочки нельзя признать удовлетворительной.
Лучших
универсальных распознавателей не существует. Однако среди всего типа КС-языков
существует множество классов и подклассов языков, для которых можно построить
распознаватели, имеющие линейную зависимость времени функционирования от длины
входной цепочки символов. Такие распознаватели называют линейными распознавателями
КС-языков!
В
настоящее время известно множество линейных распознавателей и соответствующих
им классов КС-языков. Каждый из них имеет свой алгоритм функционирования, но
все известные алгоритмы являются модификацией двух базовых алгоритмов —
алгоритма с подбором альтернатив и алгоритма «сдвиг-свертка», рассмотренных
выше. Модификации заключаются в том, что алгоритмы выполняют подбор правил
грамматики для разбора входной цепочки символов не произвольным образом, а руководствуясь установленным порядком, который создается
заранее на основе заданной КС-грамматики. Такой подход позволяет избежать возвратов
к уже прочитанной части цепочки и существенно сокращает время, требуемое на ее
разбор.
Среди
всего множества можно выделить следующие наиболее часто используемые
распознаватели:
□
распознаватели
на основе рекурсивного спуска (модификация алгоритма с подбором альтернатив);
□
распознаватели
на основе 11(1)- и 11(£)-грамматик (модификация алгоритма с подбором
альтернатив);
□
распознаватели
на основе LR(0)-
и 1й(1)-грамматик
(модификация алгоритма «сдвиг-свертка»); • ,-, Г'
□
распознаватели
на основе SLR(l)- и 1Л1Л(1)-грамматик (модификация алгоритма
«сдвиг-свертка»);
□
распознаватели
на основе грамматик предшествования (модификация алгоритма «сдвиг-свертка»).
Алгоритмы функционирования всех
перечисленных и ряда других линейных распознавателей описаны в [1-4, 7J.
Построение
синтаксического анализатора
Синтаксический анализатор должен распознавать весь текст
исходной программы. Поэтому, в отличие от лексического анализатора, ему нет
необходимости искать границы распознаваемой строки символов. Он должен
воспринимать всю информацию, поступающую ему на вход, и либо подтвердить ее
принадлежность входному языку, либо сообщить об ошибке в исходной программе.
Но,
как и в случае лексического анализа, задача синтаксического анализа не ограничивается
только проверкой принадлежности цепочки заданному языку. Необходимо оформить
найденные синтаксические конструкции для дальнейшей гене-;
рации текста результирующей программы. Синтаксический анализатор должен! иметь
некий выходной язык, с помощью которого он передает следующим фазам компиляции
информацию о найденных и разобранных синтаксических структурах. В таком случае
он уже является не разновидностью МП-автомата, а преобразователем с магазинной
памятью — МП-преобразователем [1, 2, 7]. Вопросы, связанные с представлением
информации, являющейся результатом работы синтаксического анализатора, и с
порождением на основе этой информации текста результирующей программы,
рассмотрены в лабораторной работе № 4, поэтому здесь на них останавливаться не
будем.
Построение
синтаксического анализатора — это более творческий
процесс, чем' построение лексического анализатора. Этот процесс не всегда может
быть полностью формализован.
Имея грамматику входного языка, разработчик синтаксического
анализатора должен в первую очередь выполнить ряд формальных преобразований
над этой грамматикой, облегчающих построение распознавателя. После этого он
должен проверить, относится ли полученная грамматика к одному из известных
классов КС-языков, для которых существуют линейные распознаватели. Если такой
класс найден, можно строить распознаватель (если найдено несколько классов,
следует выбрать тот, для которого построение распознавателя проще либо
построенный распознаватель будет обладать лучшими характеристиками). Если же
такой класс КС-языков найти не удалось, то разработчик должен попытаться
выполнить над грамматикой некоторые преобразования, чтобы привести ее к одному
из известных классов. Эти преобразования не могут быть описаны формально, и в
каждом конкретном случае разработчик должен попытаться найти их сам (иногда
преобразования имеет смысл искать даже в том случае, когда грамматика
подпадает под; одни из известных классов КС-языков, с целью найти
другой класс, для которого можно построить лучший по характеристикам
распознаватель). Сложностей с построением синтаксических анализаторов не
существовало бы, если, бы для КС-грамматик были
разрешимы проблемы преобразования и эквивалентности. Но поскольку в общем
случае это не так, то одним классом КС-грамматик,' для
которого существуют линейные распознаватели, ограничиться не удается. По этой
причине для всех классов КС-грамматик существует принципиально важное
ограничение: в общем случае невозможно преобразовать произвольную КС-грамматику
к виду, требуемому данным классом КС-грамматик, либо же доказать, что такого
преобразования не существует. То, что проблема неразрешима в общем случае, не
говорит о том, что она не решается в каждом конкретном частном случае, и
зачастую удается найти такие преобразования. И чем шире набор классов
КС-грамматик с линейными распознавателями, тем проще их искать. Только, когда в
результате всех этих действий не удалось найти соответствующий класс КС-языков,
разработчик вынужден строить универсальный распознаватель. Характеристики
такого распознавателя будут существенно хуже, чем у линейного распознавателя: в
лучшем случае удается достичь квадратичной зависимости времени работы
распознавателя от длины входной цепочки. Такое бывает редко, поэтому все
современные компиляторы построены на основе линейных распознавателем! (иначе
время их работы было бы недопустимо велико).
Часто одна и та же КС-грамматика может быть отнесена не к
одному, а сразу к нескольким классам КС-грамматик, допускающих построение
линейных распознавателей. Тогда необходимо решить, какой из нескольких
возможных распознавателей выбрать для практической реализации.
Ответить на этот вопрос не всегда легко, поскольку могут
быть построены два принципиально разных распознавателя, алгоритмы работы которых несопоставимы. В первую очередь речь идет
именно о восходящих и нисходящих распознавателях: в основе первых лежит
алгоритм подбора альтернатив, в основе вторых — алгоритм «сдвиг-свертка».
11а вопрос о том, какой распознаватель — нисходящий пли
восходящий — выбрать для построения синтаксического анализатора, пет
однозначного ответа. Эту проблему необходимо решать, опираясь на некую
дополнительную информацию о том, как будут использованы пли
каким образом будут обработаны результаты работы распознавателя. Более
подробно обсуждение этого вопроса можно найти в [1, 7].
СОВЕТ----------------------------------------------------------------------------------------------------------------
Следует вспомнить, что синтаксический анализатор—
это один из этапов компиляции. И с этой точки зрения
результаты работы распознавателя служат исходными данными для следующих этапов
компиляции. Поэтому выбор того или иного распознавателя во многом зависит от
реализации компилятора, от того, какие принципы положены в его основу.
Желание использовать более простой класс грамматик для
построения распознавателя может потребовать каких-то манипуляций с заданной
грамматикой, необходимых для ее преобразования к требуемому классу. При этом
нередко грамматика становится неестественной и малопонятной, что в дальнейшем
затрудняет ее использование для генерации результирующего кода. Поэтому бывает
удобным использовать исходную грамматику такой, какая она есть, не стремясь
преобразовать ее к более простому классу.
В целом следует отметить, что, с учетом всего сказанного,
интерес представляют как левосторонний, так и правосторонний анализ. Конкретный
выбор зависит от реализации конкретного компилятора, а также от сложности
грамматики входного языка программирования.
В общем виде процесс построения синтаксического анализатора
можно описать следующим образом:
1.
Выполнить
простейшие преобразования над заданной КС-грамматикой.
Проверить
принадлежность КС-грамматики, получившейся в результате преобразований, к
одному из известных классов КС-грамматик, для которых существуют линейные
распознаватели.
Суть принципа такого разбора поясняет рис. 3.1. На нем
изображена входная цепочка символов ауР5 в тот момент, когда выполняется
свертка цепочки у. Символ а является
последним символом подцепочки а, а символ b — первым
символом подцепочки р\ Тогда, если в грамматике
удастся установить непротиворечивые отношения предшествования, то в процессе
выполнения разбора по алгоритму «сдвиг-свертка» можно
всегда выполнять сдвиг до тех пор, пока между символом на
верхушке стека и текущим символом входной цепочки существует отношение <.
или =-. А как только между этими символами будет обнаружено отношение >,. сразу
надо выполнять свертку. Причем для выполнения свертки из стека надо выбирать
все символы, связанные отношением =■. Все различные правила в грамматике
предшествования должны иметь различные правые части — это гарантирует непротиворечивость
выбора правила при выполнении свертки.
a |
а |
Рис.
3.1. Отношения между символами входной цепочки в грамматике предшествования
Таким образом,
установление непротиворечивых отношений предшествования между
символами грамматики в комплексе с несовпадающими правыми частями различных
правил дает ответы на все вопросы, которые надо решить для организации работы алгоритма
«сдвиг-свертка» без возвратов.
На
основании отношений предшествования строят матрицу предшествования грамматики. Строки матрицы предшествования
помечаются первыми (левыми) символами,
столбцы — вторыми (правыми) символами отношений предшествования. В клетки
матрицы на пересечении соответствующих столбца и строки помещаются знаки отношений. При этом пустые клетки
матрицы говорят о том, что между
данными символами нет ни одного отношения предшествования. Существует несколько видов грамматик
предшествования. Они различаются по тому, какие отношения предшествования в них
определены и между какими типами символов (терминальными или нетерминальными)
могут быть установлены эти отношения. Кроме того, возможны незначительные
модификации функционирования самого
алгоритма «сдвиг-свертка» в распознавателях для таких : грамматик (в основном на этапе выбора правила для
выполнения свертки, когда возможны неоднозначности) [1]. Выделяют
следующие виды грамматик предшествования:
□ простого предшествования;
□ расширенного предшествования;
□ слабого предшествования;
□ смешанной
стратегии предшествования;
□ операторного
предшествования.
Далее будут рассмотрены ограничения на структуру правил и
алгоритмы разбора для
грамматик операторного предшествования.
Матрицу операторного
предшествования КС-грамматики можно построить, опираясь
непосредственно на определения отношений предшествования [1, 3, 7], но проще и
удобнее воспользоваться двумя дополнительными типами множеств — множествами
крайних левых и крайних правых символов, а также множествами крайних
левых терминальных и крайних правых терминальных символов для всех нетерминальных символов
грамматики.
Если имеется
КС-грамматика G(VT,VN,P,5), V = VT и VN, то множества крайних
левых и крайних правых символов определяются следующим образом:
Q
L([7)
= {Г | 3 U=>
*Tz)
— множество крайних левых символов относительно нетерминального символа U;
□ R(£Z) = {Т\ 3 [/=> *zT) —
множество крайних правых символов относительно
нетерминального
символа U,
где U — заданный
нетерминальный символ (U e VN), Г — любой символ грамматики
(Гg V),az — произвольная цепочка символов (ze V*,
цепочка z может быть и пустой цепочкой).
Множества крайних левых и
крайних правых терминальных символов определяются следующим образом:
□ L(([/)
= [t |
3 f/=> *tz или 3 U=> *Ctz) —
множество крайних левых терминальных символов относительно нетерминального
символа U;
Q R,( U) = {t 13
U =>
*zt или 3 U => *ztC} —
множество крайних правых терминальных символов относительно
нетерминального символа U,
где t— терминальный. символ (£е VT), U и
С— нетерминальные символы ([/, С е
VN), a z —
произвольная цепочка символов (z e V*, цепочка может быть и пустой цепочкой).
Множества L(U) nR(U) могут быть построены для каждого
нетерминального символа
U e VN
по очень простому алгоритму:
1. Для
каждого нетерминального символа U ищем все правила,
содержащие U в
левой части. Во множество L(f/) включаем самый левый
символ из правой части правил, а во множество R(£7) — самый правый символ из
правой части (то есть во множество L(C/) записываем все символы, с которых
начинаются правила для символа U, а
во множество R(LT) —
символы, которыми эти правила заканчиваются). Если
в правой части правила для символа U имеется только один символ, то он должен быть записан в оба множества — Ц[/) iiR(tT).
Для каждого
нетерминального символа U выполняем следующее преобразование: если множество L(U) содержит
нетерминальные символы грамматики £/', U", ...,
то его надо дополнить символами, входящими в соответствующие множества
L(Lf), L(£/")... и не
входящими в Ц(/)- Ту же операцию надо выполнить
для R(f/). Фактически, если
какой-то символ ГУ' входит в одно из множеств для
символа U, то
надо объединить множества для V и U, а
результат записать
во множество для символа U.
Алгоритм
«сдвиг-свертка» для грамматик операторного предшествования
Алгоритм «сдвиг-свертка» для грамматики операторного
предшествования выполняется
МП-автоматом с одним состоянием. Для моделирования его работы необходима
входная цепочка символов и стек символов, в котором автомат может обращаться
не только к самому верхнему символу, но и к некоторой цепочке символов на
вершине стека.
Этот алгоритм для заданной КС-грамматики G(VT,VN,P,5) при наличии построенной матрицы
предшествования можно описать следующим образом:
1.
Поместить
в верхушку стека символ 1м («начало строки»), считывающую головку
МП-автомата поместить в начало входной цепочки (текущим входным символом
становится первый символ входной цепочки). В конец входной цепочки надо
дописать символ _LK
(«конец строки»).
2.
В
стеке ищется самый верхний терминальный символ s; (если на вершине стека лежат нетерминальные символы, они игнорируются и берется первый терминальный символ,
находящийся под ними), при этом сам символ s. остается в стеке. Из входной цепочки
берется текущий символ ai (справа от считывающей головки МП-автомата).
3.
Если
символ s.
— это символ начала
строки (Х„), а символ а. — символ
конца строки (1к), то алгоритм завершен, входная цепочка символов
разобрана.
4.
В
матрице предшествования ищется клетка на пересечении строки, помечен-' ной символом s., и столбца, помеченного символом ai (выполняется сравнение текущего входного
символа и терминального символа на верхушке стека).
5.
Если
клетка, найденная на шаге 3, пустая, то значит, входная строка символов не
принимается МП-автоматом, алгоритм прерывается и выдает сообщение об; ошибке.
6.
Если
клетка, найденная на шаге 3, содержит символ «=•» («составляет основу») или «<•»
(«предшествует»), то необходимо выполнить перенос (сдвиг). При выполнении
переноса текущий входной символ а. помещается
на верхушку стека, считывающая головка МП-автомата во входной цепочке символов
сдвигаётся на одну позицию вправо (после чего текущим входным символом
становится, следующий символ ai + v i := i+ 1). После этого надо вернуться к шагу 2.
7.
Если
клетка, найденная на шаге 3, содержит символ «•>» («следует»), то необходимо
произвести свертку. Для выполнения свертки из стека
выбираются все! терминальные символы, связанные отношением «=•» («составляет
основу»)}! начиная от вершины стека, а также все нетерминальные символы,
лежащие в стеке рядом с ними. Эти символы вынимаются из стека и
собираются в ней почку у (если в стеке нет символов, связанных отношением
«=■», то из него! вынимается один самый верхний терминальный символ и
лежащие рядом с ним нетерминальные символы).
Во
всем множестве правил Р грамматики G(VT,VN,P,S) ищется правило, у которого правая
часть совпадает с цепочкой у (по условиям грамматик предшествования все
правые части правил должны быть различны, поэтому может быть найдено или одно
такое правило, или ни одного). Если правило найдено, то в стек помещается
нетерминальный символ из левой части правила, иначе, если правило не найдено,
это значит, что входная строка символов не принимается МП-автоматом, алгоритм
прерывается и выдает сообщение об ошибке. Следует отметить, что при выполнении
свертки считывающая головка автомата не сдвигается и текущий входной символ at остается неизменным. После выполнения
свертки необходимо вернуться к шагу 2.
После
завершения алгоритма решение о принятии цепочки зависит от содержимого стека.
Автомат принимает цепочку, если в результате завершения алгоритма он находится
в состоянии, когда в стеке находятся начальный символ грамматики 5 и символ ±н.
Выполнение алгоритма может быть прервано, если на одном из его шагов возникнет
ошибка. Тогда входная цепочка не принимается. Алгоритм «сдвиг-свертка» для
грамматики операторного предшествования игнорирует нетерминальные символы.
Поэтому имеет смысл преобразовать исходную грамматику таким образом, чтобы
оставить в ней только один нетерминальный символ. Это преобразование
заключается в том, что все нетерминальные символы в правилах грамматики заменяются на один нетерминальный символ (чаще всего —
целевой символ грамматики).
Построенная
в результате такого преобразования грамматика называется остовной грамматикой,
а само преобразование — основным преобразованием [1, 7]. Остовное
преобразование не ведет к созданию эквивалентной грамматики и выполняется
только для упрощения работы алгоритма (который при выборе правил все равно
игнорирует нетерминальные символы) после построения матрицы предшествования.
Полученная в результате остовного преобразования грамматика может не являться
однозначной, но все необходимые данные о порядке применения правил содержатся
в матрице предшествования и распознаватель остается детерминированным. Поэтому
остовное преобразование может выполняться без потерь информации только после
построения матрицы предшествования. При этом также необходимо следить, чтобы в
грамматике не возникло неоднозначностей из-за одинаковых правых частей правил,
которые могут появиться в остовной грамматике. Вывод, полученный при разборе на
основе остовной грамматики, называют результатом остовного разбора, или
остовным выводом. По результатам остовного разбора можно построить
соответствующий ему вывод На основе правил исходной
грамматики. Однако эта задача не представляет практического интереса, поскольку
остовной вывод отличается от вывода на основе Исходной грамматики только тем,
что в нем отсутствуют шаги, связанные с применением цепных правил, и не
учитываются типы нетерминальных символов. Для Компиляторов же распознавание
цепочек входного языка заключается не в нахождении того или иного вывода, а в
выявлении основных синтаксических конструкций исходной программы с целью
построения на их основе цепочек языка Результирующей программы. В этом смысле
типы нетерминальных символов И Цепные правила не несут
никакой полезной информации, а напротив, только Усложняют обработку цепочки
вывода. Поэтому для реального компилятора нахождение остовного вывода является
даже более полезным, чем нахождение вывода на основе исходной грамматики.
Найденный остовной вывод в дальнейших преобразованиях уже не нуждается1.
В общем виде последовательность построения распознавателя
для КС-грамматики операторного предшествования G(VT,VN,P,5) можно описать следующим образом:
1 На основе множества правил грамматики Р построить множества крайних левых и крайних правых
символов для всех нетерминальных символов грамматики Uе. VN: L(U) и
R(U).
2.
На
основе множества правил грамматики Р и построенных на
шаге 1 множеств L(U) и R(£/)
построить множества крайних левых и крайних правых терминальных символов для
всех нетерминальных символов грамматики Ue VN: L,(£/)hR,(£/).
3.
На
основе построенных па шаге 2 множеств ht(U) и R,([/)
для всех терминальных символов грамматики а е VT заполняется матрица операторного предшествования.
4.
Исходная
грамматика G(VT,VN,P,5) преобразуется в остовную грамматику G'(VT,{5},P,S) с одним нетерминальным символом.
5.
На
основе построенной матрицы предшествования и остовной грамматики | строится
распознаватель па базе алгоритма «сдвиг-свертка» для грамматик операторного
предшествования.
Важно, что алгоритм распознавателя,может
быть реализован вне зависимости от матрицы предшествования и правил исходной
грамматики. Тогда, меняя матрицу и правила, один и тот же алгоритм можно
использовать для распознавания входных цепочек любой грамматики операторного
предшествования. Далее в примере выполнения работы проиллюстрирован именно
такой подход к построению распознавателя.
Требования к выполнению
работы
Порядок
выполнения работы
Для выполнения лабораторной работы
требуется написать программу, которая выполняет лексический анализ входного
текста в соответствии с заданием, тот
рождает таблицу лексем и выполняет синтаксический разбор текста по заданной)
грамматике с построением дерева разбора. Текст па входном языке задается в виде символьного
(текстового) файла. Синтаксис входного языка и перечень допустимых лексем указаны в задании. Допускается исходить из условия, что
текст содержит не более одного предложения входного языка.
При
наличии во входном файле текста, соответствующего заданному языку, программа
должна строить и отображать дерево синтаксического разбора. Если же текст во
входном файле содержит ошибки (лексические или синтаксические), программа
должна выдавать сообщения о наличии ошибок во входном тексте и корректно завершать
свое выполнение.
Рекомендуется разбить программу на три составные части:
лексический анализ, построение цепочки вывода и построение дерева вывода.
Лексический анализатор должен выделять в тексте лексемы языка и заменять их на терминальный символ грамматики (который в
задании обозначен как а). Полученная после лексического анализа цепочка должна
рассматриваться во второй части программы в соответствии с алгоритмом разбора.
При неудачном завершении алгоритма выдается сообщение об
ошибке, при удачном — строится цепочка вывода. После построения цепочки
вывода на ее основе строится дерево разбора, в котором символы
а последовательно заменяются на лексемы из таблицы лексем.
Для
выполнения лексического анализа рекомендуется использовать программные модули,
созданные в результате выполнения лабораторной работы № 2. Длину
идентификаторов и строковых констант можно считать ограниченной 32 символами.
Программа должна допускать наличие комментариев
неограниченной длины во входном файле. Форму организации комментариев
предлагается выбрать самостоятельно.
1.
Получить
вариант задания у преподавателя.
2.
Построить
множества крайних левых и крайних правых символов, множества крайних правых и
крайних левых терминальных символов и матрицу операторного предшествования для
заданной грамматики (если для построения синтаксического распознавателя
предполагается использовать другой механизм, отличный от грамматик операторного
предшествования, то форму его надо предварительно согласовать с
преподавателем).
3.
Выполнить
разбор простейшего примера вручную по правилам заданной грамматики, убедиться,
что разбор выполняется корректно.
4.
Подготовить
и защитить отчет.
5.
Написать
и отладить программу на ЭВМ.
6.
Сдать
работающую программу преподавателю.
Требования к оформлению отчета
Отчет должен
содержать следующие разделы:
□
Задание
по лабораторной работе.
□
Краткое
изложение цели работы.
Запись
заданной грамматики входного языка в форме Бэкуса— Наура
(если для построения синтаксического распознавателя используется механизм,
требующий преобразования исходной грамматики входного языка, то эти преобразования
и полученная в результате их грамматика должны быть отражены в отчете).
□ Множества
крайних правых и крайних левых символов с указанием шагов построения.
□ Множества
крайних правых и крайних левых терминальных символов.
□ Заполненную матрицу предшествования для
грамматики (если для построения
синтаксического распознавателя используется другой механизм, отличный от грамматик операторного предшествования, то
форму его отображения в отчете надо согласовать с преподавателем).
□ Пример
выполнения разбора простейшего предложения входного языка.
□ Текст программы (оформляется после выполнения программы на
ЭВМ).
Основные
контрольные вопросы
□ Какую
роль выполняет синтаксический анализ в процессе компиляции?
□ Какие
проблемы возникают при построении синтаксического анализатора и как они могут быть решены?
□ Какие
типы грамматик существуют? Что такое КС-грамматики? Расскажите об их использовании в
компиляторе.
□ Какие типы распознавателей для КС-грамматик существуют?
Расскажите о недостатках и
преимуществах различных типов распознавателей.
□ Поясните
правила построения дерева вывода грамматики.
□ Что
такое грамматики простого предшествования?
□ Как вычисляются отношения предшествования для
грамматик простого предшествования ?
□ Что такое
грамматика операторного предшествования ?
□ Как вычисляются отношения для грамматик
операторного предшествования ?
□ Расскажите о задаче разбора. Что такое
распознаватель языка?
□ Расскажите об общих принципах работы распознавателя языка.
□ Что такое перенос, свертка? Для чего необходим
алгоритм «перенос-свертка»?
□
Расскажите, как работает алгоритм «перенос-свертка» в
общем случае (с возвратами).
□
Как работает алгоритм «перенос-свертка» без возвратов
(объясните на своем примере)?
Варианты
заданий Варианты исходных грамматик
Далее приведены варианты грамматик. Во всех вариантах
символ S является
начальным символом грамматики; S, F, Ти £
обозначают нетерминальные символы.
Терминальные символы
выделены жирным шрифтом. Вместо символа а должны
подставляться
лексемы. i
1. 5-^a:=F;
F-+F+T\T
Т^Т-Е\ TIE\E E-*{F)\-{F)\a
2. 5^a:=F;
F -> F or T | F xor T |
T
T->TsmdE\E £ -»(F) I not (F)
| a
3. 5->F;
F -» if E then T else F\iiE then
F | a := a
T
-$> if E then T else T
| a := a E
—> a<a | a>a | a=a
4. S^F;
F -> for (T) do F | a :=
a
T-^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 Идентификаторы,
римские числа
№ варианта
Допустимые лексемы входного языка грамматики
8
4 Идентификаторы, римские
числа
9
1 Идентификаторы,
шестнадцатеричные числа
10
2 Идентификаторы,
шестнадцатеричные числа
11
3 ' Идентификаторы, шестнадцатеричные числа
12
4 Идентификаторы,
шестнадцатеричные числа
13
1 Идентификаторы,
символьные константы (в одинарных кавычках)
14
2 Идентификаторы,
символьные константы Т и'F'
15
3 Идентификаторы,
строковые константы (в двойных кавычках) 16
4 Идентификаторы, строковые константы (в
двойных кавычках)
примечание-
римскими числами считать последовательности больших
латинских букв X,
V
и I.
Шестнадцатеричными числами считать
последовательность цифр и символов «а», «Ь», «с», «d»,
«е» и «f»,
начинающуюся с цифры (например: 89, 45ас9, 0abc4). Для выполнения работы
рекомендуется использовать лексический анализатор, построенный в ходе
выполнения лабораторной работы № 2.
Пример
выполнения работы Задание для примера
Для выполнения лабораторной работы возьмем тот же самый
язык, который был использован
для выполнения лабораторной работы № 2.
Этот язык может быть задан, например, с помощью следующей
КС-грамматики G({if,then,else,a,:=,or,xor,and,(,),;},{5,jF,£',D,Q,P,5) с правилами Р:
S->-F;
F—»if E then T else F | if E
then F \ a :=
Жирным шрифтом в грамматике и в правилах выделены
терминальные символы. 1 Как
было уже сказано ранее, выбранный в качестве примера язык не совпадает ни с
одним из предложенных выше вариантов и, кроме этого, служит хорошей иллюстрацией
основных особенностей построения синтаксического распознавателя, присущих различным
вариантам.
Построение матрицы операторного предшествования
Построение
множеств крайних правых и крайних левых символов
Построение множеств крайних
левых и крайних правых символов выполним согласно описанному ранее алгоритму.
На первом шаге возьмем все крайние левые
и крайние правые символы из правил грамматики G. Получим множества,
представленные в табл. 3.2.
Таблица
3.2. Множества
крайних левых и крайних правых символов. Шаг 1
Символ U L(U) R(U)
S F ;
F if, a F, E
T if, a T; E
E E, D D
D D, С С
С а, ( a, )
Из
табл. 3.2 видно, что множества L(f/) для символов S, E, D, а
также множества R([/)
для символов F, T, E, D содержат
другие нетерминальные символы, а потому должны быть дополнены.
Например, L(5) должно быть
дополнено L(F), так как символ
F входит
в L(5): Fe L(5),
a R(f) должно быть дополнено R(-E), так как символ Е входит в
R(F): E e R(F).
Выполним необходимые дополнения и получим
множества, представленные в табл. 3.3.
Таблица
3.3. Множества
крайних левых и крайних правых символов. Шаг 2
Символ U L(U) R(U)
S F, if, a , ;
F - if,
a F, E, D
T if, a T, E, D
E E, D, С D, С
D D, C, a, ( C, a,
)
С
a, ( a,)
Практически
все множества в табл. 3.3 изменились по сравнению с табл. 3.2 (кроме
множеств для символа С), а значит, построение не закончено.
Продолжим дополнять множества. Получим множества, представленные в табл. 3.4.
В
табл. 3.4 по сравнению с табл. 3.3 изменились
множества для символов F,TvlE — построение
не закончено. Продолжим дополнять множества. Получим множества, представленные в
таблице.
Лабораторная работа № 3 •
Построение простейшего дерева вывода
Таблица 3.4. Множества крайних левых и
крайних правых символов. Шаг 3
Символ U MU) ___________ *W_
F, if, a
if, a F, E, D, С
if, а Т, Е, D, С
Е Е, D, С, а, ( D,C,
а, )
D, С, а, ( с-а.)
а, |
D
С а>(
Таблица
3.5. Множества
крайних левых и крайних правых символов. Шаг 4 (результат)
Символ
U |
L(U) R<U>
s |
F, if, a |
; |
F |
if, a |
F, E, D, С
a, ) |
т |
if, a |
T, E, D, C, a, ) |
E |
E, D, C, a, ( |
D, С
a, ) |
D |
D, С,
а, ( |
C, a, ) |
С |
a, ( |
a,) |
В
табл. 3.5 по сравнению с табл. 3.4 изменились только
множества R(U) для
символов F и
Г — построение не закончено. Продолжим дополнять множества. Но если
выполнить еще один шаг (шаг 5), то можно убедиться, что множества уже больше
не изменятся (чтобы не создавать еще одну лишнюю таблицу, этот шаг здесь
выполнять не будем). Таким образом, множества, представленные в табл. 3.5, являются
результатом построения множеств крайних левых и крайних правых символов грамматики G.
Построение
множеств крайних правых и крайних левых терминальных
символов
Построение множеств крайних левых и крайних правых терминальных символов также выполним согласно
описанному выше алгоритму.
На первом шаге
возьмем все крайние левые и крайние правые
терминальные символы из правил грамматики G. Получим множества, представленные в табл. 3.6.8■
Таблица 3.6. Множества крайних левых и
крайних правых терминальных символов. Шаг 1
Символ
U Lt(U) Bt(U)
if a else, then, if, a else'
:= or, xor or,
xor and and |
S
F T
E D
Дополним множества,
представленные в табл. 3.6, на основании ранее построенных множеств
крайних левых и крайних правых символов, представленных в
табл. 3.5. Например, L,(£)
должно быть дополнено Lf(D) и Ц(С), так как символы D и С входят в L(F): D, С е L(E), a R,(F) должно
быть дополнено R,(E), R,(D) и R,(C), так
как символы
Е, D и С входят в R(F):
E,D, Ce R(F).
Получим итоговые множества
крайних левых и крайних правых терминальных символов, которые представлены в табл. 3.7.
Таблица
3.7. Множества
крайних левых и крайних правых терминальных символов. Результат
Символ U Lt(U) Rt(U)
if, a, ; if, a if, a or, xor, and, a, ( and, a, ( а, ( |
else, then, :=,
or, xor, and, a, ) else, := , or, xor, and, a, ) or, xor, and, a, ) and, a, )
a,)
Теперь
все готово для заполнения матрицы операторного предшествования.
Заполнение матрицы
предшествования
Для заполнения
матрицы операторного предшествования необходимы множества
крайних левых и крайних правых терминальных символов, представленные в табл. 3.7, и
правила исходной грамматики G.
Заполнение таблицы
рассмотрим на примере лексем or и
(. Символ or
не стоит рядом с другими терминальными символами в правилах грамматики.
Поэтому знак «=•» («составляет основу») для него не используется.
Символ or стоит слева от
нетерминального символа D в правиле Е —> Е or D. В множество L,(D) входят символы and, а и (. Поэтому в строке
матрицы, помеченной символом or,
ставим знак «<•» («предшествует») в клетках на пересечении со столбцами, помеченными
символами and, а и (.
Кроме того, символ or стоит справа от
нетерминального символа Е в том же правиле Е—» Eor D. В
множество R,(£) входят
символы or, xor, and, а и ). Поэтому
в столбце матрицы, помеченном символом or, ставим знак «•>» («следует») в
клетках на пересечении со строками, помеченными символами or, xor, and, а и ).
Больше ни в каких
правилах символ or не
встречается, поэтому заполнение матрицы для него закончено.
Символ ( стоит рядом с терминальным символом ) в правиле С —> (Е) (между ними должно
быть не более одного нетерминального символа — в данном случае один символ
Е). Поэтому в строке матрицы, помеченной символом (, ставим знак
«=■» («составляет основу») на пересечении со столбцом,
помеченным символом ). Символ ( также стоит слева от
нетерминального символа Е в том же правиле С —> (Е). В
множество L,(£)
входят символы or, not, and, а и (. Поэтому в строке
матрицы, помеченной символом (, ставим знак «<•» («предшествует») в клетках на
пересечении со столбцами, помеченными символами or, not, and, а
и (. Больше ни в каких правилах символ ( не
встречается, поэтому заполнение матрицы для него закончено.
Повторяя описанные
выше действия по заполнению матрицы для всех терминальных
символов грамматики G,
получим матрицу операторного предшествования. Останется только
заполнить строку, соответствующую символу ±и («начало строки»),
и столбец, соответствующий символу 1к («конец строки»). Начальным
символом грамматики G
является символ 5, поэтому для заполнения строки, помеченной 1н,
возьмем множество L,(5).
В это множество входят символы if, а и ;. Поэтому в строке матрицы,
помеченной символом ±п, ставим знак «<•»
(«предшествует») в клетках на пересечении со столбцами, помеченными символами а и ;.
Аналогично, для заполнения
столбца, помеченного _1_к, возьмем
множество R,(S). В
это множество входит только один символ — ;. Поэтому в
столбце матрицы, помеченном символом 1к, ставим знак «•>»
(«следует») в клетке на пересечении со строкой, помеченной символом ;.
В итоге получим
заполненную матрицу операторного предшествования, которая представлена в табл. 3.8.
iаолица о. Символы |
U. [VIC |
11 ^»ll_l,. if |
then |
else |
A |
or |
xor |
and |
( |
) |
1K |
; |
|
|
|
|
<. |
<• |
<• |
<. |
<■ |
|
■> |
if |
|
|
|
|
|
|
|
|
|
|
|
then |
■> |
<• |
|
=- |
<■ |
|
|
|
|
|
|
else |
.> |
<. |
|
■> |
<• |
;. .> |
.> |
•> |
|
•> |
|
а |
■> |
|
> |
■> |
|
<;. |
<. |
<. |
<• |
|
|
■ = |
•> |
|
.> |
<• |
|
|
|
|
|
|
|
or |
.> |
|
■> |
.> |
<• |
■> |
■> |
<. |
<• |
.> |
|
xor |
■> |
|
•> |
.> |
<• |
> |
•> |
<• |
<■ |
.> |
|
and |
•> |
|
.> |
•> |
<■ |
• > |
•> |
.> |
<■ |
•> |
|
( |
|
|
|
|
<• |
<■ |
<■
•> |
.> |
<■ |
> |
|
) |
.> |
|
•> |
.> |
|
|
|
|
|
|
|
1к |
<• |
<■ |
|
|
<■ |
|
|
|
|
|
|
Теперь на основе исходной грамматики G можно построить остовную
грамматику G'({if,then,else,a,:=,or,not,and,(,),;},{Е},Р',Е)
с правилами Р':
Е —>
Е; — правило 1;
Е —»if E then E else E | if E then Е \
а := Е — правила 2, 3 и
4;
Е —> if E then £
else Е \ а := Е —
правила 5 и 6;
Е —> Е or Е | Е
not E\E— правила 7, 8 и 9;
Е —> Е and Е \
Е — правила 10 и 11; £—> а | (Е) —
правила 12 и 13.
Жирным шрифтом в
грамматике и в правилах выделены терминальные символы. Всего
имеем 13 правил грамматики. Причем правила 2 и 5, а также правила 4 и 6 в
остовной грамматике неразличимы, а правила 9 и 11 не имеют смысла (как было уже
сказано, цепные правила в остовных грамматиках теряют смысл). То, что две пары
правил стали неразличимы, не имеет значения, так как по смыслу (семантике
входного языка) эти две пары правил обозначают одно и то же (правила 2 и 5 соответствуют
полному условному оператору, а правила 9 и 11 — оператору присваивания).
Поэтому в дереве синтаксического разбора нет необходимости их различать.
Следовательно, синтаксический распознаватель может пользоваться остовной грамматикой G'.
Примеры
выполнения разбора предложений входного языка
Рассмотрим примеры
разбора цепочек входного языка в виде последовательности
конфигураций МП-автомата, выполняющего разбор. Результат разбора будем
представлять в виде последовательности номеров правил грамматики. На основе
найденной последовательности правил после выполнения разбора при отсутствии
ошибок (когда входная цепочка принята МП-автоматом) можно построить дерево
синтаксического разбора.
Рассматриваемый
МП-автомат имеет только одно состояние. Тогда для иллюстрации работы МП-автомата будем записывать каждую
его конфигурацию в виде трех
составляющих {а|(3|у}, где:
□ а
— непрочитанная часть входной цепочки;
□ (3
— содержимое стека МП-автомата;
Q у- последовательность номеров примененных правил.
В начальном состоянии вся
входная цепочка не прочитана, стек автомата содержит только лексему типа
«начало строки», последовательность номеров правил пуста.
Для
удобства чтения стек МП-автомата будем заполнять в порядке справа налево,
тогда находящимся на верхушке стека будет считаться крайний правый символ в цепочке (3.
Пример 1
Возьмем входную цепочку
«if a or b and с
then a := 1 not с;».
После выполнения
лексического анализа, если все лексемы типа «идентификатор» и
«константа» обозначить как «а», получим цепочку: «if a or a and a then a := a not a;». Рассмотрим процесс
синтаксического анализа этой входной цепочки. Шаги функционирования
МП-автомата будем обозначать символом « + >>.
Символом « + п» будем обозначать шаги, на
которых выполняется сдвиг (перенос), символом « _=_ с» — шаги,
на которых выполняется свертка.
{if a
or a and a then а := a xor a;±J±JX} ■*■ м {a or a and a then a
:= a xor ajlJi^iflX} * „ {or a and a then a := а xor a;ij±„if a\k}
+ с {or a
and а then
a := а xor
a;lj±nif £|12} + м {a and я then а := а xor a;±J±ltif
£orjl2} + н {and a
then а := a
xor ajljj^if £ or я|12} +
c {and a then a := a xor ajJ-J_Lnif £ or
£|12 12} -{a then я := a
xor a;lJlMif £ or £ and|l2 12} - „ {then a := a xor
ajljl^if £ or £ and a|12 12} -s- c {then a := я xor a;±J±Hif
£ or £ and £|12 12 12} + c {then a ■«
a xor a;ljl„if £ or £jl2 12 12 10} + ,. {then
a := a xor a;±J±„if £jl2 12 12 10 7} * „ {a :=
a xor a;±K|±Hif £then|12 12 12 10 7} + „ {:=
a xor fl;ljlHif£then я|12 12 12 10 7} + „ {a xor a;±J±Hif£
then a:-|12 12 12 10 7} +n {xor a;±J±„if £ then a:- я|12 12 12 10 7} *
c {xor a;ljl„if £then a := £|12 12 12 10 7 12} +
„ {a;±J±Mif £ then a::- £xor|12 12 12 10 7 12} + м {;l,Jl„if£
then a:-E xor a|12 12 12 107 12} +c {;ljl„if £then a :=
£xor £jl2 12 12 10 7 12} -c {;ljl„if £then a :=
E|12 12 12 10 7 12 12 8} + c {;ll± if £ then £11212 12
10 7 12 12 8 4} + . {;ljl„£|12 12 12 10 7 12 12 8 4 3} + „
{1K|1„£;|12
12 12 10 7 12 12 8 4 3} + c
{±j£±J12 12 12 10 7 12 12 8 4 3 1} — разбор
закончен, МП-автомат перешел в конечную конфигурацию, цепочка принята.
В результате получим последовательность
правил: 12 12 12 10 7 12 12 8 4 3 1. Этой |
последовательности правил будет соответствовать цепочка вывода на основе ос- |
товной грамматики С: j
£=>, £; =>3 if £
then £; =>4 if £ then a := £; =>8
if £ then я := £ xor £; =>)2
if £ then а := £ xor a; j
=>l2 if £ then а := a xor a; =>7 if £ or £
then a := a xor я; =>J0 if £
or £ and £ then a := a xor a; j
=>,2 if £ or £ and a
then a := a xor a; =>i2 if £ or a and a then а := a xor a;
=>)2 if я or a and а >
then а := a
xor а; '
Стоит
обратить внимание, что, так как данный МП-автомат строит правосторонний вывод,
в цепочке вывода на каждом шаге правило всегда применяется тс крайнему правому
нетерминальному символу в цепочке.
Дерево синтаксического разбора, соответствующее данной
входной цепочке, приведено на рис. 3.2.
Рис.
3.2. Дерево синтаксического разбора входной цепочки «if a
or a and
a then a
:= a
xor a;»
Пример 2
Возьмем входную цепочку «if (a or b then a := 25;»..
После
выполнения лексического анализа, если все лексемы типа «идентификатор» и
«константа» обозначить как «а», получим цепочку: «if (a or a then a := а». Рассмотрим процесс синтаксического
анализа этой входной цепочки: {if
(a or a then a := a;J_J±jX} + {(a or a then a := e;lK[±ltif|A,} + п
[a or
a then a := e;!j-L if(|A.} -*■ „ {or a then а := a;±JJ_Hif(a|A} + с {orathene:=fl;±J±i(if(E|l2} *n {a then a := a;_Lj±Mif(£ or|12}
+ „ {then a := a;ljl Й(£ or a|12} + c {then a := a;lJlMif(£ or FJ12 12} + c
{then a := a;Xj±i(if(£|12 12 7} — пет отношения
предшествования между лексемами «(» и «then», разбор закончен, МП-автомат не перешел
в конечную конфигурацию, цепочка не принята (выдается сообщение об ошибке).
Реализация синтаксического распознавателя
Разбиение
на модули
В
лабораторной работе № 3. так же, как и в лабораторной работе № 2, модули, реализующие синтаксический анализатор
разделены на две группы:
□
модули,
программный код которых не зависит от входного языка;
□
модули,
программный код которых зависит от входного языка. В первую группу входят
модули:
□
SyntSymb
— описывает структуры данных для синтаксического анализа и реализует алгоритм
«сдвиг-свертка» для грамматик операторного предшествования;
□
FormLab3
— описывает интерфейс с пользователем.
Во вторую группу входит один модуль:
□ SyntRule — содержит описания матрицы операторного
предшествования и пра
вил исходной грамматики.
Такое
разбиение на модули позволяет использовать те же самые структуры данных для
организации синтаксического распознавателя при изменении входного
языка.
Кроме этих модулей для реализации лабораторной работы № 3
используются программные модули TblElem и FncTree, позволяющие работать с комбинированной
таблицей идентификаторов, которые были созданы при выполнении лабораторной работы К» 1, а
также модули LexType, LexElem, и LexAuto, которые обеспечивают работу лексического распознавателя
(эти модули были созданы при выполнении лабораторной работы № 2).
Кратко
опишем содержание программных модулей, используемых для организации
синтаксического анализатора. *,
Модуль
описания матрицы предшествования и правил грамматики
Модуль SyntRule содержит структуры данных, которые описывают
матрицу операторного предшествования и правила остовпой грамматики. Матрица
операторного предшествования (GramMatrix)
описана как двумерный массив, каждой строке и каждому столбцу которого
соответствует лексема (тип TLexType).
Важно, чтобы данные в строках и столбцах матрицы были заполнены в том же
порядке, в каком перечислены типы лексем в описании TLexType в модуле LexType. В каждой клетке матрицы находится
символ, обозначающий тип отношения предшествования:
□
'<"
— для отношения «<•» («предшествует»);
□
'.>' — для отношения <<■>>,>
(«следует»);
□
' =
' — для отношения «=•» («составляет основу»);
□ ' ' — для пустых клеток матрицы
(когда отношение операторного предшествования между двумя символами
отсутствует).
Кроме
матрицы операторного предшествования и правил грамматики в модуле SyntRule описана функция корректировки отношений предшествования
CorrectRul е, которая
позволяет расширять возможности грамматики операторного предшествования. В
данной лабораторной работе эта функция не используется (о технике ее
использования можно узнать далее из описания примера выполнения курсовой
работы).
В целом описанная в модуле SyntRule матрица операторного предшествования GramMatrix полностью соответствует построенной
матрице операторного предшествования (см. табл. 3.8). Отличие заключается в
том, что, поскольку терминальному символу а в
грамматике G
могут соответствовать два типа лексем входного языка (переменные и константы),
в матрице GramMatrix
строка и столбец, соответствующие символу а в табл. 3.8, продублированы.
Таким образом, построенный на основе
матрицы предшествования из табл. 3.8 синтаксический анализатор не различает
константы и переменные.
Это соответствует синтаксису заданного входного языка. Для этого языка
проводить различие между переменными и константами необходимо только в одном
случае: при анализе оператора присваивания (присваивать значение константе
нельзя). Для того чтобы компилятор находил такого рода ошибки, возможны два
варианта:
1. Изменить синтаксис входного языка
(грамматику G)
так, чтобы константы и переменные различались в правилах грамматики, и
перестроить синтаксический анализатор.
2.
Обрабатывать
присваивание значений константам на этапе семантического анализа.
В данном случае выбран второй вариант, который реализован в
лабораторной работе № 4 (где рассматриваются генерация кода и подготовка к
генерации кода). Позже, при разработке компилятора для выполнения курсовой
работы, рассмотрен первый вариант (см. главу, посвященную выполнению курсовой
работы). Каждый из рассмотренных вариантов имеет свои преимущества и
недостатки. В общем случае выбор того, на каком этапе компиляции будет
обнаружена та или иная ошибка, зависит от разработчика компилятора.
Правила
остовпой грамматики G'
описаны в виде массива строк GramRules.
Каждому правилу в этом массиве соответствует строка, по написанию совпадающая
с правой частью правила (пробелы игнорируются). Правила пронумерованы в порядке
слева направо и сверху вниз — так, как они были пронумерованы в остовпой
грамматике G'.
Для поиска подходящего правила используется метод простого перебора — так как
правил мало (всего 13), в данном случае этот метод вполне удовлетворителен.
Кроме двух упомянутых структур данных (GramMatrix и GramRul es) в модуле SyntRul e описана
также функция MakeSymbolStr,
возвращающая наименование нетерминального символа в правилах остовпой
грамматики. В грамматике G'
во всех правилах
3.
Если
соответствующий класс найден, взять за основу для построения распо- | знавателя алгоритм разбора входных цепочек, известный
для этого класса, если | найдено несколько классов линейных распознавателей —
выбрать из них один \ по своему усмотрению.
4.
Иначе,
если соответствующий класс по п. 2 не был найден или же найденный ] класс КС-грамматик не устраивает разработчиков
компилятора — попытаться ; выполнить над грамматикой неформальные
преобразования с целью подвести">\ ее под интересующий класс
КС-грамматик для линейных распознавателей!
и вернуться к п. 2.
5.
Если
же ни в п. 3, ни в п. 4
соответствующий распознаватель найти не удалось ' (что для современных языков
программирования практически невозможно), необходимо использовать один из
универсальных распознавателей.
6.
Определить,
в какой форме синтаксический распознаватель будет передавать! результаты своей
работы другим фазам компилятора (эта форма называется! внутренним
представлением программы в компиляторе).
Реализовать выбранный в
п. 3 или 5 алгоритм с учетом структур данных, соответ-4
ствующих п. 6.
В данной
лабораторной работе в заданиях предлагаются грамматики, не требую-'!
щие
дополнительных преобразований. Кроме того, гарантировано, что все они^
относятся
к классу КС-грамматик операторного предшествования, для которых!
существует
известный алгоритм линейного распознавателя. Поэтому создание!
синтаксического
распознавателя для выполнения лабораторной работы суще-1
ственно упрощается.
Для
грамматик, предложенных в заданиях, известно, что они относятся также!
к классам
КС-грамматик LR(\)
и LALR(i), для которых также существует известный алгоритм линейного
распознавателя, но, по мнению автора, этот алгоритм более сложен (его описание
можно найти в [1, 2,7]). Однако желающие могут на
согласиться с автором и использовать для выполнения
лабораторной работы любой из этих классов.
После несложных
преобразований эти же грамматики могут быть приведены к виду, удовлетворяющему
требованиям алгоритма рекурсивного спуска (или алгоритма анализа для И(1)-грамматик). Этот алгоритм тривиально прост, но для его
реализации надо выполнить достаточно несложные неформальные преобразования над
заданными грамматиками — автор оставляет эти преобразования для
желающих попробовать свои силы.
Выполняющие лабораторную работу могут пойти любым из
рекомендованные
путей или построить иной
синтаксический анализатор по своему усмотрению А
в этом направлении их ничто не ограничивает.
В
качестве основного пути выполнения лабораторной работы автор предлагает
распознаватель на основе грамматик операторного предшествования, поэтому именно
этот класс КС-грамматик далее рассмотрен более подробно (описания остальных
известных классов и подклассов КС-грамматик можно найти
в [1-3, 7]).
Грамматики
предшествования
КС-языки делятся на классы в соответствии со структурой
правил их грамматик. В каждом из классов налагаются дополнительные ограничения
на допустимые правила грамматики. Одним из таких классов является класс
грамматик предшествования. Они используются для синтаксического разбора
цепочек с помощью модификаций алгоритма «сдвиг-свертка».
Принцип организации распознавателя на основе грамматики
предшествования исходит из того, что для каждой упорядоченной пары символов в
грамматике устанавливается отношение, называемое отношением предшествования. В
процессе разбора МП-автомат сравнивает текущий символ входной цепочки с одним
из символов, находящихся на верхушке стека автомата. В процессе сравнения проверяется,
какое из возможных отношений предшествования существует между этими двумя
символами. В зависимости от найденного отношения выполняется либо сдвиг, либо
свертка. При отсутствии отношения предшествования между символами алгоритм
сигнализирует об ошибке.
Задача заключается в том, чтобы иметь возможность
непротиворечивым образом определить отношения предшествования между символами
грамматики. Если это возможно, то грамматика может быть отнесена к одному из
классов грамматик предшествования.
Отношения предшествования будем обозначать знаками «=.»,
«<.» и «.>». Отношение предшествования
единственно для каждой упорядоченной пары символов. При этом между какими-либо
двумя символами может и не быть отношения предшествования — это значит, что
они не могут находиться рядом ни в одном элементе разбора синтаксически
правильной цепочки. Отношения предшествования зависят от порядка, в котором
стоят символы, и в этом смысле их нельзя путать со знаками математических
операций (хотя по внешнему виду они очень похожи) — они не обладают ни
свойством коммутативности, ни свойством ассоциативности. Например, если
известно, что Bt > Bjt то не обязательно выполняется Bj <. Bi (поэтому знаки предшествования помечают
специальной точкой: «=.», «<.», «.>»). Метод предшествования основан на
том факте, что отношения предшествования между двумя соседними символами
распознаваемой строки соответствуют трем следующим вариантам:
□
.В,. <. Bj+,, если символ Bi +,
— крайний левый символ некоторой основы (это отношение между символами можно
назвать «предшествует основе» или просто «предшествует»);
□
В;.
> B.
+ v если символ Bt — крайний правый символ некоторой основы
(это отношение между символами можно назвать «следует за основой» или просто
«следует»);
Q В. =. Bi + V если символы Bt и В1+ (
принадлежат одной основе (это отношение между символами можно назвать
«составляют основу»).
Исходя из этих соотношений выполняется разбор входной строки
для грамматик :
предшествования.
(ст 69)
Ст 90
символ обозначен Е,
поэтому функция MakeSymbolStr
всегда возвращает 'Е' как результат
своего выполнения. Но тем не менее эта функция не
бессмысленна, так как могут быть другие варианты остовиых грамматик.
Модуль
структур данных для синтаксического анализа и реализации алгоритма
«сдвиг-свертка»
Модуль SyntSymb
содержит реализацию алгоритма «сдвиг-свертка» и описания всех структур
данных, необходимых для этой реализации. Поскольку сам алгоритм < «сдвиг-свертка»
не зависит от входного языка, реализующий его модуль также не
. зависит от входного языка и правил исходной грамматики
(они специально вынесены
в отдельный модуль). Основу модуля
составляют следующие структуры данных:
□ TSymblnfo —
описание двух типов символов грамматики: терминальных и не- j
терминальных;
□ TSymbol — описание всех данных,
связанных с понятием «символ грамматики»;»,■>
□ TSymbStack — описание синтаксического
стека.
Структура TSymblnfo содержит информацию о типе
символа грамматики — поле SymbType,
которое может принимать два значения: SYMBJ.EX
(терминальный сим*. вол) или SYMB_SYNT (нетерминальный символ), и
дополнительные данные:
□ ссылку
на лексему (LexOne) —
для терминального символа; щ
□ перечень
всех составляющих (LexList)
— для нетерминального символа. Перечень всех составляющих
нетерминального символа LexList
построен на оcнове
динамического массива (тип TList
из библиотеки VCL
системы программирования Delphi
5). В него вносятся ссылки на символы, на основании которыми; создан
данный символ, в том порядке, в котором они следуют в правиле грамматики. ■'- #' Структура TSymbol содержит информацию о
символе (поле5утЬ1птотипаТ5утЬ1п^щ: а также номер правила
грамматики, на основании которого создан символ (поли. .
данных iRul eNum). Для терминальных
символов номер правила равен 0, для нетер'|:, | минальных символов
он может быть от 1 до 13.
;ФЗ i Кроме этих данных
структура содержит методы, необходимые для работы с символами
грамматики:
□
конструктор CreateLex для создания терминального символа на основе лексемы; .:%!
□
конструктор CreateSymb для создания нетерминального символа на основе правила
грамматики и массива исходных символов; / ж
Q функции, процедуры и свойства для работы с
информацией, хранящейся в струкз |
-
деструктор Destroy
для освобождения занятой памяти при удалении символ.» (при
удалении нетерминального символа удаляются все ссылки на его состав!
ляющие и динамический массив
для их хранения);
Поскольку в ноле данных Symblnfo структуры TSymbol хранятся все ссылки на составляющие
символы, внутри которых, в свою очередь, могут храниться ссылки на
их составляющие и т. д., то на основе структуры TSymbol можно построить полное синтаксическое
дерево разбора.
Третья структура
данных TSymbStack
построена на основе динамического массива типа TList из библиотеки VCL системы программирования Delphi 5. Она предназначена для того,
чтобы моделировать синтаксический стек МП-автомата. В этой структуре нет никаких данных (используются только данные,
унаследованные от класса TList), но с ней связаны
методы, необходимые для работы синтаксического стека:
□ функция
очистки стека (CI ear) и деструктор для
освобождения памяти при
удалении
стека (Destroy);
р функция доступа к символам в стеке начиная от
его вершины (GetSymbol);
а функция для помещения в стек очередной
входящей лексемы (Push),
при этом лексема
преобразуется в терминальный символ;
а функция, возвращающая самую верхнюю лексему в
стеке (TopLexem),
при этом нетерминальные
символы игнорируются;
а функция, выполняющая свертку (MakeTopSymb); новый символ,
полученный в результате
свертки, помещается на вершину стека.
Кроме трех
перечисленных ранее структур данных в модуле SyntSymb описана также функция Bui 1 dSyntLi st, моделирующая работу
алгоритма «сдвиг-свертка» для грамматик операторного
предшествования. Входными данными для функции являются
список лексем (1 i stLex),
который должен быть заполнен в результате лексического анализа, и
синтаксический стек (symbStack),
который в начале выполнения функции должен бьггь пуст.
Результатом функции является:
□ нетерминальный
символ (ссылающийся на корень синтаксического дерева), если разбор был выполнен
успешно;
□ терминальный
символ, ссылающийся на лексему, где была обнаружена ошибка, если разбор выполнен
с ошибками.
Функция Bui 1 dSyntLi st моделирует алгоритм
«сдвиг-свертка» для грамматик операторного предшествования
так, как он был описан в разделе «Краткие теоретические сведения».
Текст программы распознавателя
Кроме перечисленных
выше модулей необходим еще модуль, обеспечивающий интерфейс
с пользователем. Этот модуль (FormLab3)
реализует графическое окно TLab3Form на основе класса TForm библиотеки VCL и включает в себя две
составляющие:
Q
файл программного кода (файл FormLab3.pas);
Q файл описания ресурсов пользовательского
интерфейса (файл FormLab3.dfm).
Модуль FormLab3 построен на основе
модуля FormLab2,
который использовался для реализации интерфейса с пользователем
в лабораторной работе № 2. Он содержит все данные, управляющие
и интерфейсные элементы, которые были использованы в
лабораторной работе № 2, поскольку первым этапом лабораторной работы
№ 3 является лексический анализ, который выполняется модулями, созданными для лабораторной
работы № 2.
Кроме данных,
используемых для выполнения лексического анализа так, как это было
описано.в лабораторной работе № 2, модуль содержит
поле symbStack,
которое представляет собой синтаксический стек, используемый
для выполнения синтаксического анализа. Этот стек инициализируется при
создании интерфейсной формы и уничтожается при ее закрытии. Он также очищается
всякий раз, когда запускаются процедуры лексического и синтаксического анализа.
Кроме органов управления, использованных в лабораторной работе № 2,
интерфейсная форма, описанная в модуле FormLab3, содержит органы
управления для синтаксического
анализатора лабораторной работы № 3:
□ в
многостраничной вкладке (PageControl
1) появилась новая закладка (SheetSynt) под названием
«Синтаксис»;
□ на
закладке SheetSynt
расположен интерфейсный элемент для просмотра иерархических структур (TreeSynt
типа TTreeVi ew).
Внешний вид новой
закладки интерфейсной формы TLab3Form приведен на рис. 3.3. Чтение
содержимого входного файла организовано точно так же, как в лабораторной работе № 2.
После чтения файла
выполняется лексический анализ, как это было описано в лабораторной работе № 2.
Если лексический анализ выполнен успешно, то в список
лексем 1 i stLex добавляется
информационная лексема, обозначающая конец строки, после чего вызывается
функция выполнения синтаксического анализа BuildSyntList, на вход которой
подаются список лексем (listLex)
и синтаксический стек (symbStack).
Результат выполнения функции запоминается во временной
переменной symbRes. Если
переменная symbRes
содержит ссылку на лексему, это значит, что синтаксический
анализ выполнен с ошибками и эта лексема как раз указывает на
то местоя где была
обнаружена ошибка. Тогда список строк входного файла позиционируется на
указанное место ошибки, а пользователю выдается сообщение об ошибке Иначе,
если ошибок не обнаружено, переменная symbRes указывает на корень построенного
синтаксического дерева. Тогда в интерфейсный элемент TreeSynt записывается
ссылка на корень синтаксического дерева, после чего все дерево отог! бражается на экране с
помощью функции MakeTree.
Функция MakeTree обеспечивает рекурсивное
отображение синтаксического дерева в интерфейсном элементе
типа TTreeView.
Элемент типа TTreeView
является стандартным интерфейсным элементом в ОС типа Windows для отображения иерархических
структур (например он используется для отображения
файловой структур.
Н§Лс5- |
|
..............................
■■■■■■■"'............. ...;.■„.. |
|
|
Исходный
файл J
Таблица лексем |
Синтаксис |
|||
йГ
~
' |
|
\Ш |
||
В- Е |
|
|
|
|
1 ! !-- if |
|
|
|
|
1 ! 6-Е |
|
|
|
|
1 Ф'Е |
|
|
|
|
|
:.... а |
|
|
|
|
or |
|
|
|
! Й-Е |
|
|
|
|
| L..22 |
|
|
|
|
1 ; then |
■ |
|
|
|
1 |
В-Е |
|
|
|
|
|
if |
|
|
|
Й-Е |
|
|
|
|
|
б- Е |
|
|
|
|
[ ф-Е |
|
|
|
|
| !- а |
|
|
|
|
: ОГ |
|
|
|
|
] Й-Е |
|
|
|
|
L. ь |
|
|
|
|
;■■•■
or |
|
|
|
| Й-Е |
|
ш |
|
|
-1^г. ::..... i |
||||
|В1ИУ |
. • '■ ' ■
■■■■■■ |
■................................ " |
■ ■
■...•■■■.■ |
|
штиыитпюял |
|
|
_ |
Рис. 3.3. Внешний
вид третьей закладки интерфейсной формы для лабораторной работы № 3
Полный текст
программного кода модуля интерфейса с пользователем и описание
ресурсов пользовательского интерфейса находятся в архиве, находящемся на веб-сайте
издательства, в файлах FormLab3.pas и FormLab3.dfm соответственно.
Полный текст
всех программных модулей, реализующих рассмотренный пример для
лабораторной работы № 3, можно найти в архиве, находящемся на веб-сайте издательства,
в подкаталогах LABS и COMMON (в подкаталог COMMON вынесены те
программные модули, исходный текст которых не зависит от входного языка и
задания по лабораторной работе). Главным файлом проекта является файл LAB3.DPR в подкаталоге LABS. Кроме того, текст модуля
SyntSymb
приведен в листинге
П3.7 в приложении 3.
Выводы по проделанной
работе
В результате лабораторной
работы № 3 построен синтаксический анализатор на основе грамматики
операторного предшествования. Синтаксический анализ позволяет
проверять соответствие структуры исходного текста заданной грамматике
входного языка. Синтаксический анализ позволяет обнаруживать любые синтаксические
ошибки во входной программе. При наличии одной ошибки пользователю
выдается сообщение с указанием местоположения ошибки в исходном тексте.
Анализ типа обнаруженной ошибки не производится. При наличии нескольких
ошибок в исходном тексте обнаруживается только первая из них, после чего дальнейший анализ
не выполняется.
Результатом работы
синтаксического анализатора является структура данных, представляющая
синтаксическое дерево. В комплексе с лексическим анализатором, созданным при
выполнении лабораторной работы № 2, построенный синтаксический анализатор
позволяет выполнять подготовку данных, необходимых для
выполнения следующей лабораторной работы, связанной с генерацией кода.
ЛАБОРАТОРНАЯ
РАБОТА № 4
Генерация
и
оптимизация объектного кода
Цель работы
Цель работы: изучение
основных принципов генерации компилятором объектного кода, ознакомление с
методами оптимизации результирующего объектного кода для линейного участка
программы с помощью свертки и исключения лишних операций.
Краткие теоретические сведения Общие
принципы генерации кода
Генерация объектного кода —
это перевод компилятором внутреннего представления исходной
программы в цепочку символов выходного языка. Поскольку выходным
языком компилятора (в отличие от транслятора) может быть только либо
язык ассемблера, либо язык машинных кодов, то генерация кода порождает
результирующую объектную программу на языке ассемблера или непосредственно на машинном языке (в
машинных кодах).
Генерация объектного кода
выполняется после того, как выполнены лексический и
синтаксический анализ программы и все необходимые действия по подготовке к
генерации кода: проверены семантические соглашения входного языка (семантический
анализ), выполнена идентификация имен переменных и функций, распределено
адресное пространство под функции и переменные и т. д. В
данной лабораторной работе используется предельно простой входной язык, - поэтому нет необходимости
выполнять все перечисленные преобразования. Будем
считать, что все они уже выполнены. Более подробно все эти фазы компиляции
описаны в [1-4,7], а здесь речь будет идти только о самых примитивных приемах
семантического анализа, которые будут проиллюстрированы на примере: выполнения лабораторной
работы.
Внутреннее представление программы может иметь любую
структуру в зависимости от реализации компилятора, в то время как
результирующая программа J всегда представляет собой линейную последовательность
команд. Поэтому генерация объектного кода (объектной программы) в любом
случае должна выполнять действия, связанные с преобразованием сложных
синтаксических структур ' в линейные цепочки.
Генерацию кода можно считать функцией, определенной на
синтаксическом дереве, построенном в результате синтаксического анализа,
и на информации, содержащейся в таблице идентификаторов. Характер
отображения входной программы в последовательность команд,
выполняемого генерацией, зависит от входного языка, архитектуры
целевой вычислительной системы, на которую
ориентировав на результирующая программа, а также от качества желаемого
объектного кода. ;
В идеале компилятор должен выполнить синтаксический
анализ всей входной программы, затем провести ее семантический анализ, после
чего приступать к подготовке генерации и непосредственно генерации кода. Однако
такая схема работы компилятора практически почти никогда не применяется.
Дело в том, что в общем случае ни один семантический анализатор и ни один
компилятор не способны про-.; анализировать
и оценить смысл всей исходной программы в целом. Формальные методы
анализа семантики применимы только к очень незначительной части возможных
исходных программ. Поэтому у компилятора нет практической возможности
порождать эквивалентную результирующую программу на основе всей исходной программы.
Как правило,
компилятор выполняет генерацию результирующего кода поэтапно,:
на основе законченных синтаксических конструкций входной программы.
Компилятор выделяет законченную синтаксическую конструкцию из
текста исходной программы, порождает для нее фрагмент результирующего
кода и помещает в
текст результирующей программы. Затем он переходит к следующей синтаксис ческой
конструкции. Так продолжается до тех пор, пока не будет разобрана вся исходная
программа. В качестве анализируемых законченных синтаксических конструкций
выступают блоки операторов, описания процедур и функций. Их конкретный
состав зависит от входного языка и реализации компилятора.
Смысл (семантику)
каждой такой синтаксической конструкции входного языка можно определить,
исходя из ее типа, а тин определяется синтаксическим анализатором
на основе грамматики входного языка. Примерами типов синтаксических
конструкций могут служить операторы цикла, условные операторы, операторы
выбора и т. д. Одни и те же типы синтаксических конструкций характерны
для различных языков программирования, при этом они различаются синтаксисом
(который задается грамматикой языка), но имеют схожий смысл (который
определяется семантикой). В зависимости от типа синтаксической конструкции
выполняется генерация кода результирующей программы, соответствующего
данной синтаксической конструкции. Для семантически схожих конструкций
различных входных языков программирования может порождаться типовой
результирующий код.
Синтаксически
управляемый перевод
Чтобы компилятор
мог построить код результирующей программы для синтаксической
конструкции входного языка, часто используется метод, называемый синтаксически
управляемым переводом — СУ-переводом.
Идея СУ-перевода основана
на том, что синтаксис и семантика языка взаимосвязаны. Это значит, что
смысл предложения языка зависит от синтаксической структуры
этого предложения. Теория синтаксически управляемого перевода была предложена
американским лингвистом Ноамом Хомским. Она справедлива как для
формальных языков, так и для языков естественного общения: например, смысл
предложения русского языка зависит от входящих в него частей речи (подлежащего,
сказуемого, дополнений и др.) и от взаимосвязи между ними. Однако естественные
языки допускают неоднозначности в грамматиках — отсюда происходят
различные двусмысленные фразы, значение которых человек обычно понимает
из того контекста, в котором эти фразы встречаются (и то он не всегда может
это сделать). В языках программирования неоднозначности в грамматиках
исключены, поэтому любое предложение языка имеет четко определенную структуру
и однозначный смысл, напрямую связанный с этой структурой. Входной
язык компилятора имеет бесконечное множество допустимых предложений,
поэтому невозможно задать смысл каждого предложения. Но все входные предложения
строятся на основе конечного множества правил грамматики, которые
всегда можно найти. Так как этих правил конечное число, то для каждого правила можно определить
его семантику (значение).
Но абсолютно то же самое
можно утверждать и для выходного языка компилятора. Выходной язык содержит
бесконечное множество допустимых предложений, но все они строятся на основе
конечного множества известных правил, каждое из которых имеет
определенную семантику (смысл). Если по отношению к исходной
программе компилятор выступает в роли распознавателя, то для результирующей
программы он является генератором предложений выходного языка. Задача
заключается в том, чтобы найти порядок правил выходного языка, по которым необходимо выполнить
генерацию.
Грубо говоря, идея
СУ-перевода заключается в том, что каждому правилу входного
языка компилятора сопоставляется одно или несколько (или ни одного) правил
выходного языка в соответствии с семантикой входных и выходных правил. То
есть при сопоставлении надо выбирать правила выходного языка, которые несут тот же смысл, что и
правила входного языка.
СУ-перевод
— это основной метод порождения кода результирующей
программ
на
основании результатов синтаксического анализа. Для удобства понимания суп?
метода
можно считать, что результат синтаксического анализа представлен в виде дерева
синтаксического анализа, хотя в реальных компиляторах это не всегда так.
Суть
принципа СУ-перевода заключается в следующем: с каждой вершине
дерева синтаксического
разбора N связывается цепочка некоторого промежуточного кода C(N). Код для вершины N строится путем сцепления
конкатенации в фиксированном порядке последовательности кода C(N) и последовательности кодов, связанных со всеми вершинами,
являющимися прямыми потомками. В свою очередь, для построения
последовательностей кода прямых потомков вершины Л/'потребуется найти
последовательности кода для их потомков — потомков второго уровня вершины N
— и т. д. Процесс перевода идет, таким образом
снизу вверх в строго установленном порядке, определяемом структурой дерева.
Для того
чтобы построить СУ-перевод по заданному дереву синтаксического рабора,
необходимо найти последовательность кода для корня дерева. Поэтому для каждой
вершины дерева порождаемую цепочку кода надо выбирать таким образом, чтобы код,
приписываемый корню дерева, оказался искомым кодом для всего оператора,
представленного этим деревом. В общем случае необходимо имеет единообразную
интерпретацию кода C(N), которая бы встречалась во всех ситуациях, где присутствует
вершина N. В принципе, эта задача может оказаться в тривиальной, так как
требует оценки смысла (семантики) каждой вершины дерева. При применении
СУ-перевода задача оценки смысловой нагрузки для каждой вершины дерева решается
разработчиком компилятора.
Возможна
модель компилятора, в которой синтаксический анализ исходной программы и
генерация кода результирующей программы объединены в одну фа!
Такую
модель можно представить в виде компилятора, у которого операции
кода
совмещены с операциями выполнения синтаксического разбора. Дописания
компиляторов такого типа часто используется термин СУ-компиля1
(синтаксически
управляемая компиляция).
Схему
СУ-компиляции можно реализовать не для всякого входного языка программирования.
Если принцип СУ-перевода применим ко всем входным КС-жанрам,
то применить СУ-компиляцию оказывается не всегда возможным [1, ° В процессе
СУ-перевода и СУ-компиляции не только вырабатываются цепочка текста выходного
языка, но и совершаются некоторые дополнительные действия выполняемые самим
компилятором. В общем случае схемы СУ-перевода не предусматривать
выполнение следующих действий: Q
помещение в выходной поток данных машинных кодов или команд ассемблера,
представляющих собой результат работы (выход) компилятора;
□
выдача
пользователю сообщений об обнаруженных ошибках и предупреждениях (которые
должны помещаться в выходной поток, отличный от поте используемого
для команд результирующей программы);
□
порождение
и выполнение команд, указывающих, что некоторые действия должны быть
произведены самим компилятором (например операции,
выполняемые над данными, размещенными в таблице идентификаторов).
Ниже рассмотрены некоторые основные технические вопросы,
позволяющие ревизовать схемы СУ-перевода для данной лабораторной работы. Более
подробно с механизмами СУ-перевода и СУ-компиляции можно
ознакомиться в [1, 2, 7],
Способы внутреннего
представления программ
Результатом
работы синтаксического анализатора на основе КС-грамматики входного языка
является последовательность правил грамматики, примененных для построения
входной цепочки. По найденной последовательности, зная тип распознавателя,
можно построить цепочку вывода или дерево вывода. В этом случае дерево вывода
выступает в качестве дерева синтаксического разбора и представляет собой
результат работы синтаксического анализатора в компиляторе.
Однако ни цепочка вывода, ни дерево синтаксического разбора
не являются целью работы компилятора. Для полного представления о структуре
разобранной синтаксической конструкции входного языка в принципе достаточно
знать последовательность номеров правил грамматики, примененных для ее
построения. Однако форма представления этой информации может быть различной в зависимости как от реализации самого компилятора, так и от
фазы компиляции. Эта форма называется внутренним представлением программы (иногда
используются также термины промежуточное представление или промежуточная
программа).
Все внутренние представления программы обычно содержат в
себе два принципиально различных элемента — операторы и операнды. Различия
между формами внутреннего представления заключаются лишь в том, как операторы
и операнды соединяются между собой. Также операторы и операнды должны отличаться
друг от друга, если они встречаются в любом порядке. За различение операндов и
операторов, как уже было сказано выше, отвечает разработчик компилятора, который
руководствуется семантикой входного языка.
Известны следующие формы внутреннего
представления программ1:
□ структуры связных списков, представляющие
синтаксические деревья;
□ многоадресный
код с явно именуемым результатом (тетрады);
□ многоадресный код с неявно
именуемым результатом (триады);
□ обратная (постфиксная) польская
запись операций;
□
ассемблерный код или машинные команды.
В каждом конкретном компиляторе может использоваться одна из
этих форм, выбранная разработчиками. Но чаще всего компилятор не ограничивается
использованием только одной формы для внутреннего представления программы.
На различных фазах компиляции могут использоваться различные
формы, которые по мере выполнения проходов компилятора преобразуются одна в
другую Некоторые компиляторы, незначительно
оптимизирующие результирующий код генерируют объектный код по мере разбора
исходной программы. В этом случае применяется схема СУ-компиляции, когда фазы
синтаксического разбора, семантического анализа, подготовки и генерации
объектного кода совмещены в одном проходе компилятора. Тогда внутреннее
представление программы существует только условно в виде последовательности
шагов алгоритма разбора. Алгоритмы, предложенные для выполнения данной
лабораторной работы, построены на основе использования формы внутреннего
представления программы в виде триад. Поэтому далее будет рассмотрена именно
эта форма внутреннего представления программы. С остальными формами можно более
подробно познакомиться в [1-3, 7].
Многоадресный
код с неявно именуемым результатом (триады)
Триады представляют собой запись операций в форме из трех
составляющих операция и два операнда. Например, в строковой записи триады могут
иметь вид; <операция>(<операнд1>,<операнд2>). Особенностью триад является то, что
один или оба операнда могут быть ссылками на другую триаду в том случае,
если в качестве операнда данной триады выступает результат выполнения другой
триады Поэтому триады при записи последовательно
нумеруют для удобства указания ссылок одних триад на другие (в реализации
компилятора в качестве ссылок можно использовать не номера триад, а
непосредственно ссылки в виде указателей — тогда при изменении нумерации и
порядка следования триад менять ссылки не требуется).
Например, выражение A:=B-C+D-B-10, записанное в виде триад, будет иметь
вид
1 |
* ( |
В. |
С ) |
2 |
+ ( |
"1. |
D ) |
3 |
* ( |
В, |
10 ) |
4 |
- ( |
"2. |
"3 ) |
5 |
:
= |
( А. |
'4 |
Здесь операции обозначены соответствующими знаками (при этом
присваиванй) также является операцией), а знак А
означает ссылку операнда одной триады на результат другой.
Триады представляют собой линейную последовательность
команд. При вычислении выражения, записанного в форме триад, они вычисляются
одна за другой последовательно. Каждая триада в последовательности вычисляется
так: операция, заданная триадой, выполняется над операндами, а если в качестве
одного v&
операндов (или обоих
операндов) выступает ссылка на другую триаду, то берете! результат вычисления той
триады. Результат вычисления триады нужно сохранять во временной памяти, так
как он может быть затребован последующими триадами. Если какой-то из операндов
в триаде отсутствует (например, если триада представляет собой унарную
операцию), то он может быть опущен или заменен пустым операндом (в зависимости
от принятой формы записи и ее реализации). Порядок вычисления триад может быть
изменен, но только если допустить наличие триад, целенаправленно изменяющих
этот порядок (например, триады, вызывающие безусловный переход на другую
триаду с заданным номером или переход на несколько
шагов вперед или назад при каком-то условии). Триады представляют собой
линейную последовательность, а потому для них несложно написать тривиальный
алгоритм, который будет преобразовывать последовательность триад в
последовательность команд результирующей программы (либо последовательность
команд ассемблера). В этом- их преимущество перед
синтаксическими деревьями. Однако для триад требуется также и алгоритм, отвечающий
за распределение памяти, необходимой для хранения промежуточных результатов
вычисления, так как временные переменные для этой цели не используются (в этом
отличие триад от тетрад).
Триады не зависят от архитектуры вычислительной системы, на
которую ориентирована результирующая программа. Поэтому они представляют собой
машинно-независимую форму внутреннего представления программы.
Триады обладают следующими
преимуществами:
Q являются линейной последовательностью
операций, в отличие от синтаксического дерева, и потому проще преобразуются в
результирующий код;
□
занимают
меньше памяти, чем тетрады, дают больше возможностей по оптимизации программы,
чем обратная польская запись;
□
явно
отражают взаимосвязь операций между собой, что делает их применение удобным,
особенно при оптимизации внутреннего представления программы;
□
промежуточные
результаты вычисления триад могут храниться в регистрах процессора, что удобно
при распределении регистров и выполнении машинно-зависимой оптимизации;
□
по
форме представления находятся ближе к двухадресным машинным командам, чем
другие формы внутреннего представления программ, а именно эти команды более
всего распространены в наборах команд большинства современных компьютеров.
Необходимость создания алгоритма, отвечающего за распределение
памяти для хранения промежуточных результатов, является главным недостатком
триад. Но при грамотном распределении памяти и регистров процессора этот
недостаток может быть обращен на пользу разработчиками компилятора.
Схемы СУ-перевода
Ранее
был описан принцип СУ-перевода, позволяющий получить линейную последовательность
команд результирующей программы или внутреннего представления программы в
компиляторе на основе результатов синтаксического анализа. Теперь построим
вариант алгоритма генерации кода, который получает на входе
дерево
синтаксического разбора и создает по нему последовательность триад (далее —
просто «триады») для линейного участка результирующей программы. Рассмотрим
примеры схем СУ-перевода для бинарных арифметических операций
Эти схемы достаточно просты, и на их основе можно проиллюстрировать, как
выполняется СУ-перевод в компиляторе при генерации кода Для построения триад по
синтаксическому дереву может использоваться простейшая рекурсивная процедура
обхода дерева. Можно использовать и другие методы обхода дерева - важно, чтобы
соблюдался принцип, согласно которому нижележащие операции в дереве всегда
выполняются перед вышележащими операциями (порядок выполнения операций одного
уровня не важен, он не влияет н результат и зависит от
порядка обхода вершин дерева)
Процедура генерации триад по синтаксическому дереву прежде всего должна определить тип узла дерева. Для
бинарных арифметических операций каждый узел: дерева имеет три нижележащие
вершины (левая вершина — первый операнд, средняя вершина — операция и правая
вершина — второй операнд). При этом тип узла дерева соответствует типу
операции, символом которой помечена средняя из нижележащих вершин. После
определения типа узла процедура строит триады дл узла дерева в соответствии с типом
операции
Фактически процедура генерации триад должна
для каждого узла дерева выполнит: конкатенацию
триады, связанной с текущим узлом, и цепочек триад, связанных с нижележащими
узлами. Конкатенация цепочек триад должна выполняться таким об
разом, чтобы триады, связанные с нижележащими узлами,
выполнялись до выполнения операции, связанной с текущим узлом. Причем для
арифметических операций важно, чтобы
триады, связанные с первым операндом, выполнялись раньше, не триады,
связанные со вторым операндом (так как все арифметические операции при
отсутствии скобок и приоритетов выполняются в порядке слева направо). При этом
возможны четыре ситуации:
Q левая и правая вершины указывают на
непосредственный операнд (это можно определить, если у каждой из них есть только
один нижележащий узел, помеченный символом какой-то лексемы — константы или
идентификатора)
□ левая вершина является непосредственным
операндом, а правая указывает на
другую операцию
Q левая вершина указывает на другую
операцию, а правая является непосредственным операндом;
□ обе
вершины указывают на другую операцию
Считаем,
что на вход процедуры порождения триад по синтаксическому дерев) подается
список, в который нужно добавлять триады, и ссылка на узел дерева, который надо
обработать. Тогда
процедура порождения триад для узла синтаксического дерева, связанного с
бинарной арифметической операцией, может выполняться по следующему алгоритму:
1. Проверяется тип левой вершины узла. Если она — простой
операнд, запоминается имя первого операнда, иначе для этой вершины рекурсивно
вызывается процедура порождения триад, построенные ею триады добавляются в
конец общего списка и запоминается номер последней
триады из этого списка как первый операнд.
2.
Проверяется
тип правой вершины узла. Если она — простой операнд, запоминается имя второго
операнда, иначе для этой вершины рекурсивно вызывается процедура порождения
триад, построенные ею триады добавляются в конец общего списка
и запоминается номер последней триады как второй операнд.
3.
В
соответствии с типом средней вершины в конец общего списка добавляется триада,
соответствующая арифметической операции. Ее первым операндом становится
операнд, запомненный на шаге 1, а вторым операндом — операнд, запомненный на
шаге 2.
4.
Процедура
закопчена.
Процедуры такого рода должен создавать разработчик
компилятора, так как только он может сопоставить по смыслу узлы синтаксического
дерева и соответствующие им последовательности триад. Для разных типов узлов
синтаксического дерева могут быть построены разные варианты процедур, которые
будут вызывать друг друга в зависимости от принятого порядка обхода
синтаксического дерева (в описанном выше варианте — рекурсивно).
В рассмотренном примере при порождении кода преднамеренно не
были приняты во внимание многие вопросы, возникающие при построении реальных
компиляторов. Это было сделано для упрощения примера. Например, фрагменты
кода, соответствующие различным узлам дерева, принимают во внимание тип операции,
но никак не учитывают тип операндов. Все эти требования ведут к тому, что в реальном
компиляторе при генерации кода надо принимать во внимание очень многие
особенности, зависящие от семантики входного языка и от используемой формы
внутреннего представления программы. В данной лабораторной работе эти вопросы
не рассматриваются.
Кроме
того, в случае арифметических операций код, порождаемый для узлов синтаксического
дерева, зависит только от типа операции, то есть только от текущего узла
дерева. Такие схемы можно построить для многих операций, но не для всех. Иногда
код, порождаемый для узла дерева, может зависеть от типа вышестоящего узла:
например, код, порождаемый для операторов типа Break и Continue (которые есть в языках
С, C++ и Object
Pascal), зависит от
того, внутри какого цикла они находятся. Тогда при рекурсивном построении кода
по дереву вышестоящий узел, вызывая функцию для нижестоящего узла, должен
передать ей необходимые параметры. Но код, порождаемый для вышестоящего узла,
никогда не должен зависеть от нижестоящих узлов, в противном случае принцип
СУ-перевода неприменим.
Далее
в примере выполнения работы даются варианты схем СУ-перевода для различных
конструкций входного языка, которые могут служить хорошей иллюстрацией
механизма применения этого метода.
Общие принципы
оптимизации кода
Как
уже говорилось, в подавляющем большинстве случаев генерация кода выполняется
компилятором не для всей исходной программы в целом, а последовательно
для отдельных ее конструкций. Для
построения результирующего кода различных
синтаксических конструкций входного
языка используется метод СУ-перевода. Он объединяет цепочки
построенного кода по структуре дерева без учета их взаимосвязей.
Построенный
таким образом код результирующей программы может
содержат] лишние команды и данные. Это снижает эффективность выполнения
результирующей программы. В принципе, компилятор может завершить на этом
генерацию кода, поскольку результирующая программа построена и является
эквивалентной по смыслу (семантике) программе на входном языке. Однако
эффективность результирующей программы важна для ее разработчика, поэтому
большинство с временных компиляторов выполняют еще один этап компиляции —
оптимизацию результирующей программы (или просто «оптимизацию»), чтобы повысить
ее эффективность, насколько это возможно.
Важно
отметить два момента: во-первых, выделение оптимизации в отдельны! этап
генерации кода — это вынужденный шаг. Компилятор вынужден производить
оптимизацию построенного кода, поскольку он не может выполнить семантический
анализ всей входной программы в целом, оценить ее смысл и
исходя на него построить результирующую программу. Во-вторых, оптимизация — это
нет обязательный этап компиляции. Компилятор может вообще не выполнять оптимизацию,
и при этом результирующая программа будет правильной, а сам компилятор будет
полностью выполнять свои функции. Однако практически все современные компиляторы так или иначе выполняют оптимизацию, поскольку
их? разработчики стремятся завоевать хорошие позиции на рынке средств
разработчики программного обеспечения.
Теперь дадим определение понятию
«оптимизация».
Оптимизация
программы — это
обработка, связанная с переупорядочиванием и изменением операций в
компилируемой программе с целью получения более эффективной результирующей
объектной программы. Оптимизация выполняется
на этапах
подготовки к генерации и непосредственно при генерации объектного
кода.
В
качестве показателей эффективности результирующей программы можно пользовать
два критерия: объем памяти, необходимый для выполнения результирующей
программы, и скорость выполнения (быстродействие) программы. Далеко не всегда
удается выполнить оптимизацию так, чтобы она удовлетворяла обоим этим
критериям. Зачастую сокращение необходимого программе объема данных ведет к
уменьшению ее быстродействия, и наоборот. Поэтому для оптимизации обычно
выбирается один из упомянутых критериев. Выбор критерия оптимизации обычно
выполняется в настройках компилятора. Но даже выбрав
критерий оптимизации, в общем случае практически не возможно построить код
результирующей программы, который бы являлся самым быстрым кодом, соответствующим
входной программе. Де, в том, что нет алгоритмического способа нахождения самой
короткой или самой быстрой результирующей программы, эквивалентной заданной
исходной программе. Эта задача в принципе неразрешима. Существуют алгоритмы,
которые можно ускорять сколь угодно много раз для большого числа возможных
входных данных, и при этом для других наборов входных данных они окажутся
неоптимальными [1, 2]. К тому же компилятор обладает весьма ограниченными
средствами анализа семантики всей входной программы в целом. Все, что можно
сделать на этапе оптимизации, — это выполнить над заданной программой
последовательность преобразований в надежде сделать ее более эффективной.
Чтобы оценить эффективность результирующей программы, полученной с помощью того
или иного компилятора, часто прибегают к сравнению ее с эквивалентной
программой (программой, реализующей тот же алгоритм), полученной из исходной
программы, написанной на языке ассемблера. Лучшие оптимизирующие компиляторы
могут получать результирующие объектные программы из сложных исходных программ,
написанных на языках высокого уровня, почти не уступающие по качеству
программам на языке ассемблера. Обычно соотношение эффективности программ,
построенных с помощью компиляторов с языков высокого уровня, и программ,
построенных с помощью ассемблера, составляет 1,1-1,3. То есть объектная
программа, построенная с помощью компилятора с языка высокого уровня, обычно
содержит на 10-30% больше команд, чем эквивалентная ей объектная программа,
построенная с помощью ассемблера, а также выполняется на 10-30% медленнее1.
Это очень неплохие результаты, достигнутые компиляторами с
языков высокого уровня, если сравнить трудозатраты на разработку программ на
языке ассемблера и языке высокого уровня. Далеко не каждую программу можно
реализовать на языке ассемблера в приемлемые сроки (а значит
и выполнить напрямую приведенное выше сравнение можно только для узкого круга
программ). Оптимизацию можно выполнять на любой стадии генерации кода, начиная
от завершения синтаксического разбора и вплоть до последнего этапа, когда
порождается код результирующей программы. Если компилятор использует несколько
различных форм внутреннего представления программы, то каждая из них может быть
подвергнута оптимизации, причем различные формы внутреннего представления
ориентированы на различные методы оптимизации [1-3, 7]. Таким образом,
оптимизация в компиляторе может выполняться несколько раз на этапе генерации
кода.
Принципиально различаются два основных вида оптимизирующих
преобразований:
Q
преобразования исходной программы (в форме ее внутреннего представления
в компиляторе), не зависящие от результирующего объектного
языка; Q преобразования результирующей объектной
программы.
Первый вид преобразований не зависит от архитектуры целевой
вычислитель - Нои системы, на которой будет выполняться результирующая
программа. Обычно он основан на выполнении хорошо известных
и обоснованных математических
и логических преобразований, производимых
над внутренним представлением; программы (некоторые из них будут рассмотрены
ниже).
Второй вид преобразований может зависеть не только от
свойств объектного языка (что очевидно), но и от архитектуры Вычислительной
системы, на которой буч дет выполняться результирующая
программа. Так, например, при оптимизации может учитываться объем кэш-памяти и
методы организации конвейерных one-: раций центрального процессора. В
большинстве случаев эти преобразования сильно зависят от реализации компилятора
и являются «ноу-хау» производителей компилятора. Именно этот тип оптимизирующих
преобразований позволяет cyщественно повысить эффективность
результирующего кода.
Используемые методы оптимизации ни при каких условиях не
должны приводить к изменению «смысла» исходной программы (то есть к таким ситуациям когда результат выполнения программы изменяется
после ее оптимизации). Для преобразований первого вида проблем обычно не
возникает. Преобразования второго вида могут вызывать сложности, поскольку не
все методы оптимизации, используемые создателями компиляторов, могут быть
теоретически обоснованы и доказаны для всех возможных видов исходных программ.
Именно эти преобразования могут повлиять на смысл исходной программы. Поэтому
у современных компиляторов существуют возможности выбора не только общего
критерия оптимизации, но и отдельных методов, которые будут использоваться при
выполнении оптимизации.
Нередко оптимизация ведет к тому, что смысл программы
оказывается не совсем: таким, каким его ожидал увидеть разработчик
программы, но не по причине наличия ошибки в оптимизирующей части компилятора,
а потому, что пользователь не принимал во внимание некоторые аспекты программы,
связанные с оптимизацией. Например, компилятор может исключить из программы
вызов некоторой функции с заранее известным результатом, но если эта функция
имела «побочный эффект» изменяла некоторые значения в глобальной памяти — смысл
программы может измениться. Чаще всего это говорит о плохом стиле
программирования исходной программы. Такие ошибки трудноуловимы, для их
нахождения разработчику программы следует обратить внимание на предупреждения,
выдаваемые семантическим анализатором, или отключить оптимизацию. Применение
оптимизации так же нецелесообразно в процессе отладки исходной программы Методы
преобразования программы зависят от типов синтаксических конструкций исходного
языка. Теоретически разработаны методы оптимизации для многих типовых
конструкций языков программирования.
Оптимизация
может выполняться для следующих типовых синтаксических конструкций:
□
линейных
участков программы;
□
логических
выражений;
□
циклов;
U вызовов процедур и
функций; конструкций входного языка.
Во всех случаях могут использоваться как машинно-зависимые,
так и машинно-независимые методы оптимизации.
В
лабораторной работе используются два машинно-независимых метода оптимизации
линейных участков программы. Поэтому только эти два метода будут рассмотрены
далее. С другими машинно-независимыми методами оптимизации можно более
подробно ознакомиться в [1, 2, 7]. Что касается машинно-зависимых методов, то
они, как правило, редко упоминаются в литературе. Некоторые из них
рассматриваются в технических описаниях компиляторов.
Принципы оптимизации
линейных участков
Линейный участок программы — это выполняемая по порядку
последовательность операций, имеющая один вход и один выход. Чаще всего
линейный участок содержит последовательность вычислений, состоящих из
арифметических операций и операторов присваивания значений переменным.
Любая программа предусматривает выполнение вычислений и
присваивания значений, поэтому линейные участки встречаются в любой программе.
В реальных программах они составляют существенную часть программного кода.
Поэтому для линейных участков разработан широкий спектр методов оптимизации
кода.
Кроме того, характерной особенностью любого линейного участка
является последовательный порядок выполнения операций, входящих в его состав.
Ни одна операция в составе линейного участка программы не может быть пропущена,
ни одна операция не может быть выполнена большее число раз, чем соседние с нею
операции (иначе этот фрагмент программы просто не будет линейным участком). Это
существенно упрощает задачу оптимизации линейных участков программ. Поскольку
все операции линейного участка выполняются последовательно, их можно
пронумеровать в порядке их выполнения.
Для операций, составляющих линейный участок программы, могут
применяться следующие виды оптимизирующих преобразований;
□
удаление
бесполезных присваиваний;
□
исключение
избыточных вычислений (лишних операций);
□
свертка
операций объектного кода;
□
перестановка
операций;
□
арифметические
преобразования.
Далее
рассмотрены два метода оптимизации линейных участков: исключение лишних
операций и свертка объектного кода.
Свертка объектного кода
Свертка, объектного кода — это выполнение во время компиляции тех операций исходной
программы, для которых значения операндов уже известны. Нет необходимости
многократно выполнять эти операции в результирующей программе — вполне
достаточно один раз выполнить их при компиляции.
Простейший вариант свертки — выполнение компиляторе
операций, операциями которых являются константы. Несколько более сложен процесс
определение тех операций, значения которых могут быть известны в результате
выполнения других операций. Для этой цели при оптимизации линейных участков
программ мы используется специальный алгоритм свертки объектного кода. Алгоритм
свертки для линейного участка программы работает со специальной таблицей Т,
которая содержит пары (<переменная>,<константа>) для всех
переменных, значения которых уже известны. Кроме того, алгоритм свертки помечает
f операции во
внутреннем представлении программы, для которых в результат свертки уже не
требуется генерация кода. Так как при выполнении алгоритм свертки учитывается
взаимосвязь операций, то удобной формой представления для него являются триады,
поскольку в других формах представления операции
(таких
как тетрады или команды ассемблера) требуются дополнительные структуры, чтобы
отразить связь результатов одних операций с операндами других.
Рассмотрим выполнение алгоритма свертки
объектного кода для триад. Для г
метки операций, не требующих порождения
объектного кода, будем использовать
триады специального
вида С(К,0).
Алгоритм свертки триад последовательно
просматривает триады линейного;
стека и для каждой триады делает
следующее:
1.
Если
операнд есть переменная, которая содержится в таблице Т, то операнд заменяется на соответствующее значение константы.
2.
Если
операнд есть ссылка на особую триаду типа С (К, 0), то
операнд заменяется на значение константы К.
3.
Если
все операнды триады являются константами, то триада может быть свергнута. Тогда
данная триада выполняется и вместо нее помещается особая три да вида С(К,0), где К —
константа, являющаяся результатом выполнения свергнутой триады. (При генерации
кода для особой триады объектный код не рождается, а потому она в дальнейшем
может быть просто исключена.)
4. Если
триада является присваиванием типа А:=В, тогда:
■
если В — константа, то А со
значением константы заносится в таблицу Т (ее там уже было старое значение для А, то это старое значение
исключаете!
■
если В — не константа, то А вообще исключается из таблицы Т,
если оно «
есть.
Рассмотрим
пример выполнения алгоритма.
-1
+ 1: =
3; = 6*1 + I: |
Пусть фрагмент исходной программы
(записанной на языке типа Pascal)
имеет bi
+ (1.1)
:= (I. А1)
:= (I. 3) * (6. I)
+ Г4. I) := (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 |
:=(1,*1) |
:=(!, 2) |
:=(!, 2) |
:=(!, 2) |
:=(!, 2) |
:=(!, 2) |
3 |
:=(!, 3) |
:=(1.3) |
:=(!, 3) |
:=(!, 3) |
:=(l,3) |
:= (1. 3) |
4 |
*(6, I) |
* (6, I) |
* (6, I) |
С (18, 0) |
С (18, 0) |
С (18, 0) |
5 |
+ Г4,
I) |
+ ("4, I) |
+ ("4, I) |
+ Г4,
I) |
С (21, 0) |
С (21,0) |
6 |
:= (J, "5) |
:= (J, "5) |
:=(J, л5) |
:=(J,"5) |
:= (J, "5) |
:=(J, 21) |
Т |
(,) |
(I, 2) |
(1,3) |
(I, 3) |
(1,3) |
(I, 3)(J, 21) |
Если исключить особые триады типа С(К,0) (которые не порождают объектного
кода), то в результате выполнения свертки получим следующую последовательность триад: л
1: := (I, 2)
2: := (I. 3) 3: := (J. 21)
Видно, что результирующая последовательность триад может
быть подвергнута Дальнейшей оптимизации — в ней присутствуют лишние
присваивания, но другие методы оптимизации выходят за рамки данной
лабораторной работы (с ними Можно познакомиться в [1, 2, 7]).
Алгоритм свертки объектного кода позволяет исключить из
линейного участка программы операции, для которых на этапе компиляции уже
известен результат. За счет этого сокращается время выполнения1, а также объем кода результирующей программы.
Свертка объектного кода, в принципе, может выполняться не
только для линейных участков программы. Когда операндами являются константы,
логика выполнения программы значения не имеет — свертка может быть выполнена в
любом случае. Если же необходимо учитывать известные значения переменных, то
нужно
принимать
во внимание и логику выполнения результирующей программы. Поэ этому для
нелинейных участков программы (ветвлений и циклов) алгоритм бу
дет более сложным, чем последовательный просмотр линейного списка триад.
Исключение лишних
операций
Исключение
избыточных вычислений (лишних операций) заключается в нахождении и удалении из
объектного кода операций, которые повторно обрабатываю1
одни и те же операнды.
Операция
линейного участка с порядковым номером i считается лишней опера
цией, если существует идентичная ей операция с порядковым номером j, j < i и ни! какой операнд,
обрабатываемый операцией с порядковым номером i, не изменялся никакой другой операцией,
имеющей порядковый номер между i и
j.
Алгоритм исключения лишних операций
просматривает операции в порядке из
следования. Так же как и алгоритму свертки, алгоритму исключения лишних one
раций проще всего работать с триадами, потому что они полностью отражают
взаимосвязь операций. .,
Рассмотрим алгоритм исключения лишних
операций для триад.
Чтобы следить за внутренней зависимостью переменных и триад,
алгоритм присваивает им некоторые значения, называемые числами зависимости, по
следующим правилам:
Q
изначально для
каждой переменной ее число зависимости равно 0, так как в на" чале работы
программы значение переменной не зависит ни от какой триады
□ после
обработки i-й
триады, в которой переменной А присваивается некоторое
значение, число зависимости A (dep(A)) получает значение i, так как значении
А теперь
зависит от данной i-й
триады;
□ при
обработке i-й
триады ее число зависимости (dep(i)) принимается равны,;
значению 1 +
(максимальное_из_чисел_зависимости_операндов).
Таким образом, при использовании чисел зависимости триад и
переменных моя но утверждать, что если 1-я триада идентична j-й триаде (j < i), то i-я триада считается лишней в том и только
в том случае, когда dep(i) = dep( j).
Алгоритм исключения лишних операций использует в своей
работе триады особого вида SAME С j, 0). Если такая триада встречается в позиции с номером i, то это означает,
что в исходной последовательности триад некоторая триада i идентична триаде j.
Алгоритм
исключения лишних операций последовательно просматривает триады линейного
участка. Он состоит из следующих шагов, выполняемых для каждой триады:
1.
Если какой-то операнд триады ссылается на особую триаду
вида SAME( j,0), TO он
заменяется на ссылку на триаду с номером j (Aj).
Вычисляется
число зависимости текущей триады с номером i, исходя из чисел зависимости ее
операндов.
3.
Если
в просмотренной части списка триад существует идентичная j-я триада, причем j< i ndep(i) =dep(j), то текущая триада i заменяется на триаду особого BHflaSAME(j.O).
4.
Если
текущая триада есть присваивание, то вычисляется число зависимости
соответствующей переменной.
Рассмотрим работу алгоритма на примере:
D := D + С*В А := D +
С*В С := D
+ С*В
Этому
фрагменту программы будет соответствовать следующая последовательность триад:
3) *1) л2) 3) В) "8) |
* (С
+ (D
: = (D
* (С + (D := (А
* (С + (D := (С
Видно, что в данном примере некоторые операции вычисляются
дважды над одними и теми же операндами, а значит, они являются лишними и могут
быть исключены. Работа алгоритма исключения лишних операций отражена в табл. 4.2.
Таблица 4.2. Пример работы алгоритма
исключения лишних операций
Обрабатываемая |
Числа зависимости |
|
Числа |
Триады, полученные |
||
триада i |
пере |
ценных |
|
|
зависимости |
после выполнения |
|
А |
В |
с |
D |
триад dep(i) |
алгоритма |
1)*(С, В) |
0 |
0 |
0 |
0 |
1 |
U* (С,
В) |
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) |
Теперь,
если исключить триады особого вида SAME(j,
0), то в результате выполнения алгоритма получим следующую последовательность
триад:
(D, л1) = (А, Ч) -- (С. Ч)
Обратите
внимание, что в итоговой последовательности изменилась нумерация триад и номера
в ссылках одних триад на другие. Если в компиляторе в качестве ссылок
использовать не номера триад, а непосредственно указатели на них, изменения
ссылок в таком варианте не потребуется.
Алгоритм исключения лишних операций позволяет избежать
повторного выполнения одних и тех же операций над одними и теми же операндами.
В результат] оптимизации по этому алгоритму сокращается и время выполнения, и
объем ко; результирующей программы.
Общий алгоритм генерации и оптимизации объектного кода
Теперь рассмотрим общий вариант алгоритма генерации кода, который получав на входе дерево вывода (построенное в
результате синтаксического разбора) и со| дает по нему фрагмент объектного кода
результирующей программы.
Алгоритм
должен выполнить следующую последовательность действий: □ построить последовательность триад на основе
дерева вывода;
Q выполнить оптимизацию кода методом
свертки для линейных участков резу.; тирующей
программы;
Q
выполнить
оптимизацию кода методом исключения лишних операций линейных участков
результирующей программы;
О преобразовать последовательность триад в
последовательность команд на языке ассемблера (полученная последовательность
команд и будет результат выполнения алгоритма).
Алгоритм преобразования триад в команды
языка ассемблера — это единственная машинно-зависимая часть общего алгоритма.
При преобразовании компилятора для работы с другим результирующим объектным
кодом потребуется изменить только эту часть, при этом все алгоритмы оптимизации
и внутреннее выставление программы останутся неизменными. ,
В
данной работе алгоритм преобразования триад в команды языка ассемблер
предлагается разработать самостоятельно. В тривиальном виде такой алгоритм
заменяет каждую триаду на последовательность соответствующих команд, в
результате выполнения запоминается во временной переменной с некоторым и нем (например TMPi,
где i — помер триады).
Тогда вместо ссылки на эту триаду в другой триаде будет подставлено значение
этой переменной. Однако алгоритм может предусматривать и оптимизацию временных
переменных.
Требования
к выполнению работы Порядок выполнения
работы
Для
выполнения лабораторной работы требуется написать программу, которая ;; на основании дерева синтаксического разбора порождает
объектный код и выполняет затем его оптимизацию методом свертки объектного
кода и методом исключения лишних операций. В качестве исходного дерева
синтаксического разбора, рекомендуется взять дерево, которое порождает
программа, построенная по заданию лабораторной работы № 3.
Программу
рекомендуется построить из трех основных частей: первая часть
;
порождение дерева синтаксического разбора (по результатам лабораторной работы
№ 3), вторая часть — реализация алгоритма порождения объектного кода по;/
дереву разбора и третья часть — оптимизация порожденного объектного кода (если
в результирующей программе присутствуют линейные участки кода). Результатом
работы должна быть построенная на основе заданного предложения грамматики
программа на объектном языке или построенная последовательность триад (по
согласованию с преподавателем выбирается форма представления конечного результата).
В
качестве объектного языка предлагается взять язык ассемблера для процессоров
типа late] 80x86 в
реальном режиме (возможен выбор другого объекта нового языка по согласованию с
преподавателем). Все
встречающиеся в исходной программе идентификаторы считать простыми скалярными
переменными, не требующими выполнения преобразования типов. Ограничения на
длину идентификаторов и констант соответствуют требованиям лабораторной работы
№ 3.
1. Получить вариант задания у преподавателя.
2.
Изучить
алгоритм генерации объектного кода по дереву синтаксического разбора.q
3.
Разработать
схемы СУ-перевода для операций исходного языка в
соответствием с заданной грамматикой.
4.
Выполнить
генерацию последовательности триад вручную для выбранного простейшего примера.
Проверить корректность результата.
5.
Изучить
и реализовать (если требуется) для заданного входного языка алгоритмы
оптимизации результирующего кода методом свертки и методом исключения лишних
операций.
6.
Разработать
алгоритм преобразования последовательности триад в заданный объектный код (по
согласованию с преподавателем).
7.
Подготовить
и защитить отчет.
8.
Написать
и отладить программу на ЭВМ.
9.
Сдать
работающую программу преподавателю.
Требования
к оформлению отчета
Отчет
должен содержать следующие разделы: а
Задание по лабораторной работе. а Краткое изложение
цели работы.
□ Запись заданной грамматики входного языка
в форме Бэкуса—Наура.
□
Описание
схем СУ-перевода для операций исходного языка в соответствии с заданной
грамматикой.
□ Пример генерации и оптимизации
последовательности триад на основе простейшей исходной программы.
□
Текст программы (оформляется после выполнения программы на
ЭВМ).
\
it
Основные контрольные
вопросы
□
Что
такое транслятор, компилятор и интерпретатор? Расскажите об обще! структуре компилятора.
□
Как
строится дерево вывода (синтаксического разбора)? Какие исходные данные
необходимы для его построения?
□
Какую
роль выполняет генерация объектного кода? Какие данные необходимы компилятору
для генерации объектного кода? Какие действия выполняет! компилятор перед
генерацией?
□ Объясните, почему генерация объектного
кода выполняется компилятором на отдельным
синтаксическим конструкциям, а не для всей исходной программы в целом.
□
Расскажите,
что такое синтаксически управляемый перевод.
□ Объясните работу алгоритма генерации
последовательности триад по дереву синтаксического разбора на своем примере.
□ За счет чего обеспечивается возможность
генерации кода на разных объектных языках по одному и тому же дереву?
□ :
Дайте определение понятию оптимизации программы. Для чего используете
оптимизация?
Каким условиям должна удовлетворять оптимизация?
□ Объясните, почему генерацию программы
приходится проводить в два этап
генерация и оптимизация.
□ Какие существуют методы оптимизации
объектного кода?
□ Что такое триады и для чего они
используются? Какие еще существуют методы для представления объектных команд?
□
Объясните работу алгоритма свертки. Приведите пример выполнения свертки
объектного кода.
□ Что
такое лишняя операция? Что такое число зависимости?
□
Объясните работу алгоритма исключения лишних операций. Приведите пример
исключения лишних операций.
Варианты заданий
Варианты заданий соответствуют вариантам заданий для
лабораторной работы № 3. Для выполнения работы рекомендуется использовать
результаты, полученные в ходе выполнения лабораторных работ № 2 и 3.
Пример
выполнения работы Задание для примера
В качестве задания для примера возьмем язык, заданный
КС-грамматикой G({if,then,else,a,:=,or,xor,and,(,),;},{5,F,£,AC},P,5) с правилами Р:
F—> if fthen Telse F\ if £then F| a := E
Г—»if
£ then Г else T\
a := E
E^EorD\ExorD\D
D->DandC\C
C->a|(£)
Жирным шрифтом в грамматике и в правилах
выделены терминальные символы.
Этот язык уже был использован для иллюстрации выполнения
лабораторных работ № 2 и № 3.
Результатом примера выполнения лабораторной работы № 4 будет
генератор списка триад. Преобразование списка триад в ассемблерный код
рассмотрено далее в примере выполнения курсовой работы (см. главу «Курсовая
работа»).
Построение
схем СУ-перевода
Все операции, которые могут присутствовать во входной
программе на языке, заданном грамматикой G, по смыслу (семантике) можно разделить
на следующие группы:
□ логические операции (or, not и and);
□ оператор присваивания;
□ полный условный, оператор (if... then ... else ...) и неполный условный оператор (if... then ...);
□ операции, не несущие смысловой нагрузки,
а служащие только для создания синтаксических конструкций исходной программы (в
данном языке таких операций две: круглые скобки и точка с запятой).
Рассмотрим схемы СУ-перевода для всех
перечисленных групп операций.
СУ-перевод для линейных
операций
Линейной операцией будем
называть такую операцию, для которой порождается код, представляющий собой
линейный участок результирующей программы. Например, рассмотренные ранее
бинарные арифметические операции (см. раздел «Краткие теоретические сведения»)
являются линейными.
В
заданном входном языке логические операции выполняются над целыми десятичными
числами как побитовые операции, то есть они также являются бинарными линейными
операциями. Поэтому для них могут быть использованы те же Я самые схемы
СУ-перевода, что были рассмотрены ранее.
СУ-перевод
для оператора присваивания
Оператор
присваивания также является бинарной логической операцией, поэтому для него
может быть использована соответствующая схема СУ-перевода. Отличие оператора
присваивания от прочих бинарных линейных операций заключается в том, что первым
операндом у него всегда должна быть переменная. Поэтому функция, строящая код
для оператора присваивания, должна проверят тип первого операнда. Эта проверка
представляет собой реализацию простейшее го семантического анализа и в данном
случае необходима, так как присваиваний значений константам не отслеживается на
этапе синтаксического анализа (об этом было сказано в лабораторной работе № 3).
СУ-перевод для условных
операторов
Для
условных операторов генерация кода должна выполняться в следующем по! рядке:
1.
Порождается
блок кода № 1, вычисляющий логическое выражение, находящееся между лексемами if (первая нижележащая вершина) и then (третья нижележащая вершина), —
для этого должна быть рекурсивно вызвана функция! порождения кода для второй
нижележащей вершины.
2.
Порождается
команда условного перехода, которая передает управление в зависимости
от результата вычисления логического выражения:
■
в
начало блока кода № 2, если логическое выражение имеет ненулевое значение;
■
в
начало блока кода № 3 (для полного условного оператора) или в конец оператора
(для неполного условного оператора), если логическое выражение имеет нулевое
значение.
3. Порождается
блок кода № 2, соответствующий операциям после лексемы then
(третья нижележащая
вершина), — для этого должна быть рекурсивно вызвана функция порождения кода
для четвертой нижележащей вершины.
4.
Для
полного условного оператора порождается команда безусловного перехода в конец
оператора.
5.
Для
полного условного оператора порождается блок кода № 3, соответствующий
операциям после лексемы else (пятая
нижележащая вершина), — для этого должна быть рекурсивно вызвана функция
порождения кода для шестой нижележащей вершины.
Схемы
СУ-перевода для полного и неполного условных операторов представлены на рис.
4.1.
Блок кода № 1 между
лексемами if
и then |
Блок
кода № 1
между
лексемами
t |
t |
if и
then
Триада if (i,*)- |
Триада
if (*,•)■
Блок кода № 2 между лексемами then и else |
Триада jmp (1, |
Блок
кода № 3 после лексемы else |
Блок кода № 2 после
лексемы then
Полный условный оператор |
Неполный
условный оператор
|
Рис. 4.1. Схемы
СУ-перевода для условных операторов
Для того чтобы реализовать эти схемы, необходимы два типа
триад: триада условного перехода и триада безусловного перехода.
Эти два типа триад реализуются следующим
образом:
□
1Р(<операнд1>,<операнд2>)
— триада условного перехода;
□
JMP(l,<onepaHfl2>) — триада безусловного перехода.
У триады IF
первый операнд может быть переменной, константой или ссылкой на другую триаду,
второй операнд — всегда ссылка на другую триаду. Триада IF передает управление на триаду,
указанную вторым операндом, если первый операнд равен нулю, иначе управление
передается на следующую триаду.
У
триады JMP первый операнд не
имеет значения (для определенности он всегда будет равен 1), второй операнд —
всегда ссылка на другую триаду. Триада JMP всегда передает управление на триаду,
указанную вторым операндом.
СУ-перевод
для семантически ненагруженных конструкций
Операции, которые не несут никакой смысловой нагрузки, не
требуют построения результирующего кода. Для них не требуется строить схемы
СУ-перевода.
Тем не менее функция генерации
списка триад должна обрабатывать и эти операции. Они должны обрабатываться следующим
образом:
□
для вершины, у которой первая нижележащая вершина — открывающая скобка, вторая нижележащая
вершина — узел дерева (не концевая вершина) и
третья нижележащая вершина — закрывающая скобка, должна рекурсивно вызываться функция порождения кода для второй
нижележащей вершины;
Q для вершины, у
которой первая нижележащая вершина — узел дерева (не концевая
вершина) и вторая нижележащая вершина — точка с запятой, должна рекурсивно
вызываться функция порождения кода для первой нижележащей вершины.
Пример генерации списка
триад
Возьмем
в качестве примера входную цепочку:
if a and Ъ or a
and b and 345 then a := 5 or 4 and 7;
В результате лексического и синтаксического разбора этой
входной цепочки будет построено дерево синтаксического разбора, приведенное на
рис. 4.2.
Этому
дереву будет соответствовать следующая последовательность триад:
1: and (a, b)
2: and (а.
Ь)
3: and П. 345)
4: ог П,
"3) .
5: if ("4. А9)
6: and (4. 7)
7: or (5. %)
8: := (а, V) ■
9: ...
В этой последовательности два линейных участка: от триады
1 до триады 5 и триады
6 до триады 9.
После оптимизации методом свертки объектного кода
получим последовательность
триад:
and (a. b) and (a. b). and Г2, 345) ог ("1. "3)
if Г4. А9) С (4. 0) С (5.
0) : = (а. 5) |
1
2 3 4 5 6 7
Если
удалить триады типа С, то эта последовательность
примет следующий вид:
1: and (a, b)
2: and (a. b)
and Г2. 345) or
П. А3) if Г4. V)
:=
(а. 5)
осле
оптимизации методом исключения лишних операций получим последовательность триад:
and (a. b) same ("1, 0) and П.
345) or П, А3) if (-4. "7) := (а. 5)
Если
удалить триады типа same, то эта последовательность примет следующий
and (a, b) and ("1. 345) or П. А2)
if (А3. ~6)
:= (а. 5)
После применения оптимизации получаем последовательность
из пяти триад. Это j на 37,5% меньше, чем в исходной без применения
оптимизации последовательности, состоявшей из восьми триад.
Следовательно, объем результирующего кода и время его выполнения в
данном случае сократятся примерно на 37,5% (слово «при-
' мерно» указано здесь потому, что разные триады могут
порождать различное количество команд в результирующем коде, а потому
соотношения между количеством триад и между количеством команд
объектного кода могут немного различаться).*:! Можно еще обратить
внимание на то, что алгоритм оптимизации методом исключения лишних операций не
учитывает особенности выполнения логических и
арифметических операций. Методами булевой алгебры последовательность операций «a and b or a and b and 345» можно преобразовать в «я and b» точно
так же как последовательность операций «а-b + я-6-345»
— в «а-/>-346», что было бы эффективней, чем варианты,
которые строит алгоритм оптимизации методом исключения лишних
операций. Но для таких преобразований нужны алгоритмы, ориентированные
на особенности выполнения логических и арифметических операций[1,2,7].
Реализация генератора списка триад Разбиение на модули
Так же, как и для лабораторных работ № 2 и 3, модули,
реализующие генератор списка триад, в лабораторной работе № 4 разделены на две
группы:
□
модули, программный код которых не зависит от входного
языка;
□ модули,
программный код которых зависит от входного языка. В первую группу входят
модули:
□
Triads —
описывает структуры данных для представления триад;
□ TrdOpt —
реализует два алгоритма оптимизации: методом свертки объекта
кода и
методом исключения лишних операций;
□ FormLab4 — описывает интерфейс с
пользователем. Во
вторую группу входят модули:
□
TrdType —
описывает допустимые типы триад и их текстовое представление;
□
TrdMake —
строит список триад па основе дерева синтаксического разбора;
Q TrdCal с — обеспечивает вычисление значений для триад разных
типов при свертке объектного кода. Такое разбиение на модули позволяет использовать
те же самые структуры да пых для
организации нового генератора списка триад при изменении входного языка.
Кроме этих модулей
для реализации лабораторной работы № 4 используются следующие программные
модули:
а ТЫ El em и FncTree — позволяют работать с
комбинированной таблицей идентификаторов (созданы при
выполнении лабораторной работы № 1);
□ LexType, LexElem, и LexAuto — обеспечивают работу
лексического распознавателя (созданы при выполнении
лабораторной работы № 2);
□ SyntRule и SyntSymb— обеспечивают работу
синтаксического распознавателя (созданы при выполнении лабораторной работы № 3).
Кратко опишем
содержание программных модулей, используемых для организации генератора списка
триад.
Модуль описания
допустимых типов триад
Модуль TrdType содержит структуры
данных, которые описывают допустимые типы триад.
Он содержит следующие важные типы данных и переменные: a TTriadType — перечисление всех возможных типов триад;
□ TriadStr — массив строковых
обозначений для всех типов триад;
□ Tri adLi neSet — множество тех триад,
которые являются линейными операциями (оно важно для
оптимизации и для порождения кода).
Модуль описания структур
данных для триад
Модуль Tri ads содержит структуры
данных, которые описывают триады и список триад. Эти структуры
зависят от реализации компилятора, но не зависят от входного языка.
Он
содержит следующие важные структуры данных:
□ TOperand — описывает операнд триады;
□ TTriad — описывает триаду и все
связанные с нею данные;
□ TTriadList — описывает список триад.
Структура TOperand
описывает операнд триады. Она содержит следующие данные:
□ ОрТуре — тип операнда, который может принимать три
значения:
■ 0PC0NST — константа;
■ 0P_VAR — переменная
(идентификатор);
■ 0PJ.INK — ссылка на другую триаду;
□ и
дополнительную информацию по операнду:
■
ConstVal
— значение (для константы);
■
VarLink —
ссылка на таблицу идентификаторов (для переменной);
■
TriadNum —
номер триады (для ссылки на триаду).
Один из вопросов, который
необходимо было решить при реализации операндов триад, состоял в следующем: что
использовать для описания ссылки на триаду
— непосредственно ссылку на тип данных (указатель) или номер триад списке? I
Оба варианта имеют свои преимущества и
недостатки:
а при использовании
указателя легче осуществлять доступ к триаде (не надо выбирать ее из списка),
не надо менять указатели при перемещении триад в списке, но при удалении любой
триады из списка нужно корректно менять все указатели на эту триаду, какие
только есть; О при использовании номера триады легче порождать список триад
по дереву разбора, но при любом перемещении и удалении триад из списка нужно
пересчитывать все номера. Какой вариант выбрать, решает разработчик
компилятора. В данном случае автор выбрал второй вариант (номер триады, а не
указатель на нее), поскольку наглядная иллюстрация алгоритмов оптимизации
требует удаления триад, а перестановка указателей при каждом удалении намного
сложнее, чем изменение номеров (это! недостаток оказался решающим). Но
поскольку в реальном компиляторе не нужна иллюстрировать работу алгоритмов
оптимизации выводом списка триад (достаточна просто не порождать код для триад
с типами С и same), в этом случае указатели, па мнению автора, были бы
предпочтительнее.
Структура
TTriad описывает
триаду и все связанные с ней данные. Она содержа
следующие поля данных:
□ TriadType — тип триады (один из перечисленных в
типе TTriadType в модуль
TrdType);
□ Operands — массив операндов триады (из двух
операндов типа TOperand);
Q Info — дополнительная информация о триаде для алгоритмов
оптимизации; |
О IsLi nked — флаг, сигнализирующий о том, что на
триаду имеется ссылка из др)|
гой триады, обеспечивающей передачу управления (типа IF или JMP). Для хранения дополнительной информации
можно было использовать один и двух подходов: хранить ее непосредственно в
самой триаде или хранить внутри триады только ссылку (указатель), а саму
дополнительную информацию разрешать во внешней структуре данных.
Этот
вопрос уже возникал при выборе метода хранения информации при организации
таблиц идентификаторов в лабораторной работе № 1. Тогда было отдай предпочтение
второму варианту, поскольку характер и размер хранимой информации для каждого
идентификатора был неизвестен. В данном случае известно, что для каждой триады
потребуется хранить информацию, обрабатываемую двумя алгоритмами оптимизации —
алгоритмом свертке объектного кода и алгоритмом исключения липших операций. Оба
эти алгоритм? работают со значениями, которые могут принимать триады — для
заданного входного языка это целые десятичные числа. Для их
хранения достаточно одного очисленного
поля (два алгоритма никогда не выполняются одновременно, а по ^ fofi,y:MovyT, использовать одно и то же
поле данных), Поэтому тут выбран первый вариант и хранимая информация включена
непосредственно в структуру данных триады в виде поля Info.
Флаг наличия ссылки важен для определения границ линейных участков
программы при оптимизации: если на
какую-то триаду есть ссылка из триад типа IF
или JMP, значит,
на нее может быть передано управление. Такая триада является возможной точкой
входа участка программы, а потому — границей линейного участка.
Кроме
перечисленных данных структура TTriad
содержит следующие процедуры и функции:
□
конструктор
Create для создания
триады;
□
функцию
проверки совпадения двух триад IsEqual;
а функцию MakeStri ng, формирующую строковое представление
триады для отображения триад на экране;
а
функции, процедуры и свойства для доступа к данным триады.
Нужно
обратить внимание, что функция проверки совпадения двух триад IsEqual считает триады эквивалентными, если они
имеют один тип и одинаковые операнды. Эта функция нужна для выполнения
алгоритма исключения лишних опера- . ций — она
проверяет первое условие того, что операция является лишней, то есть имеется ли
совпадающая с ней операция. Второе условие (что ни один из операндов не
изменялся между двумя операциями) проверяется с помощью чисел зависимости.
Структура данных TTri adLi st описывает список триад и методы работы с
ним. Как и некоторые списки, рассмотренные ранее (в лабораторных работах № 2 и
3), она построена на основе динамического массива типа TList из библиотеки VCL системы программирования Delphi 5. В этой структуре нет никаких данных
(используются только данные, унаследованные от класса TList), но с ней связаны методы, необходимые
для работы со списком триад:
□
функция
очистки списка триад (Clear)
и деструктор для освобождения памяти при удалении списка триад (Destroy);
□
функция
записи списка триад в текстовом представлении в список строк для отображения
списка триад на экране (WriteToLi st);
□
функция
удаления триады из списка (DelTriad);
□
функция
GetTriad и свойство Triads для доступа к триадам в списке по их порядковому
номеру.
Следует отметить, что функция записи списка триад в список
строк (Wri teToLi st) последовательно вызывает функцию MakeStri ng для записи в список строк каждой триады
из списка триад. Функция удаления триады из списка (DelTriad) освобождает память, занятую удаляемой
триадой, а кроме того, следит за тем, чтобы при
удалении триады флаг метки (IsLi
nked) от удаляемой
триады был корректно переставлен на следующую по списку триаду.
Кроме
трех перечисленных структур данных в модуле Tri ads описана также функция DelTri adTypes, которая выполняет удаление из списка
триад всех триад заданного типа. Эта функция необходима только для наглядной
иллюстрации работы I алгоритмов оптимизации. Для этого надо удалять из списка
триад триады с типа-1 ми С и same, которые не порождают
результирующего кода. Удаление триад из
списка можно выполнить в виде двух вложенных циклов:
□ первый
обеспечивает просмотр всего списка триад;
□ второй
обеспечивает изменение номеров всех ссылок и всех последующих три-1 ад в списке при удалении
какой-либо триады.
'■
Тогда среднее количество просмотров списка триад можно оценить как N + K-N-Nm где N — количество
триад в списке, К — средний процент удаляемых триад. При! хорошей
оптимизации, когда К велико, время
работы функции удаления триад из! списка будет квадратично зависеть от
количества триад. При увеличении объема! результирующей программы (при росте N) это
время будет существенно возраст;
тать.
Поэтому функция удаления триад из списка реализована
другим путем. Она выполняет
два просмотра списка триад:
1. На
первом просмотре подсчитывается количество удаляемых триад и для каждой
триады запоминается, на какую величину изменится ее номер при удалении.
2.
На втором просмотре удаляются те триады, которые должны
быть удалены, а для остальных номера и ссылки меняются на величину, запомненную
при первом
просмотре.
При такой реализации
функции количество просмотров списка триад всегда будет
равно 2ЛГ и обеспечит линейную зависимость времени выполнения функции от
количества триад. Правда, в таком случае функция потребует еще дополнительно
N ячеек памяти для хранения изменений индексов каждой триады, но это
оправдано существенным выигрышем во времени ее выполнения.
Модуль
построения списка триад по дереву синтаксического разбора
Модуль TrdMake
содержит функцию, которая строит список триад на основе дерева
синтаксического разбора. Эта функция работает с типами триад, описанным!
в модуле TrdType,
и со структурами данных, описанными в модуле Triads. Дерев;1
синтаксического разбора описано структурами данных из
модуля SyntSymb,
который был создан при выполнении лабораторной работы № 3.
Функция построения списка триад на основе синтаксического дерева
зависит от входного язык;
а
потому вынесена в отдельный модуль.
Модуль
содержит одну функцию, доступную извне, — MakeTriadList. Входным
данными
этой функции являются:
U symbTop — ссылка на корень синтаксического дерева, по которому
строится сп:
сок триад; Q
1 istTriad —
список, в который должны быть записаны построенные триады,
Результатом выполнения
функции является пустая ссылка, если при построении списка триад не было
обнаружено семантических ошибок, или же ссылка на лексему,
возле которой обнаружена семантическая ошибка, если такая ошибка обнаружена.
Генератор списка триад обнаруживает один вид семантических ошибок — присваивание значения
константе.
Функция MakeTriadList выполняет
построение списка триад, добавляет в конец списка триад завершающую
триаду типа NOP (No Operation — Нет операции), чтобы
корректно обрабатывать ссылки на конец списка триад, а также обеспечивает расстановку флагов IsLinked
для всех триад в списке.
Функция MakeTri adLi st построена на основе
внутренней функции модуля TrdMake — MakeTri adLi stNOP, которая и выполняет
главные действия по порождению списка триад. Эта функция
обрабатывает те же входные данные и имеет такой же результат выполнения, что и
функция MakeTriadList.
Функция MakeTri adLi stNOP реализует схемы
СУ-перевода, которые были рассмотрены выше. Выбор схемы
СУ-перевода происходит по номеру правила остовной грамматики
G', взятого из
текущего нетерминального символа дерева:
□ для
правил 2 и 5 — схема полного условного оператора;
□ для
правила 3 — схема неполного условного оператора;
□ для
правил 4 и 6 — схема оператора присваивания;
□ для
правил 7, 8 и 10 — схема для бинарных линейных операций;
□ для
правила 13 — схема для скобок;
□ в
остальных случаях — схема для точки с запятой.
Функция
MakeTri adLi stNOP содержит две
вспомогательные функции:
□ функцию
MakeOperand
для порождения кода, связанного с дочерним узлом дерева (одним из
операндов);
□ функцию
MakeOperation,
реализующую схему СУ-перевода для бинарных линейных операций в зависимости от типа
операции.
Для построения кода для
нижележащих нетерминальных символов по дереву функция MakeTri adLi stNOP рекурсивно вызывает сама
себя. Этот вызов реализован в функции MakeOperand, если нижележащий узел
является нетерминальным символом, а также напрямую для узлов,
связанных со скобками и с точкой с запятой (как было рассмотрено ранее
при построении схем СУ-перевода).
■
Модуль
вычисления значений триад на этапе компиляции
Модуль TrdCalc содержит функцию, которая
вызывается, когда необходимо вычислить значение триады на этапе компиляции.
Эта функция нужна для алгоритма оптимизации методом свертки объектного кода.
Она зависит от типов триад, которые зависят от входного языка,
поэтому вынесена в отдельный модуль.
Модуль содержит одну
единственную функцию Cal cTri ad, которая предельно проста и в комментариях не
нуждается.
Модуль, реализующий алгоритмы
оптимизации
Модуль TrdOpt реализует два алгоритма оптимизации
списка триад:
□ методом
свертки ооъектного кода;
Q
методом
исключения лишних операций.
Алгоритмы,
реализованные в модуле TrdOpt,
в общем случае не зависят от входного языка, однако они обрабатывают триады
типа «присваивание» (в данной p&f ;1 ализации — TRD_ASSIGN). Кроме того, границы линейных участков,
на которых paботают
эти алгоритмы, зависят от триад условного и безусловного перехода 1 (в дайной
реализации — TRD_IF и TRDJMP). Сами алгоритмы требуют для себя триад
специального типа, которые в данном случае реализованы как TRD_C и TRDSAME. 3 В итоге
реализация алгоритмов оптимизации зависит от следующих типов триаде \
□
триад
присваивания;
□
триад
условного и безусловного перехода;
триад специальных типов.
В общем случае эти типы триад и их реализация зависят от
входного языка триад специальных типов, которые разработчик компилятора может реализовать но своему усмотрению). Но поскольку сложно
представить себе язык программирования, в котором не было бы операций
присваивания, условных и бек;J
условных переходов, можно считать, что в такой реализации модуль TrdOpt 4£. j входного языка не зависит.
Функция вычисления значении триад при
свертке объектного кода, которая показывает явную зависимость от входного
языка, вынесена в отдельный модуль TrdCal с, функция Cal
cTri ad).
Кроме
функций, реализующих алгоритмы оптимизации, модуль TrdOpt содержщ ': две структуры
данных: -
□ TConstlnfo — для хранения информации о значениях
переменных;
О TDepInfo — для хранения информации о числах
зависимости переменных. Ж
Обе эти структуры построены на основе
структуры TAddVarlnfo,
описанной TblElem (этот модуль был создан при выполнении
лабораторной работы i
№ 1), и предназначены для хранения информации, связанной с переменной в таблице
идентификаторов.
Структура TConstlnfo хранит информацию о значении переменной,
если оно известно. Она используется в алгоритме оптимизации методом свертки
объектного
кода.
Структура TDepInfo хранит информацию о числе зависимости
переменной. Он
используется в алгоритме оптимизации методом исключений лишних операций
Каждая из этих структур имеет одно поле, которое и предназначено для
хранен1Щ
информации. Для доступа к этому полю используются виртуальные функции
и связанные с ними свойства, которые переопределяют функции и свойства данных TAddV а г I n f о.
Эти
структуры данных создаются по мере выполнения соответствующих ад;гё ритмов и уничтожаются после завершения их выполнения.
Теперь можно сравнить два подхода к
хранению дополнительной информации*:1
1.
Хранение
информации внутри структур данных (реализовано для триад).
2. Хранение внутри структур данных только
ссылок (указателей), а самой ин формации — во внешних структурах.
"'■■:'■■■
Первый подход имеет
следующие преимущества:
3 доступ к хранимой информации осуществлять
проще и быстрее;
а нет необходимости работать с
динамической памятью, выделять и освобождать ее по мере надобности.
В то же время первый подход имеет ряд
недостатков:
а при хранении разнородной информации оперативная память
расходуется не эффективно, будут появляться неиспользуемые поля данных на
разных ста днях компиляции;
□ обеспечивается меньшая гибкость в обработке
информации.
Второй подход имеет следующие преимущества:
□
можно
хранить разнородную информацию в зависимости от потребностей каждой стадии
компиляции;
□
оперативная
память расходуется только на хранение необходимой информации и только тогда,
когда она действительно используется;
□
обеспечивается
более гибкая обработка информации (например, легко реализуется понятие
«отсутствие данных» в алгоритме оптимизации методом сделки объектного кода
через пустую ссылку nil).
Но и он имеет ряд недостатков:
□ использование ссылок увеличивает время
доступа к хранимой информации
может быть важно при обработке компилятором, больших объемов
данных использование ссылок требует
работы с динамической памятью, выделений и освобождения памяти по мере
использования информации, что расходу! время и ресурсы ОС.
Какой
подход выбрать в каждом конкретном случае, решает разработчик компилятора,
принимая во внимание их достоинства и недостатки. Здесь проиллюстрирована
реализация обоих подходов: первого — для идентификаторов (переменных) в
лабораторных работах № 1 и 4, второго — для триад в лабораторной работе №4.
Почему были выбраны именно эти подходы, было описано ранее №Д& || ."Переменных, и для триад.
Алгоритмы
оптимизации реализованы в модуле TrdOpt в виде двух процедур: Р OptimizeConst — для оптимизации методом свертки
объектного кода;
□ OptimizeSame — для оптимизации методом исключения
лишних операций
Модуль, реализующий
алгоритмы оптимизации
Модуль TrdOpt реализует два алгоритма оптимизации
списка триад:
а методом свертки объектного кода;
□ методом исключения лишних операций.
Алгоритмы,
реализованные в модуле TrdOpt,
в общем случае не зависят от входЯ
ного
языка, однако они обрабатывают триады типа
«присваивание» (в данной pes
ализации
— TRDASSIGN). Кроме того, границы линейных участков,
на которых pal
ботают
эти алгоритмы, зависят от триад условного и безусловного перехода
(в данной реализации — TRD_IF и TRDJMP). Сами алгоритмы требуют для себя трщ
ад специального типа, которые в
данном случае реализованы как TRD_C и TRD_SAM
В итоге реализация алгоритмов оптимизации
зависит от следующих типов триад
Q триад присваивания;
□ триад условного и безусловного перехода;
□ триад специальных типов.
В общем случае эти типы триад и их реализация зависят от
входного языка (кр! ме триад специальных типов, которые разработчик компилятора
может реалиэ! вать по своему усмотрению). Но поскольку сложно представить себе
язык прс граммирования, в котором не было бы операций присваивания, условных и б® условных переходов, можно считать, что в такой
реализации модуль TrdOpt
с входного языка не зависит.
Функция вычисления значений триад при свертке объектного
кода, которая ий ет явную зависимость от входного языка, вынесена в отдельный
модуль (моду; TrdCal
с, функция Cal cTri ad).
Кроме функций, реализующих алгоритмы оптимизации, модуль TrdOpt содеря две структуры данных:
□
TConstlnfo
— для хранения информации о значениях переменных;
□
TDepInfo
— для хранения информации о числах зависимости переменных. Обе эти структуры
построены на основе структуры TAddVarlnfo,
описанной щ дуле TblElera
(этот модуль был создан при выполнении лабораторной рабе № 1), и предназначены
для хранения информации, связанной с переменной в т лице идентификаторов.
Структура
TConstlnfo хранит
информацию о значении переменной, если оной вестно. Она используется в
алгоритме оптимизации методом свертки объектно кода.
Структура TDepInfo
хранит информацию о числе зависимости переменной. :<i используется в алгоритме оптимизации
методом исключения лишних операн Каждая из этих
структур имеет одно поле, которое и предназначено для хранение информации. Для
доступа к этому полю используются виртуальные функци* и
связанные с ними свойства, которые переопределяют функции и свойства .f^ данных TAddVarlnfo.
Эти структуры
данных создаются по мере выполнения соответствующих алгоритмов и уничтожаются
после завершения их выполнения.
Теперь можно сравнить два подхода к
хранению дополнительной информации:
1.
Хранение
информации внутри структур данных (реализовано для триад).
2.
Хранение
внутри структур данных только ссылок (указателей), а самой информации — во
внешних структурах.
Первый подход имеет следующие преимущества:
□ доступ
к хранимой информации осуществлять проще и быстрее;
а нет необходимости работать с
динамической памятью, выделять и освобождать ее по мере надобности.
В то же время первый подход имеет ряд
недостатков:
а при хранении разнородной информации
оперативная память расходуется неэффективно, будут появляться неиспользуемые
поля данных на разных стадиях компиляции;
□ обеспечивается меньшая гибкость в обработке
информации.
Второй подход имеет следующие преимущества:
□
можно
хранить разнородную информацию в зависимости от потребностей на каждой стадии
компиляции;
□
оперативная
память расходуется только на хранение необходимой информации и только тогда,
когда она действительно используется;
□
обеспечивается
более гибкая обработка информации (например, легко реализуется понятие
«отсутствие данных» в алгоритме оптимизации методом свертки объектного кода
через пустую ссылку nil).
Но и он имеет ряд недостатков:
Q использование ссылок увеличивает время
доступа к хранимой информации, что может быть важно при обработке компилятором
больших объемов данных;
Q использование
ссылок требует работы с динамической памятью, выделения и освобождения памяти
по мере использования информации, что расходует время и ресурсы ОС.
Какой
подход выбрать в каждом конкретном случае, решает разработчик компилятора,
принимая во внимание их достоинства и недостатки. Здесь проиллюстрирована
реализация обоих подходов: первого — для идентификаторов (переменных) в
лабораторных работах № 1 и 4, второго — для триад в лабораторной
работ № 4. Почему были выбраны именно эти подходы, было описано ранее и для
Переменных, и для триад.
Алгоритмы оптимизации реализованы в
модуле TrdOpt в виде двух
процедур:
fr OptimizeConst — для оптимизации методом свертки
объектного кода;
Q Opti mizeSame — для оптимизации методом исключения
лишних операций.
Обе процедуры принимают на вход один параметр — список
триад. Все необходимые операции выполняются над этим списком, поэтому
результатом их работ будет тот же самый список, в котором некоторые триады
изменены, а другие на триады
специального вида:
□ С (TRDC) — при оптимизации методом свертки объектного кода;
Q Same (TRDSAME) — при оптимизации
методом исключения лишних операций.
Триады
специального вида можно удалить из общего списка триад с помощью функции
удаления триад заданного типа (DelTriadTypes),
которая была описана в модуле Triads.
В принципе, нет необходимости выполнять это, так как на порождаемый объектный
код эта операция никак не влияет — триады специально вида не порождают никакого
кода, но для иллюстрации работы алгоритмов оптимизации такая операция полезна.
Процедуры OptimizeConst
и OptimizeSame реализуют
алгоритмы оптимизации, которые были описаны в разделе «Краткие теоретические
сведения», поэтому в дополнительных пояснениях не нуждаются.
Можно отметить только, что для хранения информации,
связанной с переменными (значения переменных и числа зависимости переменных),
эти процедуры » пользуют непосредственно таблицу идентификаторов. И в этом
случае проявляются преимущества того, что в триадах в качестве ссылки на
переменную испозуется именно ссылка на таблицу идентификаторов, а не на имя
перемени s Эффективность
прямого обращения в таблицу за требуемым значением намного выше, чем поиск
переменной по ее имени. Это справедливо для любых операций выполняемых
компилятором на этапах подготовки к генерации кода, генерации кода и
оптимизации.
Текст
программы генератора списка триад
Кроме перечисленных модулей необходим еще модуль,
обеспечивающий интерфейс с пользователем. Этот модуль (FormLab4) реализует
графическое ок TLab4Form на основе класса TForm библиотеки VCL и включает в себя две
составляющие
□
файл
программного кода (файл FormLab4.pas);
□
файл
описания ресурсов пользовательского интерфейса (файл FormLab4.dfi
Модуль FormLab4
построен на основе модуля FormLab3,
который использовался реализации интерфейса с пользователем в лабораторной
работе № 3. Он сод' жит все данные, управляющие и интерфейсные элементы,
которые были использованы в лабораторных работах № 2 и 3. Такой
подход оправдан, поскольку главным этапом лабораторной работы № 4 является
лексический анализ, который выполняется модулями, созданными для лабораторной
работы № 2, а вторым этом — синтаксический анализ, который выполняется
модулями, созданными в лабораторной работы № 3.
Кроме
данных, используемых для выполнения лексического и синтаксическое анализа так,
как это было описано в лабораторных работах № 2 и 3, модуль содержит поле listTriad, которое представляет собой список
триад. Этот список инициализируется при создании интерфейсной формы и
уничтожается при ее закрытии. Он также очищается всякий раз, когда запускаются
процедуры лексического и синтаксического анализа.
Кроме органов управления, использованных в лабораторной
работе № 3, интерфейсная форма, описанная в модуле Forntab4, содержит органы управления для
генератора списка триад лабораторной работы № 4:
□
в
многостраничной вкладке (PageControll)
появилась новая закладка (Sheet-Triad) под названием «Триады»;
□
на
закладке SheetTriad
расположены интерфейсные элементы для вывода и просмотра списков триад (группа
с заголовком и список строк для отображения каждого списка триад);
■ GroupTriadAll и ListTriadAll — для отображения полного списка триад,
построенного до применения алгоритмов оптимизации;
■ GroupTriadConst и ListTriadConst — для отображения списка триад, построенного
после оптимизации методом свертки объектного кода;
■ GroupTriadSame и ListTriadSame — для отображения списка триад, построенного
после оптимизации методом исключения лишних операций.
□
на
той же закладке SheetTri ad
расположены два сплиттера для управления размерами Списков триад;
□
на
первой закладке SheetFi I
e («Исходный файл»)
появились два дополнительных органа управления — флажки с двумя состояниями
(«пусто» или «отмечено»):
■ CheckDelC — при установке этого флажка триады типа
С удаляются из списка триад после выполнения оптимизации методом свертки
объектного кода;
■ CheckDel Same — при установке этого флажка триады типа same удаляются из списка
триад после выполнения оптимизации методом исключения лишних операций.
Внешний вид новой
закладки интерфейсной формы TLab4Form приведен на рис. 4.3.
Чтение
содержимого входного файла организовано точно так же, как в лабораторной
работе № 2.
После
чтения файла выполняется лексический анализ, как это было описано в лабораторной
работе № 2, а затем, при успешном выполнении лексического анализа,
синтаксический анализ, как это было описано в лабораторной работе № 3. Если
синтаксический анализ выполнен успешно, полученная в результате его выполнения
переменная symbRes
указывает на корень построенного синтаксического дерева. Тогда, после того как
синтаксическое дерево отобразится на экране с помощью функции MakeTree, вызывается функция построения списка
триад по синтаксическому дереву MakeTri adLi st (из модуля TrdMake). Список триад запоминается в список listTriad, а результат выполнения функции — во
временную переменную lexTmp.