957 lines
59 KiB
TeX
957 lines
59 KiB
TeX
\documentclass[a4paper,12pt]{article}
|
||
\usepackage{ucs}
|
||
\usepackage{cmap}
|
||
\usepackage[utf8x]{inputenc}
|
||
\usepackage[T2A]{fontenc}
|
||
\usepackage[english,russian]{babel}
|
||
\usepackage[hmargin=2cm,vmargin=2cm]{geometry}
|
||
\usepackage{indentfirst}
|
||
\usepackage{listings}
|
||
\usepackage{syntax}
|
||
\usepackage[section]{placeins}
|
||
|
||
\setlength{\grammarparsep}{10pt plus 1pt minus 1pt}
|
||
\setlength{\grammarindent}{9em}
|
||
|
||
\lstset{
|
||
frame=TB,
|
||
morekeywords={begin,end,if,then,else,fi,while,do,od,write,read}
|
||
}
|
||
|
||
\title{Компилятор \textsc{CMilan}}
|
||
\author{Э. Ф. Аллахвердиев, Д. А. Тимофеев}
|
||
|
||
\begin{document}
|
||
\maketitle
|
||
\tableofcontents
|
||
|
||
\section{Обзор языка Милан}
|
||
|
||
Язык Милан --- учебный язык программирования, описанный в
|
||
учебнике~\cite{karpov05}.
|
||
|
||
Программа на Милане представляет собой последовательность операторов,
|
||
заключенных между ключевыми словами \texttt{begin} и \texttt{end}. Операторы
|
||
отделяются друг от друга точкой с запятой. После последнего оператора в блоке
|
||
точка с запятой не ставится. Компилятор \textsc{CMilan} не учитывает регистр
|
||
символов в именах переменных и ключевых словах.
|
||
|
||
В базовую версию языка Милан входят следующие конструкции: константы,
|
||
идентификаторы, арифметические операции над целыми числами, операторы чтения
|
||
чисел со стандартного ввода и печати чисел на стандартный вывод, оператор
|
||
присваивания, условный оператор, оператор цикла с предусловием.
|
||
|
||
Программа может содержать комментарии, которые могут быть многострочными.
|
||
Комментарий начинается символами `\texttt{/*}' и заканчивается символами
|
||
`\texttt{*/}'. Вложенные комментарии не допускаются.
|
||
|
||
Грамматика языка Милан в расширенной форме Бэкуса-Наура приведена на
|
||
рисунке~\ref{milan-grammar}.
|
||
|
||
\begin{figure}
|
||
\begin{grammar}
|
||
|
||
<program> ::= `begin' <statementList> `end'
|
||
|
||
<statementList> ::= <statement> `;' <statementList>
|
||
\alt $\epsilon$
|
||
|
||
<statement> ::= <ident> `:=' <expression>
|
||
\alt `if' <relation> `then' <statementList> [`else' <statementList>] `fi'
|
||
\alt `while' <relation> `do' <statementList> `od'
|
||
\alt `write' `(' <expression> `)'
|
||
|
||
<expression> ::= <term> \{<addop> <term>\}
|
||
|
||
<term> ::= <factor> \{<mulop> <factor>\}
|
||
|
||
<factor> ::= <ident> | <number> | `(' <expression> `)'
|
||
|
||
<relation> ::= <expression> <cmp> <expression>
|
||
|
||
<addop> ::= `+' | `-'
|
||
|
||
<multop> ::= `*' | `/'
|
||
|
||
<cmp> ::= `=' | `!=' | `<' | `<=' | `>' | `>='
|
||
|
||
<ident> ::= <letter> \{<letter> | <digit>\}
|
||
|
||
<letter> ::= `a' | `b' | `c' | \ldots | `z' | `A' | `B' | `C' | \ldots | `Z'
|
||
|
||
<digit> ::= `0' | `1' | `2' | `3' | `4' | `5' | `6' | `7' | `8' | `9'
|
||
|
||
\end{grammar}
|
||
\label{milan-grammar}
|
||
\caption{Грамматика языка Милан}
|
||
\end{figure}
|
||
|
||
\subsection{Константы}
|
||
|
||
В языке реализована поддержка знаковых целочисленных констант. Синтаксически
|
||
константы представляют собой последовательность цифр, перед которой может
|
||
находиться знак `-'. Примеры констант: \texttt{0}, \texttt{9}, \texttt{1234},
|
||
\texttt{-19}, \texttt{-0}.
|
||
|
||
\subsection{Идентификаторы}
|
||
|
||
Идентификаторы представляют собой последовательность букв латинского алфавита и
|
||
цифр. Первым символом идентификатора должна быть буква. Максимальная длина
|
||
идентификатора ограничена 63 символами. Регистр символов не учитывается.
|
||
Идентификаторы не должны совпадать с ключевыми словами \texttt{begin},
|
||
\texttt{end}, \texttt{if}, \texttt{then}, \texttt{else}, \texttt{fi},
|
||
\texttt{do}, \texttt{od}, \texttt{while}, \texttt{read}, \texttt{write}.
|
||
|
||
Примеры идентификаторов: \texttt{a}, \texttt{X}, \texttt{GCD}, \texttt{Milan23}.
|
||
Цепочки символов, недопустимые в качестве идентификаторов: \texttt{12a} (первый
|
||
символ не является буквой), \texttt{Begin} (цепочка символов совпадает с
|
||
ключевым словом), \texttt{a\_1} (цепочка содержит символ подчеркивания).
|
||
|
||
\subsection{Арифметические выражения}
|
||
|
||
\begin{grammar}
|
||
<expression> ::= <term> \{<addop> <term>\}
|
||
|
||
<term> ::= <factor> \{<mulop> <factor>\}
|
||
|
||
<factor> ::= <ident> | <number> | `(' <expression> `)'
|
||
\end{grammar}
|
||
|
||
Арифметические выражения строятся по традиционным для языков программирования
|
||
правилам. Элементами арифметических выражений могут быть константы,
|
||
идентификаторы, знаки арифметических операций `\texttt{+}' (сложение),
|
||
`\texttt{-}' (вычитание), `\texttt{*}' (умножение), `\texttt{/}'
|
||
(деление), скобки и ключевое слово \texttt{read}, которое обозначает операцию
|
||
чтения числа со стандартного ввода.
|
||
|
||
Примеры правильных выражений: \texttt{12 + 4}, \texttt{i}, \texttt{(x+1)*y},
|
||
\texttt{2*x+1}, \texttt{read}, \texttt{read * (x - read)}.
|
||
|
||
Операции `\texttt{*}' и `\texttt{/}' имеют более высокий приоритет, чем
|
||
операции `\texttt{+}' и `\texttt{-}'. Все эти операции левоассоциативны.
|
||
|
||
Значения арифметических выражений вычисляются слева направо с учетом приоритета
|
||
операций. Порядок вычисления важен из-за присутствия операции \texttt{read},
|
||
благодаря которой вычисление значения выражения имеет побочный эффект.
|
||
|
||
\subsection{Печать чисел на стандартный вывод}
|
||
|
||
\begin{grammar}
|
||
<statement> ::= `write' `(' <expression> `)'
|
||
\end{grammar}
|
||
|
||
Печать чисел осуществляется с помощью оператора \texttt{write}, аргументом
|
||
которого является произвольное арифметическое выражение. Примеры использования
|
||
оператора \texttt{write}: \texttt{write(5)},
|
||
\texttt{write(n+7)}, \texttt{write((2+read)*3)}, \texttt{write(read)}.
|
||
|
||
\subsection{Оператор присваивания}
|
||
|
||
\begin{grammar}
|
||
<statement> ::= <ident> `:=' <expression>
|
||
\end{grammar}
|
||
|
||
При выполнении оператора присваивания сначала вычисляется значение выражения,
|
||
записанного в его правой части. Затем результат записывается в ячейку памяти,
|
||
соответствующую переменной в левой части оператора присваивания.
|
||
|
||
Последовательность символов `\texttt{:=}' является неделимой и не может
|
||
содержать пробелов между символами `\texttt{:}' и `\texttt{=}'.
|
||
|
||
Примеры корректных операторов присваивания: \texttt{a := b + 1}, \texttt{c :=
|
||
read}.
|
||
|
||
\subsection{Условный оператор}
|
||
|
||
\begin{grammar}
|
||
<statement> ::= `if' <relation> `then' <statementList> [`else' <statementList>] `fi'
|
||
|
||
<relation> ::= <expression> <cmp> <expression>
|
||
|
||
<cmp> ::= `=' | `!=' | `<' | `<=' | `>' | `>='
|
||
\end{grammar}
|
||
|
||
Условный оператор включает:
|
||
\begin{enumerate}
|
||
\item условие, которое представляет собой проверку на равенство или неравенство
|
||
двух арифметических выражений,
|
||
\item последовательность операторов, которая должна быть выполнена, если условие
|
||
истинно (блок \texttt{then}),
|
||
\item необязательную последовательность операторов, которая должна быть
|
||
выполнена, если условие ложно (блок \texttt{else}).
|
||
\end{enumerate}
|
||
|
||
Проверка условия $a ? b$, где $a$ и $b$ --- арифметические выражения, а <<$?$>>
|
||
--- один из операторов сравнения, производится следующим образом:
|
||
\begin{enumerate}
|
||
\item вычисляется значение выражения $a$;
|
||
\item вычисляется значение выражения $b$;
|
||
\item проверяется выполнение условия.
|
||
\end{enumerate}
|
||
|
||
Поддерживаются следующие отношения: `\texttt{=}' (<<равно>>), `\texttt{!=}'
|
||
(<<не равно>>), `\texttt{<}' (<<меньше>>), `\texttt{<=}' (<<меньше или равно>>),
|
||
`\texttt{>}' (<<больше>>), `\texttt{>=}' (<<больше или равно>>).
|
||
|
||
Условный оператор завершается ключевым словом \texttt{fi}. Оно используется,
|
||
чтобы избежать известной проблемы <<висячего else>>, которая встречается, в
|
||
частности, в языках C, C++ и Java. Наличие <<закрывающей скобки>> \texttt{fi}
|
||
позволяет однозначно интерпретировать вложенные условные операторы.
|
||
|
||
Примеры использования условного оператора приведены на рисунках~\ref{ifthen} и
|
||
\ref{ifthenelse}. Обе эти программы считывают со стандартного ввода два числа.
|
||
Программа на рисунке~\ref{ifthen} печатает наибольшее из двух чисел. Программа
|
||
на рисунке~\ref{ifthenelse} печатает числа в порядке возрастания.
|
||
|
||
\begin{figure}
|
||
\begin{lstlisting}
|
||
begin
|
||
x := read;
|
||
y := read;
|
||
max := x;
|
||
if y > max then
|
||
max := y
|
||
fi;
|
||
write (max)
|
||
end
|
||
\end{lstlisting}
|
||
\label{ifthen}
|
||
\caption{Пример условного оператора \texttt{if..then..fi}}
|
||
\end{figure}
|
||
|
||
\begin{figure}
|
||
\begin{lstlisting}
|
||
begin
|
||
x := read;
|
||
y := read;
|
||
if x <= y then
|
||
write (x);
|
||
write (y)
|
||
else
|
||
write (y);
|
||
write (x)
|
||
fi
|
||
end
|
||
\end{lstlisting}
|
||
\label{ifthenelse}
|
||
\caption{Пример условного оператора \texttt{if..then..else..fi}}
|
||
\end{figure}
|
||
|
||
\subsection{Цикл с предусловием}
|
||
|
||
\begin{grammar}
|
||
<statement> ::= `while' <relation> `do' <statementList> `od'
|
||
|
||
<relation> ::= <expression> <cmp> <expression>
|
||
|
||
<cmp> ::= `=' | `!=' | `<' | `<=' | `>' | `>='
|
||
\end{grammar}
|
||
|
||
При выполнении цикла с предусловием сначала проверяется, истинно ли условие.
|
||
Если оно истинно, последовательно выполняются операторы, составляющие тело
|
||
цикла. После того, как последний оператор будет выполнен, управление снова
|
||
возвращается к проверке условия. Если условие оказывается ложным, выполнение
|
||
цикла завершается. Вычисление выражений, необходимых для проверки условия,
|
||
производится так же, как и при выполнении условного оператора.
|
||
|
||
Пример программы, использующей цикл, приведен на рисунке~\ref{while}. Эта
|
||
программа считывает со стандартного ввода число и печатает его факториал.
|
||
|
||
\begin{figure}
|
||
\begin{lstlisting}
|
||
begin
|
||
n := read;
|
||
factorial := 1;
|
||
x := 1;
|
||
while x <= n do
|
||
factorial := factorial * x;
|
||
x := x + 1
|
||
od;
|
||
write (factorial)
|
||
end
|
||
\end{lstlisting}
|
||
\label{while}
|
||
\caption{Пример цикла с предусловием}
|
||
\end{figure}
|
||
|
||
\section{Использование компилятора}
|
||
|
||
Компилятор \textsc{CMilan} преобразует программы на языке Милан в
|
||
последовательность команд виртуальной машины Милана. Общее описание виртуальной
|
||
машины можно найти в учебнике~\cite{karpov05}, а более детальное
|
||
описание --- или в документации, прилагаемой к исходным текстам виртуальной
|
||
машины.
|
||
|
||
Исполняемый файл компилятора называется \texttt{cmilan} (в операционных системах
|
||
семейства Unix) или \texttt{cmilan.exe} (в Windows). Компилятор имеет интерфейс
|
||
командной строки. Чтобы скомпилировать программу, записанную в файле
|
||
<<program.mil>>, нужно выполнить команду <<\texttt{cmilan program.mil}>>.
|
||
Результатом работы компилятора является либо последовательность команд для
|
||
виртуальной машины Милана, которая печатается на стандарный вывод, либо набор
|
||
сообщений о синтаксических ошибках, которые выводятся в стандартный поток
|
||
ошибок.
|
||
|
||
При вызове программы \texttt{cmilan} без параметров компилятор печатает краткую
|
||
информацию о порядке его вызова.
|
||
|
||
Чтобы сохранить генерируемую компилятором программу для виртуальной машины в
|
||
файл, достаточно перенаправить в этот файл стандартный поток вывода.
|
||
|
||
На рисунке~\ref{cmilan-usage} приведен пример сеанса работы с компилятором.
|
||
|
||
\begin{figure}
|
||
\begin{verbatim}
|
||
bash$ cat factorial.mil
|
||
BEGIN
|
||
n := READ;
|
||
factorial := 1;
|
||
i := 1;
|
||
WHILE i <= n DO
|
||
factorial := factorial * i;
|
||
i := i + 1
|
||
OD;
|
||
WRITE(factorial)
|
||
END
|
||
bash$ cmilan factorial.mil > factorial.out
|
||
bash$ milanvm factorial.out
|
||
Reading input from factorial.out
|
||
> 5
|
||
120
|
||
\end{verbatim}
|
||
\label{cmilan-usage}
|
||
\caption{Пример использования компилятора \textsc{CMilan}}
|
||
\end{figure}
|
||
|
||
\subsection{Сборка компилятора в Unix}
|
||
|
||
Для сборки компилятора в операционных системах семейства Unix (в частности,
|
||
GNU/Linux и FreeBSD) необходим компилятор C++, в качестве которого рекомендуется
|
||
использовать GCC, и GNU Make. Для сборки компилятора достаточно перейти в
|
||
каталог \texttt{src}, в котором расположены исходные тексты компилятора
|
||
\textsc{CMilan}, и выполнить команду \texttt{make}.
|
||
|
||
\subsection{Сборка компилятора в Windows}
|
||
|
||
Для сборки компилятора \textsc{CMilan} в Windows можно использовать компилятор
|
||
GCC или Microsoft Visual Studio. При использовании GCC (Cygwin, MinGW) сборка
|
||
производится так же, как и в Unix. Для сборки компилятора с помощью Visual
|
||
Studio 2010 в дистрибутив компилятора включены файлы проекта (каталог
|
||
\texttt{src\\cmilan\_vs2011}). Если необходимо использовать другие версии Visual
|
||
Studio, достаточно создать пустой проект консольного приложения Win32 и добавить
|
||
в проект существующие исходные тексты из каталога \texttt{src}.
|
||
|
||
\section{Устройство компилятора}
|
||
|
||
\subsection{Архитектура компилятора}
|
||
Компилятор \textsc{CMilan} включает три компонента:
|
||
\begin{enumerate}
|
||
\item лексический анализатор;
|
||
\item синтаксический анализатор;
|
||
\item генератор команд виртуальной машины Милана.
|
||
\end{enumerate}
|
||
|
||
Лексический анализатор посимвольно читает из входного потока текст программы и преобразует
|
||
группы символов в лексемы (терминальные символы грамматики языка Милан). При
|
||
этом он отслеживает номер текущей строки, который используется синтаксическим
|
||
анализатором при формировании сообщений об ошибках. Также лексический анализатор
|
||
удаляет пробельные символы и комментарии. При формировании лексем анализатор
|
||
идентифицирует ключевые слова и последовательности символов (например, оператор
|
||
присваивания), а также определяет значения числовых констант и имена переменных.
|
||
Эти значения становятся значениями атрибутов, связанных с лексемами.
|
||
|
||
Синтаксический анализатор читает сформированную лексическим анализатором
|
||
последовательность лексем и проверяет ее соответствие грамматике языка Милан.
|
||
Для этого используется метод рекурсивного спуска. Если в процессе
|
||
синтаксического анализа обнаруживается ошибка, анализатор формирует сообщение об
|
||
ошибке, включающее сведения об ошибке и номер строки, в которой она возникла.
|
||
В процессе анализа программы синтаксический анализатор генерирует набор машинных
|
||
команд, соответствующие каждой конструкции языка.
|
||
|
||
Генератор кода представляет собой служебный компонент, ответственный за
|
||
формирование внешнего представления генерируемого кода. Генератор поддерживает
|
||
буфер команд и предоставляет синтаксическому анализатору набор функций,
|
||
позволяющий записать указанную команду по определенному адресу. После того, как
|
||
синтаксический анализатор заканчивает формирование программы, генератор кода
|
||
используется для печати на стандартный вывод отсортированной по возрастанию
|
||
адресов последовательности инструкций. Использование генератора кода несколько
|
||
упрощает устройство компилятора, поскольку синтаксический анализатор может
|
||
генерировать команды не в порядке их следования в программе, а по мере получения
|
||
всей необходимой информации для их формирования. Это особенно важно при
|
||
трансляции таких инструкций, как условные операторы или циклы.
|
||
|
||
Все три компонента объединяются <<драйвером>> --- управляющей программой
|
||
компилятора. Драйвер анализирует аргументы командной строки, инициализирует
|
||
синтаксический анализатор и вызывает его метод \texttt{parse()}, запуская
|
||
процесс трансляции.
|
||
|
||
\textsc{CMilan} является примером простейшего однопроходного компилятора,
|
||
в котором синтаксический анализ и генерация кода выполняются совместно. При этом
|
||
компилятор никак не оптимизирует код. Реальные компиляторы
|
||
обычно выполняют трансляцию и оптимизацию кода в несколько проходов, используя
|
||
несколько видов внутреннего представления программы. Сведения об архитектуре
|
||
таких компиляторов можно найти, в частности, в классической <<книге
|
||
дракона>>~\cite{dragonbook11}. В случае \textsc{CMilan}, тем не менее,
|
||
предпочтение было отдано не качеству генерируемого кода, а простоте реализации и
|
||
легкости расширения.
|
||
|
||
\subsection{Генератор кода}
|
||
|
||
Основной задачей генератора кода является хранение и заполнение буфера
|
||
инструкций последовательностью команд для виртуальной машины Милана. Генератор
|
||
кода не отвечает за правильность этой последовательности и не выполняет никаких
|
||
семантических преобразований. Тем не менее, генератор кода вполне мог бы быть
|
||
расширен для того, чтобы выполнять простые оптимизации на уровне машинных
|
||
команд.
|
||
|
||
Генератор кода описан в файлах \texttt{codegen.h} и \texttt{codegen.cpp}. Его
|
||
реализация состоит из двух классов. Класс \texttt{Command} описывает машинные
|
||
инструкций и отвечает за их вывод. В классе Codegen реализованы функции добавления
|
||
инструкций в буфер, получения текущего адреса в буфере, резервирования ячейки
|
||
для инструкции, которая должна быть добавлена в код позднее, и печати
|
||
последовательности инструкций из буфера на стандартный вывод.
|
||
|
||
Приведем краткое описание методов генератора кода.
|
||
\begin{itemize}
|
||
\item \texttt{void Command::print(int address, ostream\& os)}
|
||
|
||
Печать инструкции с указанием адреса \texttt{address} в поток вывода \texttt{os}.
|
||
|
||
\item \texttt{void CodeGen::emit(Instruction instruction)}
|
||
|
||
Добавление инструкции \texttt{instruction} без аргументов в конец буфера.
|
||
|
||
\item \texttt{void CodeGen::emit(Instruction instruction, int arg)}
|
||
|
||
Добавление инструкции \texttt{instruction} с аргументом \texttt{arg} в конец буфера.
|
||
|
||
\item \texttt{void CodeGen::emitAt(int address, Instruction instruction)}
|
||
|
||
Запись инструкции \texttt{instruction} без аргументов в буфер со смещением
|
||
\texttt{address}.
|
||
|
||
\item \texttt{void CodeGen::emitAt(int address, Instruction instruction, int
|
||
arg)}
|
||
|
||
Запись инструкции \texttt{instruction} с аргументом \texttt{arg} в буфер
|
||
со смещением \texttt{address}
|
||
|
||
\item \texttt{int CodeGen::getCurrentAddress()}
|
||
|
||
Возврат адреса, по которому будет записана очередная инструкция.
|
||
|
||
\item \texttt{int CodeGen::reserve()}
|
||
|
||
Резервирование ячейки памяти для инструкции. Метод добавляет в конец
|
||
буфера инструкцию \texttt{NOP} и возвращает ее адрес.
|
||
|
||
\item \texttt{void CodeGen::flush()}
|
||
|
||
Вывод последовательности инструкций в выходной поток.
|
||
\end{itemize}
|
||
|
||
\subsection{Лексический анализатор}
|
||
|
||
Лексический анализатор преобразует считываемые из входного потока символы в
|
||
лексемы языка Милан. Например, последовательность символов `\texttt{B}',
|
||
`\texttt{E}', `\texttt{G}', `\texttt{I}', `\texttt{N}' соответствует ключевому
|
||
слову `\texttt{begin}'. Каждой лексеме соответствует отдельный элемент
|
||
перечислимого типа \texttt{Token}. В частности, ключевому слову `\texttt{begin}'
|
||
соответствует константа \texttt{T\_BEGIN}. С некоторыми лексемами связана
|
||
дополнительная информация --- значение атрибута. Так, с лексемой
|
||
\texttt{T\_NUMBER} (целочисленная константа) связано число, равное значению этой
|
||
константы, а с лексемой \texttt{T\_IDENTIFIER} (идентификатор) --- строка,
|
||
содержащая имя переменной.
|
||
|
||
В таблице~\ref{lexemes} приведен полный список лексем и значений связанных с ними
|
||
атрибутов.
|
||
|
||
\begin{table}
|
||
\begin{center}
|
||
\begin{tabular}{|l|l|p{6cm}|}
|
||
\hline
|
||
\hline
|
||
Имя лексемы & Значение & Атрибут \\
|
||
\hline
|
||
\hline
|
||
\texttt{T\_EOF} & Конец текстового потока & \\
|
||
\hline
|
||
\texttt{T\_ILLEGAL} & Недопустимый символ & \\
|
||
\hline
|
||
\texttt{T\_IDENTIFIER}& Идентификатор & Имя переменной \\
|
||
\hline
|
||
\texttt{T\_NUMBER} & Константа & Значение константы \\
|
||
\hline
|
||
\texttt{T\_BEGIN} & Ключевое слово `\texttt{begin}' & \\
|
||
\hline
|
||
\texttt{T\_END} & Ключевое слово `\texttt{end}' & \\
|
||
\hline
|
||
\texttt{T\_IF} & Ключевое слово `\texttt{if}' & \\
|
||
\hline
|
||
\texttt{T\_THEN} & Ключевое слово `\texttt{then}' & \\
|
||
\hline
|
||
\texttt{T\_ELSE} & Ключевое слово `\texttt{else}' & \\
|
||
\hline
|
||
\texttt{T\_FI} & Ключевое слово `\texttt{fi}' & \\
|
||
\hline
|
||
\texttt{T\_WHILE} & Ключевое слово `\texttt{while}' & \\
|
||
\hline
|
||
\texttt{T\_DO} & Ключевое слово `\texttt{do}' & \\
|
||
\hline
|
||
\texttt{T\_OD} & Ключевое слово `\texttt{od}' & \\
|
||
\hline
|
||
\texttt{T\_WRITE} & Ключевое слово `\texttt{write}' & \\
|
||
\hline
|
||
\texttt{T\_READ} & Ключевое слово `\texttt{read}' & \\
|
||
\hline
|
||
\texttt{T\_ASSIGN} & Оператор `\texttt{:=}' & \\
|
||
\hline
|
||
\texttt{T\_ADDOP} & Операция типа сложения &
|
||
\texttt{A\_PLUS}~(`\texttt{+}'), \texttt{A\_MINUS}~(`\texttt{-}') \\
|
||
\hline
|
||
\texttt{T\_MULOP} & Операция типа умножения &
|
||
\texttt{A\_MULTIPLY}~(`\texttt{*}'), \texttt{A\_DIVIDE}~(`\texttt{/}') \\
|
||
\hline
|
||
\texttt{T\_CMP} & Оператор отношения &
|
||
\texttt{C\_EQ}~(`\texttt{=}'), \texttt{C\_NE}~(`\texttt{!=}'),
|
||
\texttt{C\_LT}~(`\texttt{<}'), \texttt{C\_LE}~(`\texttt{<=}'),
|
||
\texttt{C\_GT}~(`\texttt{>}'), \texttt{C\_GE}~(`\texttt{=}') \\
|
||
\hline
|
||
\texttt{T\_LPAREN} & Открывающая скобка & \\
|
||
\hline
|
||
\texttt{T\_RPAREN} & Закрывающая скобка & \\
|
||
\hline
|
||
\texttt{T\_SEMICOLON} & `\texttt{;}' & \\
|
||
\hline
|
||
\hline
|
||
\end{tabular}
|
||
\end{center}
|
||
\caption{Лексемы языка Милан}
|
||
\label{lexemes}
|
||
\end{table}
|
||
|
||
Все ключевые слова перечислены в ассоциативном массиве \texttt{keywords\_}. При
|
||
этом ключом является строка, соответствующая ключевому слову в нижнем регистре,
|
||
а значением --- соответствующая лексема. С помощью массива \texttt{tokenNames\_}
|
||
каждой лексеме сопоставлено ее строковое внешнее представление, которое
|
||
используется при формировании сообщений об ошибках. Последовательность
|
||
символов, не соответствующая ни одной из лексем, считается ошибочной. В этом
|
||
случае лексический анализатор возвращает значение \texttt{T\_ILLEGAL}.
|
||
|
||
Исходный текст лексического анализатора находится в файлах \texttt{scanner.h} и
|
||
\texttt{scanner.cpp}. Алгоритм лексического анализа реализован в классе
|
||
\texttt{Scanner}. Он содержит следующие открытые методы:
|
||
|
||
\begin{itemize}
|
||
\item \texttt{Token token()}
|
||
|
||
Получение текущей лексемы. Одна и та же лексема может быть прочитана
|
||
многократно.
|
||
|
||
\item \texttt{void nextToken()}
|
||
|
||
Переход к следующей лексеме.
|
||
|
||
\item \texttt{int getIntValue()}
|
||
|
||
Получение целочисленного атрибута текущей лексемы. В базовой версии Милана
|
||
этот метод используется только для получения значений целочисленных
|
||
констант.
|
||
|
||
\item \texttt{string getStringValue()}
|
||
|
||
Получение строкового атрибута текущей лексемы. В базовой версии Милана
|
||
этот метод используется для получения имен идентификаторов.
|
||
|
||
\item \texttt{Cmp getCmpValue()}
|
||
|
||
Получение кода операции сравнения текущей лексемы (используется только
|
||
вместе с лексемой \texttt{T\_CMP}).
|
||
|
||
\item \texttt{Arithmetic getArithmeticValue()}
|
||
|
||
Получение кода арифметической операции (используется вместе с лексемами
|
||
\texttt{T\_ADDOP} и \texttt{T\_MULOP}).
|
||
|
||
\item \texttt{const int getLineNumber()}
|
||
|
||
Получение номера текущей строки в исходном файле (используется при
|
||
формировании сообщений об ошибках).
|
||
\end{itemize}
|
||
|
||
Поля класса описывают состояние лексического анализатора:
|
||
\begin{itemize}
|
||
\item \texttt{const string fileName\_}
|
||
|
||
Имя входного файла.
|
||
|
||
\item \texttt{int lineNumber\_}
|
||
|
||
Номер текущей строки в анализируемой программе.
|
||
|
||
\item \texttt{Token token\_}
|
||
|
||
Текущая лексема.
|
||
|
||
\item \texttt{int intValue\_}
|
||
|
||
Целочисленный атрибут лексемы.
|
||
|
||
\item \texttt{string stringValue\_}
|
||
|
||
Строковый атрибут лексемы.
|
||
|
||
\item \texttt{Cmp cmpValue\_}
|
||
|
||
Код оператора сравнения.
|
||
|
||
\item \texttt{Arithmetic arithmeticValue\_}
|
||
|
||
Код арифметической операции.
|
||
|
||
\item \texttt{map<string, Token> keywords\_}
|
||
|
||
Ассоциативный массив, описывающий ключевые слова языка Милан. Используется
|
||
при обнаружении цепочки символов, которая может быть как идентификатором,
|
||
так и ключевым словом.
|
||
|
||
\item \texttt{ifstream input\_}
|
||
|
||
Входной поток для чтения из файла.
|
||
|
||
\item \texttt{char ch\_}
|
||
|
||
Очередной символ программы.
|
||
\end{itemize}
|
||
|
||
Закрытые методы класса (служебные функции):
|
||
\begin{itemize}
|
||
\item \texttt{bool isIdentifierStart(char c)}
|
||
|
||
Метод возвращает значение \texttt{true}, если символ \texttt{c} может быть
|
||
первым символом идентификатора (в базовой версии языка Милан это означает,
|
||
что символ является буквой латинского алфавита).
|
||
|
||
\item \texttt{bool isIdentifierBody(char c)}
|
||
|
||
Метод возвращает значение \texttt{true}, если символ \texttt{c} может
|
||
быть частью идентификатора. В базовой версии языка Милан эт означает, что
|
||
символ является буквой латинского алфавита или цифрой.
|
||
|
||
\item \texttt{void skipSpace()}
|
||
|
||
Пропуск всех пробельных символов (символов пробела, табуляции, перевода
|
||
строки). При чтении символа перевода строки увеличивается номер текущей
|
||
строки \texttt{lineNumber\_}.
|
||
|
||
\item \texttt{void nextChar()}
|
||
|
||
Переход к следующему символу программы.
|
||
\end{itemize}
|
||
|
||
Основная часть алгоритма лексического анализа реализована в методе
|
||
\texttt{nextToken}. Для того, чтобы перейти к следующей лексеме, выполняется
|
||
следующая последовательность действий,
|
||
|
||
Прежде всего необходимо удалить пробелы и комментарии. Признаком начала
|
||
комментария является последовательность символов `\texttt{/*}'. При обнаружении
|
||
символа `\texttt{/}' необходимо проверить следующий за ним символ. Если он не
|
||
совпадает с `\texttt{*}', значит, был найден оператор деления. В этом случае
|
||
переменной \texttt{token\_} присваивается значение \texttt{T\_MULOP}, а атрибуту
|
||
\texttt{arithmeticValue\_} --- значение \texttt{A\_DIVIDE}. Если за символом
|
||
`\texttt{/}' непосредственно следует `\texttt{*}', лексический анализатор
|
||
пропускает все символы, пока не встретит закрывающую комментарий
|
||
последовательность `\texttt{*/}' или конец файла. После того, как был найден
|
||
признак конца комментария, еще раз вызывается функция \texttt{skipSpace}.
|
||
Операция удаления комментариев повторяется многократно, пока очередной
|
||
непробельный символ не окажется отличным от символа `\texttt{/}'. Если в
|
||
процессе удаления пробелов или комментариев был встречен конец файла, очередной
|
||
лексемой считается \texttt{T\_EOF}.
|
||
|
||
После удаления пробелов и комментариев происходит анализ очередного символа. Он
|
||
выполняется по следующим правилам.
|
||
\begin{enumerate}
|
||
\item Если символ является цифрой, то очередная лексема --- целочисленная константа
|
||
(\texttt{T\_NUMBER}). Лексический анализатор считывает из входного потока эту
|
||
и все следующие за ней цифры, преобразуя полученную последовательность в целое
|
||
число. Это число становится значением атрибута \texttt{intValue\_}.
|
||
|
||
\item Если символ может быть началом идентификатора, из потока считываются все
|
||
последующие символы, которые могут быть частью идентификатора. Полученная
|
||
последовательность проверяется на совпадение с ключевым словом. В случае
|
||
совпадения очередной лексемой считается лексема, соответствующая этому ключевому
|
||
слову. Если цепочка символов не совпала ни с одним ключевым словом, очередная
|
||
лексема считается идентификатором (\texttt{T\_IDENTIFIER}), а сама цепочка
|
||
становится значением строкового атрибута \texttt{stringValue\_}.
|
||
|
||
\item Если символ равен `\texttt{:}', лексический анализатор считывает следующий
|
||
символ и проверяет, что он равен `\texttt{=}'. В этом случае возвращается
|
||
лексема \texttt{T\_ASSIGN}. Если следом за `\texttt{:}' идет любой другой
|
||
символ, лексема считается ошибочной (\texttt{T\_ILLEGAL}).
|
||
|
||
\item Если символ равен `\texttt{!}', производится аналогичная проверка на
|
||
равенство следующего символа `\texttt{=}'. В случае успеха текущей лексемой
|
||
становится лексема \texttt{T\_CMP}, а атрибут \texttt{cmpValue\_} принимает
|
||
значение \texttt{T\_NE}.
|
||
|
||
\item Если символ равен `\texttt{<}', `\texttt{>}' или `\texttt{=}', текущей
|
||
лексемой становится \texttt{T\_CMP}. Чтобы определить значение атрибута
|
||
\texttt{cmpValue\_}, лексический анализатор может прочитать еще один символ для
|
||
того, чтобы отличить оператор `\texttt{<}' от оператора `\texttt{>}', а оператор
|
||
`\texttt{>}' от оператора `\texttt{>=}'.
|
||
|
||
\item Если символ равен `\texttt{+}' или `\texttt{-}', переменная
|
||
\texttt{token\_} принимает значение \texttt{T\_ADDOP}, при этом соответствующим
|
||
образом устанавливается значение атрибута \texttt{arithmeticValue\_}. Аналогично
|
||
обрабатываются символ `\texttt{*}' (лексема \texttt{T\_MULOP}). Оператор деления
|
||
обрабатывать таким же образом не нужно, поскольку он обнаруживается в ходе
|
||
удаления комментариев.
|
||
|
||
\item Если символ совпадает с `\texttt{;}', `\texttt{(}', `\texttt{)}',
|
||
очередная лексема устанавливается в соответствующее символу значение.
|
||
\end{enumerate}
|
||
|
||
\subsection{Синтаксический анализатор}
|
||
|
||
Задачами синтаксического анализатора является проверка соответствия программы
|
||
грамматике языка Милан (рисунок~\ref{milan-grammar}) и формирование кода для
|
||
виртуальной машины Милана в соответствии со структурой программы. Синтаксический
|
||
анализ выполняется методом рекурсивного спуска. Каждому нетерминальному символу
|
||
грамматики сопоставлен метод, выполняющий проверку соответствия
|
||
последовательности лексем одному из тех правил грамматики, в левой части которых
|
||
стоит данный нетерминальный символ. Семантические действия (генерация кода)
|
||
встроены в код метода.
|
||
|
||
Исходный текст синтаксического анализатора находится в файлах \texttt{parser.h}
|
||
и \texttt{parser.cpp}. Алгоритм синтаксического анализа реализован в классе
|
||
\texttt{Parser}. Конструктор класса в качестве аргумента принимает имя файла, в
|
||
котором находится анализируемый текст, и создает экземпляры лексического
|
||
анализатора и генератора кода.
|
||
|
||
Синтаксический анализатор предоставляет один открытый метод \texttt{void parse()},
|
||
который используется для того, чтобы начать процесс анализа.
|
||
|
||
Состояние анализатора описывается следующими полями:
|
||
\begin{itemize}
|
||
\item \texttt{Scanner* scanner\_}
|
||
|
||
Экземпляр лексического анализатора.
|
||
|
||
\item \texttt{CodeGen* codegen\_}
|
||
|
||
Экземпляр генератора кода.
|
||
|
||
\item \texttt{std::ostream\& output\_}
|
||
|
||
Выходной поток, в который должны быть выведены инструкции программы.
|
||
Базовая версия компилятора Милан использует в качестве выходного потока
|
||
стандартный вывод (\texttt{std::cout}).
|
||
|
||
\item \texttt{bool error\_}
|
||
|
||
Признак ошибки в тексте программы. Если \texttt{error\_} принимает
|
||
значение <<истина>>, генерируемый машинный код не выводится.
|
||
|
||
\item \texttt{map<string, int> variables\_}
|
||
|
||
Таблица имен, найденных в программе. Она сопоставляет каждой переменной ее
|
||
адрес в памяти виртуальной машины. Поскольку базовая версия языка Милан не
|
||
содержит вложенных блоков, процедур или функций, таблица имен представляет
|
||
собой простой ассоциативный массив.
|
||
|
||
\item \texttt{int lastVar\_}
|
||
|
||
Адрес последней найденной переменной.
|
||
\end{itemize}
|
||
|
||
Синтаксический анализатор включает ряд вспомогательных функций (закрытые методы
|
||
класса).
|
||
|
||
\begin{itemize}
|
||
\item \texttt{bool see(Token t)}
|
||
|
||
Сравнение текущей лексемы с образцом. Текущая позиция в потоке лексем не
|
||
изменяется.
|
||
|
||
\item \texttt{bool match(Token t)}
|
||
|
||
Проверка совпадения текущей лексемы с образцом. Если лексема и образец
|
||
совпадают, лексема изымается из потока.
|
||
|
||
\item \texttt{void mustBe(Token t)}
|
||
|
||
Проверка совпадения текущей лексемы с образцом. Если лексема и образец
|
||
совпадают, лексема изымается из потока. В противном случае формируется
|
||
сообщение об ошибке, а программа считается некорректной.
|
||
|
||
\item \texttt{void next()}
|
||
|
||
Переход к следующей лексеме.
|
||
|
||
\item \texttt{void reportError(const string\& message)}
|
||
|
||
Формирование сообщения об ошибке. Каждое сообщение включает текст
|
||
\texttt{message} и номер строки, в которой обнаружена ошибка.
|
||
|
||
\item \texttt{void recover(Token t)}
|
||
|
||
Восстановление после ошибки. Используется примитивный алгоритм: если
|
||
очередная лексема не совпадает с ожидаемой, анализатор пропускает все
|
||
последующие лексемы, пока не встретит ожидаемую лексему или
|
||
конец файла. Хотя такой метод восстановления не отличается точностью, он,
|
||
тем не менее, позволяет продолжить анализ и, возможно, найти ошибки в
|
||
оставшейся части программы.
|
||
|
||
\item \texttt{int findOrAddVariable(const string\&)}
|
||
|
||
Поиск имени переменной в таблице имен. Если имя найдено, метод возвращает
|
||
адрес переменной, в противном случае имя добавляется в таблицу имен, и для
|
||
соответствующей переменной резервируется новый адрес.
|
||
\end{itemize}
|
||
|
||
Кроме служебных методов, класс \texttt{Parser} содержит закрытые методы
|
||
\texttt{void program()}, \texttt{void statementList()}, \texttt{void
|
||
statement()}, \texttt{void expression()}, \texttt{void term()}, \texttt{void
|
||
factor()}, \texttt{void relation()}, которые соответствуют нетерминальным
|
||
символам грамматики. Эти методы не возвращают значений и не принимают
|
||
аргументов, поскольку с нетерминальными символами не связаны явные атрибуты: все
|
||
семантические действия, которые производятся во время анализа программы,
|
||
модифицируют буфер общего для всех методов генератора кода.
|
||
|
||
\subsubsection{Распознавание операторов}
|
||
|
||
Продемонстрируем алгоритм работы синтаксического анализатора на примере анализа
|
||
операторов (метод \texttt{statement}).
|
||
|
||
В соответствии с грамматикой языка Милан, в качестве оператора может выступать
|
||
оператор присваивания, условный оператор \texttt{if}, оператор цикла
|
||
\texttt{while} или оператор печати \texttt{write}. Для выбора нужной
|
||
альтернативы достаточно прочитать первую лексему оператора.
|
||
|
||
Если очередная лексема равна \texttt{T\_IDENTIFIER}, синтаксический анализатор
|
||
имеет дело с оператором присваивания. Он считывает имя переменной, стоящей в
|
||
левой части присваивания, и определяет ее адрес. Затем анализатор проверяет, что
|
||
следующая лексема совпадает с \texttt{T\_ASSIGN}. После знака `\texttt{:=}' в
|
||
программе должно следовать арифметическое выражение, поэтому анализатор вызывает
|
||
метод \texttt{expression()}. Этот метод проверяет правильность арифметического
|
||
выражения и генерирует последовательность команд для его вычисления. В
|
||
результате выполнены этой последовательности на вершине стека будет находиться
|
||
значение выражения. Чтобы выполнить присваивание, достаточно записать значение с
|
||
вершины стека в память по адресу переменной в левой части оператора
|
||
присваивания. Для этого после вызова \texttt{expression()} нужно добавить в
|
||
буфер команд инструкцию \texttt{STORE}, аргументом которой будет запомненный
|
||
ранее адрес переменной.
|
||
|
||
Если очередная лексема равна \texttt{T\_IF}, анализатор должен начать разбор
|
||
условного оператора. Сначала он вызывает метод \texttt{relation()}, проверяя,
|
||
что после лексемы \texttt{T\_IF} следует правильное сравнение двух выражений.
|
||
Метод \texttt{relation()} генерирует код, вычисляющий и сравнивающий два
|
||
выражения. При выполнении этого кода на вершине стека останется значение $1$,
|
||
если условие выполнено, и $0$ в противном случае. Если условие не выполнено,
|
||
должен произойти переход к списку операторов блока \texttt{else} или, если этого
|
||
блока нет, по адресу следующей за условным оператором инструкции. Однако этот
|
||
адрес еще не известен синтаксическому анализатору. Чтобы решить эту проблему,
|
||
применяется так называемый <<метод обратных поправок>>~\cite{dragonbook11}.
|
||
Анализатор резервирует место для условного перехода \texttt{JUMP\_NO} и
|
||
запоминает адрес этой инструкции в переменной \texttt{jumpNoAddress}, после чего
|
||
переходит к дальнейшему анализу условного оператора.
|
||
|
||
Следом за условием в тексте программы должна находиться лексема
|
||
\texttt{T\_THEN}. Если она обнаружена, анализатор переходит к следующей лексеме
|
||
и вызывает метод \texttt{statementList()}, который анализирует список операторов
|
||
и генерирует для них исполняемый код. Дальнейшие действия зависят от того,
|
||
присутствует ли в условном операторе ветвь \texttt{else}. Если следующая лексема
|
||
равна \texttt{T\_ELSE}, синтаксический анализатор должен сгенерировать
|
||
инструкцию \texttt{JUMP} для выхода из условного оператора, после чего
|
||
приступить к анализу блока \texttt{else}. Если условный оператор не содержит
|
||
блока \texttt{else}, инструкция безусловного перехода в конце блока
|
||
\texttt{then} не нужна.
|
||
|
||
Генерация безусловного перехода также требует знания адреса, следующего за
|
||
последней инструкцией условного оператора, поэтому анализатор снова должен
|
||
зарезервировать ячейку памяти для команды перехода. Следующий за этой ячейкой
|
||
адрес содержит первую команду блока \texttt{else}. Это именно тот адрес, по
|
||
которому необходимо было выполнить переход в случае, если условие не было
|
||
выполнено. Поскольку теперь этот адрес известен, синтаксический анализатор
|
||
генерирует инструкцию безусловного перехода и помещает ее в буфер команд по
|
||
адресу, который записан в переменной \texttt{jumpNoAddress}. В случае отсутствия
|
||
блока \texttt{else} поправка происходит таким же образом, но в качестве адреса
|
||
перехода просто используется следующий за последней инструкцией блока
|
||
\texttt{then} адрес. После вызова функции \texttt{statementList}, которая
|
||
разбирает список операторов блока \texttt{else}, аналогично исправляется
|
||
зарезервированная инструкция безусловного перехода.
|
||
|
||
Структура кода, соответствующая условному оператору, приведена на
|
||
рисунке~\ref{ifthenelse-code}.
|
||
|
||
\begin{figure}
|
||
\begin{verbatim}
|
||
Вычисление выражений
|
||
...
|
||
COMPARE operator
|
||
JUMP_NO elseLabel
|
||
Операторы блока THEN
|
||
...
|
||
JUMP endLabel
|
||
elseLabel: Операторы блока ELSE
|
||
...
|
||
endLabel:
|
||
\end{verbatim}
|
||
\caption{Структура исполняемого кода условного оператора}
|
||
\label{ifthenelse-code}
|
||
\end{figure}
|
||
|
||
Если очередная лексема равна \texttt{T\_WHILE}, синтаксическому анализатору
|
||
предстоит разобрать оператор цикла. Анализатор должен запомнить текущий адрес
|
||
команды, поскольку после каждой итерации цикла по этому адресу нужно будет
|
||
возвращаться. Затем анализатор вызывает метод \texttt{relation()}, чтобы
|
||
сгенерировать код проверки условия. Если условие окажется ложным, нужно будет
|
||
выполнить переход на следующий за циклом оператор. Для этого синтаксический
|
||
анализатор резервирует ячейку памяти для команды \texttt{JUMP\_NO}. После этого
|
||
он проверяет наличие лексемы \texttt{T\_DO} и вызывает метод
|
||
\texttt{statementList()} для обработки тела цикла. Затем анализатор генерирует
|
||
инструкцию безусловного перехода назад, к началу проверки условия, и записывает
|
||
в зарезервированную ячейку инструкцию условного перехода по известному теперь
|
||
адресу конца цикла. Для завершения анализа инструкции достаточно убедиться, что
|
||
очередная лексема равна ожидаемому значению \texttt{T\_OD}. Структура
|
||
исполняемого кода для цикла приведена на рисунке~\ref{while-code}.
|
||
|
||
\begin{figure}
|
||
\begin{verbatim}
|
||
whileLabel: Вычисление выражений
|
||
...
|
||
COMPARE operator
|
||
JUMP_NO endLabel
|
||
...
|
||
Операторы тела цикла
|
||
...
|
||
JUMP whileLabel
|
||
endLabel:
|
||
\end{verbatim}
|
||
\caption{Структура исполняемого кода цикла с предусловием}
|
||
\label{while-code}
|
||
\end{figure}
|
||
|
||
Если очередная лексема равна \texttt{T\_WRITE}, необходимо проверить, что
|
||
следующая лексема равна \texttt{T\_LPAREN}, затем вызвать метод
|
||
\texttt{expression()} и проверить, что за выражением в потоке следует лексема
|
||
\texttt{T\_RPAREN}. Поскольку код, вычисляющий значение выражения, оставляет его
|
||
значение на вершине стека, для выполнения печати достаточно после вызова
|
||
\texttt{expression()} сгенерировать инструкцию \texttt{PRINT}.
|
||
|
||
\begin{thebibliography}{9}
|
||
\addcontentsline{toc}{section}{Список литературы}
|
||
|
||
\bibitem{dragonbook11}
|
||
А. Ахо, М. Лам, Р. Сети, Дж. Ульман,
|
||
\emph{Компиляторы: принципы, технологии и инструментарий}, 2-е изд.
|
||
М.: Вильямс, 2011.
|
||
|
||
\bibitem{karpov05}
|
||
Ю.Г. Карпов,
|
||
\emph{Теория и технология программирования. Основы построения трансляторов}.
|
||
СПб.: БХВ-Петербург, 2005.
|
||
\end{thebibliography}
|
||
|
||
\end{document}
|
||
|