Принципы разработки компиляторов

ВВЕДЕНИЕ

компилятор программа грамматика

Компилятор — программный модуль, задачей которого является перевод программы, написанной на одном из языков программирования (исходный язык) в программу на язык ассемблера или язык машинных команд.

Большинство компиляторов переводят программу с некоторого высокоуровневого языка программирования в машинный код, который может быть непосредственно выполнен компьютером.

Целью данной курсовой работы является изучение составных частей, основных принципов построения и функционирования компиляторов, практическое освоение методов построения составных частей компилятора для заданного входного языка.

Курсовая работа заключается в создании отдельных частей компилятора заданного языка.

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

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

В третьей части работы требуется разработать программу, которая на основании таблицы лексем выполняет синтаксический разбор текста по заданной грамматике с построением дерева разбора.

Результатами курсовой работы являются программная реализация заданного компилятора и пояснительная записка, оформленная в соответствии с требованиями стандартов и задания на курсовую работу.

В качестве среды разработка для реализации программы использован язык программирования C++ и среда программирования Visual Studio C++ 2012.

1. ОПИСАНИЕ ВХОДНОГО ЯЗЫКА

Входной язык представляет собой подмножество языка программирования Pascal.

Программа на данном языке может включать в себя символы латиницы, цифры, знак “ _ “, символьные константы, различные операторы. Текст на входном языке содержится в текстовом файле.

Набор идентификаторов организуются в таблицу по методу упорядоченного списка. Необходима возможность осуществления многократного поиска идентификатора в этой таблице. Список идентификаторов считать заданным в виде текстового файла. Длина идентификатора ограничена 32 символами. Он может включать в себя символы кириллицы и латиницы, цифры, знаки “ ^ ” и ” _ ”. Идентификатор не может начинаться с цифры.

Предусмотрены следующие варианты операторов входной программы:

— оператор присваивания (:=);

— зарезервированные слова If, Else, Then, While, Do, Prog, End;

— арифметические операции (+, -, /, *);

— операндами в выражениях могут выступать идентификаторы и константы (один символ, заключенный в одинарные кавычки);

— все идентификаторы должны восприниматься как переменные;

— допускается присутствие комментариев оформленных виде: //комментарий

Для выделения лексем заранее строится конечный автомат.

Данный язык относится к КС-языкам, поэтому может быть описан следующей грамматикой:

<буква>>”A” |….| ”Z” |….| ”a” |….| ”z” |”_”

<арифм.опер.>>”+” | ”-” | ”*” |”/”

<цифра>>”0|”1”|”2”|”3”|”4”|”5”|”6”|”7”|”8”|”9”

< ID >><буква>

|<ID><буква>

|<ID><цифра>

<симв.конст.> >'<буква>’

|'<цифра>’

<операнд>><ID>

|< симв.конст.>

<арифм.выр.>> <операнд><арифм.оп.><операнд>

|<арифм.выр><арифм.оп.><операнд>

|<операнд><арифм.оп.>< арифм.выр >

|<операнд><арифм.выр.><операнд>

<оператор>><оп.цикла>

|< оп.присв>

|<услов.оп>

<оп.присв.>><ID>”:=”<операнд>”;”

|<ID>”:=”<арифм.выр.>”;”

<блок опер.> ><оператор> ”;” <оператор>

|<блок>”;”<оператор>

<тело>>”{“<блок опер>”;}”

<оп.цикла>> “do<тело>while ”(” <арифм.выр.>”)” ”;”

|“do””{“ <оператор> ”}” “while””(” <арифм.выр.>”)””;”

<услов.оп>> if “(”<арифм.выр>“)””then”<тело>”else”<тело>

|if “(” <арифм.выр>“)””then”<тело>

|if “(”<арифм.выр>“)”then”<оператор>”else”<оператор>

|if “(” <арифм.выр>“)””then”<оператор>

|if “(”<арифм.выр>“)””then”<оператор>”else”<тело>

|if “(”<арифм.выр>“)””then”<тело>”else”<оператор>

<прогр.>> “prog”<тело> “end

|“prog”<оператор> “end

Далее, используя эту грамматику по методу сдвиг-свертка, производится проверка входного языка на синтаксические ошибки.

2. ОРГАНИЗАЦИЯ ТАБЛИЦЫ ИДЕНТИФИКАТОРОВ

2.1 Назначение таблицы идентификаторов

Таблица используется на всех стадиях работы компилятора и формируется на этапе лексического анализа.

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

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

2.2 Метод упорядоченного списка

Этот метод является простым методом построения таблиц идентификаторов. Элементы записываются в таблицу в порядке возрастания. Так как упорядочивание таблицы идентификаторов происходит на всех этапах обращения к таблице, то для ее построения можно пользоваться только алгоритмом прямого упорядоченного включения элементов. При добавлении нового элемента в таблицу идентификаторов он сначала добавляется в конец таблицы, а затем идет переупорядочивание элементов таблицы идентификаторов. Эффективным методом для поиска элементов является логарифмический поиск, на каждом шаге которого, число элементов, которые могут содержать искомый элемент, сокращается в два раза. Максимально число сравнений при поиске 1+log2(N).

Схема алгоритма добавления идентификатора представлена на рис. 1

Рисунок 1 — Алгоритм добавления идентификатора

Схема алгоритма бинарного поиска идентификатора представлена на рис. 2

Рисунок 2 — Алгоритм поиска идентификатора

2.3 Результат выполнения программы

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

К положительным качествам метода можно отнести простоту его организации.

3. ПРОЕКТИРОВАНИЕ ЛЕКСИЧЕСКОГО АНАЛИЗАТОРА

3.1 Назначение лексического анализатора

Лексический анализатор (или сканер) — это часть компилятора, которая читает литеры программы на исходном языке и строит из них слова (лексемы) исходного языка. На вход лексического анализатора поступает текст исходной программы, а выходная информация передается для дальнейшей обработки компилятором на этапе синтаксического анализа и разбора.

В основном лексические анализаторы выполняют исключение из текста исходной программы комментариев и незначащих пробелов, а также выделение лексем следующих типов: идентификаторов, строковых, символьных и числовых констант, ключевых (служебных) слов входного языка.

3.2 Граф переходов лексического анализатора

Распознаватель лексем языка для данной грамматики задан конечным детерминированным автоматом, схема которого представлена на рисунках 3, 4 и 5.

Рисунок 3 — Схема распознавателя 1

Рисунок 4 — Схема распознавателя 2

Рисунок 5 — Схема распознавателя 3

Легенда:

V — любой определенный алфавитно-цифровой символ (буквы латинского алфавита, знак «_», десятичные цифры);

V(*) — любой символ кроме перечисленных в скобках;

B — буквы латинского алфавита и знак «_»;

B(*) — любая буква кроме перечисленных в скобках;

Р — пробел, табуляция, перенос строки;

D — недопустимые символы (все кроме перечисленных);

F — сохранение (ID — в таблице идентификаторов; L -в таблице лексем);

e — ошибка;

s — имя лексемы;

Состояния соответствуют:

Н — начальное состояние;

К — конечное состояние;

P1, P2, P3, P4 — состояния, соответствующие ключевому слову “prog”;

En1, En2 — состояния, соответствующие ключевому слову “end”;

I1, I2 — состояния, соответствующие ключевому слову “if”;

E1, E2, E3, E4 — состояния, соответствующие слову “else”;

T1, T2, T3, T4 — состояния, соответствующие слову “then”;

W1, W2, W3, W4, W5 — состояния, соответствующие ключевому слову “while”;

D1, D2 — состояния, соответствующие ключевому слову “do”;

S1, S2, S3 — состояния, соответствующие символьное константе:

A1, A2 — состояния, соответствующие оператору присваивания “:=”;

С1, С2 — комментарий;

Программа, реализованная на основе данного автомата, выполняет лексический анализ текста программы на заданном языке.

3.3 Результат выполнения программы

Результат разбора входных выражений на лексемы представлен на рисунке 6.

Рисунок 6 — Результат работы лексического анализатора (таблица лексем)

Спроектированный лексический анализатор выполняет лексический анализ входного текста в соответствии с заданной грамматикой и порождает таблицу лексем с указанием их типов. Программа выводит также сообщения о наличие во входном тексте ошибок. Этот алгоритм послужит в дальнейшем базой для построения дерева вывода в 3 части курсовой работы.

4. ПОСТРОЕНИЕ СИТАКСИЧЕСКОГО АНАЛИЗАТОРА

4.1 Дерево вывода

Лексический анализатор выделяет в тексте лексемы языка. Полученная после лексического анализа цепочка во второй части программы рассматриваться в соответствии с алгоритмом разбора. После построения цепочки вывода на ее основе строится дерево разбора.

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

Длину идентификаторов и строковых констант считать ограниченной 32 символами.

4.2 Синтаксический анализатор

Перед синтаксическим анализатором стоят две основные задачи: проверить правильность конструкций программы, которая представляется в виде уже выделенных слов входного языка, и преобразовать ее в вид, удобный для дальнейшей семантической (смысловой) обработки и генерации кода. Одним из таких способов представления является дерево синтаксического разбора.

Программирование работы недетерминированного МП-автомата — это сложная задача. Разработанный алгоритм, позволяет для произвольной КС-грамматики определить, принадлежит ли ей заданная входная цепочка (алгоритм Кока-Янгера-Касами).

Доказано, что время работы этого алгоритма пропорционально n3, где n — длина входной цепочки. Для однозначной КС-грамматики при использовании другого алгоритма (алгоритм Эрли) это время пропорционально n2. Подобная зависимость делает эти алгоритмы требовательными к вычислительным ресурсам. На практике и не требуется анализ цепочки произвольного КС-языка — большинство конструкций языков программирования может быть отнесено в один из классов КС-языков, для которых разработаны алгоритмы разбора, линейно зависящие от длины входной цепочки.

КС-языки делятся на классы в соответствии со структурой правил их грамматик. В каждом из классов налагаются дополнительные ограничения на допустимые правила грамматики.

Одним из таких классов является класс грамматик предшествования. Они используются для синтаксического разбора цепочек с помощью алгоритма “сдвиг-свертка”. Выделяют следующие типы грамматик предшествования:

— простого предшествования;

— расширенного предшествования;

— слабого предшествования;

— смешанной стратегии предшествования;

— операторного предшествования.

Алгоритм построения синтаксического анализатора включает следующие этапы:

1) составление правил грамматики языка;

2) выявление множества крайних правых и кайних левых терминальных и нетерминальных символов;

3) построение матрицы предшествования.

Рассмотрим эти этапы более подробно.

4.3 Таблицы предшествования

Множество правил грамматики имеет вид:

<буква>>”A” |….| ”Z” |….| ”a” |….| ”z” |”_”

<арифм.опер.>>”+” | ”-” | ”*” |”/”

<цифра>>”0”|”1”|”2”|”3”|”4”|”5”|”6”|”7”|”8”|”9”

< ID >><буква>

|<ID><буква>

|<ID><цифра>

<симв.конст.> >'<буква>’

|'<цифра>’

<операнд>><ID>

|< симв.конст.>

<арифм.выр.>> <операнд><арифм.оп.><операнд>

|<арифм.выр><арифм.оп.><операнд>

|<операнд><арифм.оп.>< арифм.выр >

<оператор>><оп.цикла>

|< оп.присв>

|<услов.оп>

<оп.присв.>><ID>”:=”<операнд>”;”

|<ID>”:=”<арифм.выр.>”;”

<блок опер.> ><оператор> ”;” <оператор>

|<блок>”;”<оператор>

<тело>>”{“<блок опер>”;}”

<оп.цикла>> “do”<тело>“while” ”(” <арифм.выр.>”)” ”;”

|“do””{“ <оператор> ”}” “while””(” <арифм.выр.>”)””;”

<услов.оп>> if “(”<арифм.выр>“)””then”<тело>”else”<тело>

|if “(” <арифм.выр>“)””then”<тело>

|if “(”<арифм.выр>“)”then”<оператор>”else”<оператор>

|if “(” <арифм.выр>“)””then”<оператор>

|if “(”<арифм.выр>“)””then”<оператор>”else”<тело>

|if “(”<арифм.выр>“)””then”<тело>”else”<оператор>

<прогр.>> “prog”<тело> “end

|“prog”<оператор> “end

Грамматика является грамматикой операторного предшествования, так как она не содержит -правил и правые части правил не содержат смежных нетерминальных символов. Построим множества крайних левых и крайних правых символов L(U), R(U) относительно всех нетерминальных символов грамматики.

Таблица 3.1 — Множества крайних правых и крайних левых символов

Символ (U)

Начало построения

L(U)

R(U)

<элемент>

<число>,ID, <элемент>

<число>,ID

<лев.выр>

<элемент>,<лев.выр>

<элемент>,<число>

<выр>

<лев.выр>

”;”

<сис.уравн>

<сис.уравн>,<выр>

<выр>

На основе полученных множеств построим множества крайних левых и крайних правых терминальных символов Lt(U), Rt(U) относительно всех нетерминальных символов грамматики.

Таблица 3.2 — Множества крайних правых и крайних левых терминальных символов

Символ (U)

Начало построения

L(U)

R(U)

<элемент>

<число>,ID

<число>,ID

<лев.выр>

<число>,ID

<число>,ID

<выр>

<число>,ID

”;”

<сис.уравн>

<число>,ID

”;”

На основе этих множеств и правил грамматики G построим матрицу предшествования грамматики:

Таблица 3.3 — Матрица предшествования исходной грамматики

константа

переменная.

;

=

+

*

/

Константа

<

<

<

<

<

Переменная

<

<

<

<

<

;

<

<

=

<

<

<

+

<

<

*

<

<

/

<

<

На основе матрицы предшествования производится синтаксический анализ методом “сдвиг-свертка” в результате которого формируется матрица коэффициентов для дальнейшего решения методом Гаусса.

5. ГЕНЕРАЦИЯ КОДА

Генерация объектного кода — это перевод компилятором внутреннего представления исходной программы в цепочку символов выходного языка.

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

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

Характер отображения входной программы в последовательность команд, выполняемую генерацией, зависит от входного языка, архитектуры вычислительной системы, на которую ориентирована результирующая программа, а также от качества желаемого объектного кода.

5.1 Общие принципы генерации кода

Задача генератора кода — построение для программы на входном языке эквивалентной машинной программы. Обычно в качестве входа для генератора кода служит некоторое промежуточное представление программы.

Генерация кода включает ряд специфических, относительно независимых подзадач: распределение памяти (в частности, распределение регистров), выбор команд, генерацию объектного (или загрузочного) модуля. Конечно, независимость этих подзадач относительна: например, при выборе команд нельзя не учитывать схему распределения памяти, и, наоборот, схема распределения памяти (регистров, в частности) ведет к генерации той или иной последовательности команд. Однако удобно и практично эти задачи все же разделять, обращая при этом внимание на их взаимодействие.

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

5.2 Основные методы оптимизации

Задача оптимизации кода состоит в создании эффективного (с точки зрения размера памяти и времени выполнения) целевого кода. Желаемая степень оптимизации будет зависеть от обстоятельств. Иногда она не нужна, например, если у программы малое время выполнения, умеренные запросы к памяти и, возможно, малый срок жизни.

Необходимость оптимизации может требоваться для программ с большим временем выполнения либо значительными запросами к памяти и, возможно, с длительным временем существования. Стоимость оптимизации главным образом оценивается в терминах времени компиляции. Некоторые виды оптимизации могут быть дорогостоящими в смысле времени компиляции, другие — сравнительно дешевыми. Обычно более дешевые типы оптимизации всегда стоит осуществлять, а более дорогие — не всегда.

Некоторые компиляторы, в зависимости от требуемой степени оптимизации, могут работать в более чем одном режиме.

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

6. ОПИСАНИЕ ПРОГРАММЫ

#include «stdafx.h»

//Подключаем необходимые заголовочные файлы

#include <iostream>

#include <string>

#include <conio.h>

///////////////////

#include «states.h» //функции переходов автомата

#include «common.h» //вспомогательные функции

///////////////////

//по умолчанию используем пространство имен «std»

using namespace std;

//таким образом делаем переменные видимыми в разных модулях

//extern lexem* idtable[MAXHASH]; //таблица идентификаторов

extern lexem** idtable = NULL;//таблица идентификаторов

extern lexem* lexTableHead = NULL; //указатель на начало (начальный елемент) таблицы лексем

extern lexem* lexTableEnd = NULL; //указатель на конец (последний елемент) таблицы лексем

int row = 0;

int col = 0;

//»главная» функция

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL,»Russian» ); //данная строчка необходима для корректного отображения кириллицы

header(); //выводим «шапку»

string fileName = «c:/test.txt»;

//задаем имя файла

//cout << «Введите путь и имя файла n»;

//cin >> fileName;

//считаем содерживое файла (текст программы) в строку

string programText = readFile(fileName);

initIdTable();

string lexem = «»; //переменная для хранения имени лексемы

STATE currState = sBEGIN; //текущее состояние автомата

//текс программы разберем посимвольно в цикле

for(unsigned int i = 0; i < programText.length(); i++){

char c = toupper(programText[i]); //текущий символ

if(c == ‘n’)

{

row++;

col = 0;

}

switch(currState){

case sBEGIN:

lexem.clear();

currState = beginState(c,lexem);

break;

/////////////////////////////////////////

case sIF1:

currState = if1State(c,lexem);

break;

/////////////////////////////////////////

case sIF2:

currState = if2State(c,lexem);

break;

/////////////////////////////////////////

case sELSE1:

currState = else1State(c,lexem);

break;

/////////////////////////////////////////

case sELSE2:

currState = else2State(c,lexem);

break;

/////////////////////////////////////////

case sELSE3:

currState = else3State(c,lexem);

break;

/////////////////////////////////////////

case sELSE4:

currState = else4State(c,lexem);

break;

/////////////////////////////////////////

case sFOR1:

currState = for1State(c,lexem);

break;

/////////////////////////////////////////

case sFOR2:

currState = for2State(c,lexem);

break;

/////////////////////////////////////////

case sFOR3:

currState = for3State(c,lexem);

break;

/////////////////////////////////////////

case sDO1:

currState = do1State(c,lexem);

break;

/////////////////////////////////////////

case sDO2:

currState = do2State(c,lexem);

break;

/////////////////////////////////////////

case sPROG1:

currState = prog1State(c,lexem);

break;

/////////////////////////////////////////

case sPROG2:

currState = prog2State(c,lexem);

break;

/////////////////////////////////////////

case sPROG3:

currState = prog3State(c,lexem);

break;

/////////////////////////////////////////

case sPROG4:

currState = prog4State(c,lexem);

break;

/////////////////////////////////////////

case sEND1:

currState = end1State(c,lexem);

break;

/////////////////////////////////////////

case sEND2:

currState = end2State(c,lexem);

break;

/////////////////////////////////////////

case sSYMBOL1:

lexem = «‘»;

currState = symbol1State(c,lexem);

break;

/////////////////////////////////////////

case sSYMBOL2:

currState = symbol2State(c,lexem);

break;

/////////////////////////////////////////

case sSYMBOL3:

currState = symbol3State(c,lexem);

break;

/////////////////////////////////////////

case sASSIGN1:

lexem = «:»;

currState = assign1State(c,lexem);

break;

/////////////////////////////////////////

case sASSIGN2:

lexem = «»;

currState = assign2State(c,lexem);

break;

/////////////////////////////////////////

case sCOMMENT1:

lexem = «»;

currState = comment1State(c,lexem);

break;

/////////////////////////////////////////

case sCOMMENT2:

currState = comment2State(c,lexem);

break;

/////////////////////////////////////////

case sIDENT:

currState = idState(c,lexem);

break;

case sNUMBER:

/////////////////////////////////////////

currState = numberState(c,lexem);

break;

}

lexem += c;

col++;

}

//сохраняем таблицы

saveIdentTable();

saveLexTable();

//освободим ресурсы (удалим содержимое таблиц)

clearIdentTable();

clearLexTable();

wcout << endl << L»Для завершения программы нажмите любую клавишу…»;

_getch();//»задержка»

return 0;

}

ЗАКЛЮЧЕНИЕ

В результате выполнения курсовой работы для заданного входного языка были построены отдельные части компилятора.

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

Во второй части работы была написана программа, которая выполняет лексический анализ входного текста и порождает таблицу лексем с указанием их типов и значений.

Третья часть курсовой работы была посвящена разработке программы, которая порождает таблицу лексем и выполняет синтаксический разбор текста с построением дерева разбора.

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

СПИСОК ЛИТЕРАТУРЫ

1. Гордеев А.В. Молчанов Л.Ю. Системное программное обеспечение, — СПб.: Питер. 2002. — 734с.

2. Кампапиец Р.II. Манькоп Е.В., Филатов Н.Е. Системное программирование. Основы построения трансляторов: Учеб. пособие для высших и средних учебных заведений. — СПб.: КОРОНА Принт, 2000. -256 с.

3. Гордеев А.В. Операционные системы: Учебник для вузов.

2-е изд.-СПб.: Питер, 2004. — 416 с.

4. Олифер В.Г., Олифер Н.А. Сетевые операционные системы. — СПб.: Питер. 2002. — 544 с.

5. Брайан Оверленд C++ без страха,- СПб.: Питер. 2005. — 432с.

6. Марченко А.Л. C++ Бархатный путь,- СПб.: Питер. 2005. — 401с.

Нужна похожая работа?

Оставь заявку на бесплатный расчёт

Смотреть все Еще 421 дипломных работ