Скачиваний:
448
Добавлен:
11.02.2014
Размер:
1.25 Mб
Скачать

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

>

.>

>

<■

>

>

.>

<■

>

(

<•

<■

<■ •>

.>

<■

>

)

.>

>

.>

<•

<■

<■

Теперь на основе исходной грамматики 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;±KHif £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;lKltif|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' во всех правилах

  1. Если соответствующий класс найден, взять за основу для построения распо- | знавателя алгоритм разбора входных цепочек, известный для этого класса, если | найдено несколько классов линейных распознавателей — выбрать из них один \ по своему усмотрению.

  2. Иначе, если соответствующий класс по п. 2 не был найден или же найденный ] класс КС-грамматик не устраивает разработчиков компилятора — попытаться ; выполнить над грамматикой неформальные преобразования с целью подвести">\ ее под интересующий класс КС-грамматик для линейных распознавателей!

и вернуться к п. 2.

  1. Если же ни в п. 3, ни в п. 4 соответствующий распознаватель найти не удалось ' (что для современных языков программирования практически невозможно), необходимо использовать один из универсальных распознавателей.

  2. Определить, в какой форме синтаксический распознаватель будет передавать! результаты своей работы другим фазам компилятора (эта форма называется! внутренним представлением программы в компиляторе).

Реализовать выбранный в п. 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. Проверяется тип левой вершины узла. Если она — простой операнд, запоминается имя первого операнда, иначе для этой вершины рекурсивно вызывается процедура порождения триад, построенные ею триады добавляются в конец общего списка и запоминается номер последней триады из этого списка как первый операнд.

  1. Проверяется тип правой вершины узла. Если она — простой операнд, запомина­ется имя второго операнда, иначе для этой вершины рекурсивно вызывается процедура порождения триад, построенные ею триады добавляются в конец об­щего списка и запоминается номер последней триады как второй операнд.

  2. В соответствии с типом средней вершины в конец общего списка добавляется триада, соответствующая арифметической операции. Ее первым операндом становится операнд, запомненный на шаге 1, а вторым операндом — операнд, запомненный на шаге 2.

  3. Процедура закопчена.

Процедуры такого рода должен создавать разработчик компилятора, так как только он может сопоставить по смыслу узлы синтаксического дерева и соответствую­щие им последовательности триад. Для разных типов узлов синтаксического де­рева могут быть построены разные варианты процедур, которые будут вызывать друг друга в зависимости от принятого порядка обхода синтаксического дерева (в описанном выше варианте — рекурсивно).

В рассмотренном примере при порождении кода преднамеренно не были приня­ты во внимание многие вопросы, возникающие при построении реальных компи­ляторов. Это было сделано для упрощения примера. Например, фрагменты кода, соответствующие различным узлам дерева, принимают во внимание тип опера­ции, но никак не учитывают тип операндов. Все эти требования ведут к тому, что в реальном компиляторе при генерации кода надо принимать во внимание очень многие особенности, зависящие от семантики входного языка и от используемой формы внутреннего представления программы. В данной лабораторной работе эти вопросы не рассматриваются.

Кроме того, в случае арифметических операций код, порождаемый для узлов син­таксического дерева, зависит только от типа операции, то есть только от текущего узла дерева. Такие схемы можно построить для многих операций, но не для всех. Иногда код, порождаемый для узла дерева, может зависеть от типа вышестоящего узла: например, код, порождаемый для операторов типа 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, исходя из чи­сел зависимости ее операндов.

  1. Если в просмотренной части списка триад существует идентичная j-я триада, причем j< i ndep(i) =dep(j), то текущая триада i заменяется на триаду особо­го BHflaSAME(j.O).

  2. Если текущая триада есть присваивание, то вычисляется число зависимости соответствующей переменной.

Рассмотрим работу алгоритма на примере:

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 (третья нижележащая вершина), — для этого должна быть рекурсивно вызвана функция порождения кода для четвертой нижележащей вершины.

  1. Для полного условного оператора порождается команда безусловного перехо­да в конец оператора.

  2. Для полного условного оператора порождается блок кода № 3, соответствую­щий операциям после лексемы else (пятая нижележащая вершина), — для этого должна быть рекурсивно вызвана функция порождения кода для шестой ни­жележащей вершины.

Схемы СУ-перевода для полного и неполного условных операторов представле­ны на рис. 4.1.

Блок кода № 1

между лексемами