Описание компилятора
Общая схема компиляции
В общем виде процесс перевода текста программы на языке ПАСКАЛЬ в текст программы на языке ассемблера можно представить в виде следующей схемы:

Можно выделить модули компилятора:
- лексический анализатор
Назначение: выделение в тексте программы лексем, исключение комментариев, заполнение таблиц лексем и формирование потока внутреннего представления лексем – токенов.
- менеджер таблиц лексем
Назначение: корректное формирование и заполнение таблиц лексем, использующихся в тексте входной программы. Менеджер таблиц участвует в формировании токенов взаимодействуя с лексическим анализатором.
- синтаксический анализатор
Назначение: реализация алгоритма «перенос-свёртка», и на основе собственных управляющих таблиц осуществление разбор текста программы в SLR(1)грамматике. В ходе выполнения алгоритма формируется внутреннее представление программы в виде потока тетрад. Синтаксический анализатор взаимодействует с менеджером таблиц лексем для определения семантической корректности программы и формирования обобщённой таблицы лексем.
- генератор выходного языка
Назначение: преобразование внутреннего представления программы в виде тетрад в код программы на Ассемблере.
Результат трансляции:код программы на Ассемблере.
Разработка лексического анализатора
Лексический анализ – это первый этап компиляции.Лексический анализ выполняется за один проход по тексту программы и решает следующие задачи:
- игнорирование в тексте программы комментариев;
- выделение в исходном тексте программы лексем и заполнение таблицы лексем;
- формирование токенов, соответствующих исходному тексту программы;
- обнаружение появления недопустимых символов и конструкций в тексте.
Лексический анализатор взаимодействует с менеджером таблиц лексем, чтобы корректно заполнить соответствующие таблицы и сформировать токены. Результаты работы лексического анализатора являются исходными данными для следующего этапа компиляции – синтаксического анализа.
Типы выделяемых лексем
Для того, чтобы упростить работу синтаксического анализатора, исходный текст программы выгодно представить не в виде потока символов (как подготавливает его пользователь), а в виде потока лексем. Каждая лексема может быть описана автоматной грамматикой, их распознавание можно осуществить с помощью конечного детерминированного автомата. Входная программа может содержать следующие виды лексем:
- ключевые слова и идентификаторы;
- целочисленные, вещественные и строковые константы;
- односимвольные и двусимвольные разделители;
- комментарии;
- незначащие символы (пробелы, табуляция, перевод каретки).
Описание ключевых слов и идентификаторов
Идентификатор - последовательность букв, цифр и символа подчёркивания, начинающаяся с буквы или символа подчёркивания. Этому соответствует следующая автоматная грамматика:
identifier letter idrest | underline idrest
idrest letter idrest | underline idrest | digit idrest | eps
Ряд идентификаторов имеет предопределённое значение, и пользователь не может использовать их для именования программы, констант и переменных. Такие идентификаторы называются ключевыми словами. Список ключевых слов входного языка:
program, begin, end, integer, real, boolean, char, string, true, false, stack, push, pop, top, for, to, do, or, and, not, if, then, else, read, write, writeln, const, var.
Предварительное составление таблицы ключевых слов языка позволяет упростить работу лексического анализатора. То есть работа лексического анализатора сводится к выделению в тексте программы произвольного идентификатора, а менеджер таблиц распознаёт среди выделенных идентификаторов ключевые слова.
Примеры идентификаторов:
_2382, hello, sum7, Bond_007
Длина идентификатора потенциально не ограничена.
Описание целочисленных, вещественных и строковых констант
В тексте программы могут быть явно указаны целочисленные (intnum), вещественные(realnum) и строковые константы (strvalue) (общее название таких констант – “литералы”). Для каждого типа литералов представлена своя автоматная грамматика:
intnum digit digits
digits digit digits | eps
realnum digit fullpart
fullpart digit fullpart | period fracpart | e scipart
fracpart digit fracpart | eps
scipart + power | - power
power digit powerrest
powerrest digit powerrest | eps
strvalue quote strrest
strrest symbol strrest | quote
Примеры целочисленных констант:
0, 1234, 777, 5
Примеры вещественных констант:
0.909, 12., 44e+05, 1E-12
Примеры строковых констант:
‘Milk’, ‘’, ‘I&*$@’, ‘happy’
Описание разделителей
Разделители могут обозначать знаки операций в выражениях, значимые символы при определении переменных, констант, операторов и прочих конструкций входного языка. Ниже приведен список возможных разделителей:
- арифметические:+ - * /
- знаки отношения: = <> > >= <= <
- знаки присваивания: :=
- прочие разделители: ; , . : ( )
Описание комментариев и незначащих символов
Так как почти любая программа снабжена теми или иными комментариями, приведем для их описания автоматную грамматику, схожую с грамматикой строковых литералов:
comment figurebracketopen commrest
commrest anysymbol commrest | figurebracketclose
В отличие от строковых литералов, внутри скобок комментария допустимы произвольные символы, включая символы перевода каретки и табуляции. В отличие от всех остальных лексем, комментарии корректно разбираются даже в том случае, когда маркер конца текста встречается раньше, чем закрывающая скобка.
Помимо всех вышеперечисленных лексем, в тексте программы встречаются незначащие символы. К таким символам относятся пробелы, знаки табуляции и переводы каретки. Их расположение в тексте программы должно подчиняться следующим правилам:
- наличие незначащего символа необходимо для разделения лексем только тогда, когда его отсутствие приведет к некорректному считыванию первой лексемы.
Пример:
в записи “boolValue and true” пробелы необходимы, так как их отсутствие приведёт к считыванию лексемы-идентификатора “boolValueandtrue”.
в записи “boolValue := true” пробелы могут быть опущены.
- все незначащие символы равноправны, то есть в позиции, в которой может располагаться пробел, могут располагаться знаки табуляции, и переводы каретки.
- количество незначащих символов не имеет значения, то есть В позиции, в которой может находиться один незначимый символ, может находиться произвольное количество незначимых символов.
Лексический анализатор может выявить в тексте программы единственный тип ошибок: в тексте обнаружен символ, не являющийся началом ни одной из возможных лексем. Некоторые символы (например, #, % или ~) не могут принадлежать ни одной из лексем, в этом случае лексический анализатор распознаёт ошибку “неизвестный символ”.
Диаграмма лексического анализатора
Как упоминалось выше, лексический анализатор описывается автоматными грамматиками и может быть представлен конечным автоматом. Для иллюстрации его работы можно воспользоваться диаграммой переходов конечного автомата, приведённой на схеме. Символом “S” обозначается начальное состояние, символом “F” - конечное состояние, в прямоугольниках расположены семантические процедуры выделения лексем и символы текста программы, вызывающие выполнение той или иной процедуры.

Спецификация лексического анализатора
Так как непосредственная реализация лексического анализатора в виде конечного автомата достаточно громоздка, то возникает необходимость в представлении в памяти функции переходов автомата, в хранении текущего состояния и так далее. Более наглядно реализовать распознавание автоматных грамматик некоторым подобием метода рекурсивного спуска, в котором первые символы входной лексемы определяют процедуру, необходимую для её распознавания.
Лексический анализатор оформлен в виде отдельного класса, заголовок которого приведён ниже:
class CLexAn
{
unsigned m_LineNumber;
static char Delims[];
//----------------------------------------------------------------
std::string m_Buff;
std::string m_Src;
std::string m_ErrBuf;
std::string::iterator m_Iter;
//----------------------------------------------------------------
bool IsDelimiter( char Ch );
//----------------------------------------------------------------
void ToBuffer();
void FlushBuffer( std::string &Target, ENUM_TOKEN_TYPES tot);
//----------------------------------------------------------------
void ExtractComment( std::string &Rcv );
void ExtractString( std::string &Rcv );
void ExtractIdentifier( std::string &Rcv );
void ExtractNumber( std::string &Rcv );
void ExtractDelimiter( std::string &Rcv );
void ExtractColon( std::string &Rcv );
void ExtractPeriod( std::string &Rcv );
void ExtractGreater( std::string &Rcv );
void ExtractLess( std::string &Rcv );
void LookForErrors();
//----------------------------------------------------------------
public:
//----------------------------------------------------------------
void ProcessText( std::string inText , std::string &outText );
//----------------------------------------------------------------
};
Основная работа лексического анализатора описывается в интерфейсной функции ProcessText. Данная функция последовательно считывает символы входного потока и переносит их в буфер до тех пор, пока входной символ может продолжать текущую константу. Таким образом, реализуется логика работы лексического анализатора, который формирует максимально длинные лексемы входного языка. Вспомогательные функции Extract* позволяют разобрать некоторые конкретные типы лексем, вызов функции FlushBuffer осуществляет последовательное заполнение таблиц лексем и формирование выходного потока токенов.
Множество токенов лексического анализатора
Токен в общем виде представляет собой ссылку на лексему в сформированной таблице лексем. Некоторые токены соответствуют ровно одной возможной лексеме (например, токены ключевых слов), некоторые – представляют множество возможных лексем (например, идентификатор или литерал). Ниже приведён полный список токенов, которые формирует лексический анализатор во время прохода по тексту программы.
Таблица 4
|
Токен |
Описание |
|
TKN_AND |
ключевое слово “and” |
|
TKN_ASSIGN |
присваивание |
|
TKN_BEGIN |
ключевое слово “begin” |
|
TKN_BOOLEAN |
ключевое слово “boolean” |
|
TKN_BRACKET_CLOSE |
закрывающая круглая скобка |
|
TKN_BRACKET_OPEN |
открывающая круглая скобка |
|
TKN_CHAR |
ключевое слово “char” |
|
TKN_COLON |
двоеточие |
|
TKN_ZPT |
запятая |
|
TKN_CONST |
ключевое слово “const” |
|
TKN_DO |
ключевое слово “do” |
|
TKN_ELSE |
ключевое слово “else” |
|
TKN_END |
ключевое слово “end” |
|
TKN_FALSE |
ключевое слово “false” |
|
TKN_FOR |
ключевое слово “for” |
|
TKN_ID |
произвольный идентификатор |
|
TKN_IF |
ключевое слово “if” |
|
TKN_INTEGER |
ключевое слово “integer” |
|
TKN_LITER_CHAR |
символьный литерал |
|
TKN_LITER_INT |
целочисленный литерал |
|
TKN_LITER_REAL |
вещественный литерал |
|
TKN_LITER_STRING |
строковый литерал |
|
TKN_MULT |
знак операции приоритета произведения |
|
TKN_NOT |
ключевое слово “not” |
|
TKN_OR |
ключевое слово “or” |
|
TKN_TOCHKA |
точка |
|
TKN_PLUS |
знак операции приоритета сложения |
|
TKN_POP |
ключевое слово “pop” |
|
TKN_PROGRAM |
ключевое слово ‘program” |
|
TKN_PUSH |
ключевое слово “push” |
|
TKN_READ |
ключевое слово “read” |
|
TKN_REAL |
ключевое слово “real” |
|
TKN_REL_EQUAL |
символ равенства |
|
TKN_REL_NOT_EQUAL |
символы неравенства |
|
TKN_ TOCHKA - ZPT |
точка с запятой |
|
TKN_STACK |
ключевое слово “stack” |
|
TKN_STRING |
ключевое слово “string” |
|
TKN_THEN |
ключевое слово “then” |
|
TKN_TO |
ключевое слово “to” |
|
TKN_TOP |
ключевое слово “top” |
|
TKN_TRUE |
ключевое слово “true” |
|
TKN_VAR |
ключевое слово “var” |
|
TKN_WHILE |
ключевое слово “while” |
|
TKN_WRITE |
ключевое слово “write” |
|
TKN_WRITELN |
ключевое слово “writeln” |
Формат используемых таблиц лексем
Для хранения информации о найденных в тексте лексемах, используются следующие типы таблиц:
- таблица ключевых слов;
- таблица “специальных символов”, к которым относятся знаки операций и символы-разделители;
- таблицы литералов: символьных, строковых, целочисленных и вещественных;
- таблица идентификаторов.
Содержание таблиц ключевых слов и специальных символов не зависит от конкретной программы; они инициализируются до начала лексического анализа.
Каждая запись в таблица ключевых слов имеет следующий формат:
-
Лексема
Токен
Формат записей в таблице идентификаторов:
-
Лексема
Тип
Флаг константности
Внутреннее имя
Флаг константности устанавливается на этапе синтаксического анализа всем идентификаторам, обозначающим константу.
Внутреннее имя представляет собой уникальный идентификатор, присваиваемый каждому идентификатору и литералу во время синтаксического анализа.
Формат записей в таблице специальных символов:
-
Лексема
Токен
Операция
Необходимость поля “Операция” обусловлена тем, что одному токену в общем случае может соответствовать несколько операций одного приоритета.
Записи таблиц литералов имеют наиболее простой формат и фактически состоят из одного поля, хранящего значение литерала. Для каждого типа данных существуют своя таблица литералов, поэтому поле типа не требуется. Применения шаблонов позволяет использовать для таблиц литералов разных типов один и тот же код.
Таблицы идентификаторов, ключевых слов и специальных символов основаны на ассоциативных массивах, ключевой информацией для которых является поле “Лексема”. Таким образом, обеспечивается быстрый поиск и доступ к полям на этапе лексического анализа. Также доступ к записям таблиц возможен по номеру записи, что позволяет также быстро обращаться к записям во время синтаксического анализа и кодогенерации.
Представление потока токенов на выходе лексического анализатора
Для представления отдельных токенов и потока токенов в удобном для последующего анализа виде, разработаны классы CToken CTokenStream:
class CToken
{
ENUM_TOKENS m_tok;
unsigned m_index;
unsigned m_line;
public:
CToken(ENUM_TOKENS tok, unsigned index, unsigned line)
: m_tok(tok), m_index(index), m_line(line) { }
operator ENUM_TOKENS() { return m_tok; }
unsigned Index() { return m_index; }
unsigned Line() { return m_line; }
};
Как видно из кода, объект класса CToken хранит в себе название токена (m_tok), положение соответствующей токену записи в одной из таблиц (m_index) и номер строки, в которой был обнаружен данный токен (используется для вывода информации об ошибках).
class CTokenStream
{
public:
typedef std::vector<CToken> container;
typedef container::iterator iterator;
private:
container m_Container;
public:
CTokenStream() {};
iterator begin() { return m_Container.begin(); }
iterator end() { return m_Container.end(); }
bool empty() const { return m_Container.empty(); }
void clear() { m_Container.clear(); }
void Add(CToken &Tok) { m_Container.push_back(Tok); }
};
Класс CTokenStream, как видно из кода, просто инкапсулирует в себе структуру данных, используемую для хранения токенов и организации последовательного доступа к ним.
