This commit is contained in:
2025-05-28 14:19:58 +03:00
parent fdd30a8854
commit c001ccb12a
9 changed files with 565 additions and 21 deletions

View File

@@ -0,0 +1,7 @@
BEGIN
x := 0;
write(x++);
write(x);
write(++x);
write(x)
END

View File

@@ -0,0 +1,6 @@
BEGIN
i := 1;
j := 2;
IF i < --j THEN WRITE(100) ELSE WRITE(-100) FI
END

View File

@@ -0,0 +1,13 @@
BEGIN
y := x++;
write(y);
write(x);
y := 10 - --x;
write(y);
write(x);
y := 10 -++x;
write(y);
write(x)
END

View File

@@ -0,0 +1,5 @@
BEGIN
x := 10;
y := 10 - --x; /* Корректно: минус и декремент разделены пробелом */
y := 10 ---x; /* Некорректно: три минуса подряд */
END

BIN
lab4/report/img/result1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
lab4/report/img/result2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
lab4/report/img/result3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
lab4/report/img/result4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -391,45 +391,558 @@
\phantom{text}
\section{Особенности реализации}
\subsection{Token}
\subsection{Изменения в лексическом анализаторе}
\subsubsection{Файл \texttt{Scanner.h}}
В перечисление \texttt{Token} в файле \texttt{Scanner.h} были добавлены новые токены: \texttt{INC}, \texttt{DEC}. Код обновлённого перечисления представлен в листинге~\ref{lst:token}, добавлены строки 24-25.
\begin{lstlisting}[language=C++, caption=Обновлённое перечисление \texttt{Token}, label=lst:token]
enum Token {
T_EOF, // Конец текстового потока
T_ILLEGAL, // Признак недопустимого символа
T_IDENTIFIER, // Идентификатор
T_NUMBER, // Целочисленный литерал
T_BEGIN, // Ключевое слово "begin"
T_END, // Ключевое слово "end"
T_IF, // Ключевое слово "if"
T_THEN, // Ключевое слово "then"
T_ELSE, // Ключевое слово "else"
T_FI, // Ключевое слово "fi"
T_WHILE, // Ключевое слово "while"
T_DO, // Ключевое слово "do"
T_OD, // Ключевое слово "od"
T_WRITE, // Ключевое слово "write"
T_READ, // Ключевое слово "read"
T_ASSIGN, // Оператор ":="
T_ADDOP, // Сводная лексема для "+" и "-" (операция типа сложения)
T_MULOP, // Сводная лексема для "*" и "/" (операция типа умножения)
T_CMP, // Сводная лексема для операторов отношения
T_LPAREN, // Открывающая скобка
T_RPAREN, // Закрывающая скобка
T_SEMICOLON, // ";"
T_INC, // Оператор инкремента
T_DEC // Оператор декремента
};
\end{lstlisting}
\subsubsection{Файл \texttt{Scanner.cpp}}
В массив названий токенов \texttt{tokenNames\_} были добавлены новые строки, соответствующие инкременту и декременту: \texttt{"++"} и \texttt{"\textminus\textminus"}, соответственно. Код обновлённого массива представлен в листинге~\ref{lst:token_names}, добавлены строки 24-25.
\begin{lstlisting}[language=C++, caption=Обновлённый массив \texttt{tokenNames\_}, label=lst:token_names]
static const char * tokenNames_[] = {
"end of file",
"illegal token",
"identifier",
"number",
"'BEGIN'",
"'END'",
"'IF'",
"'THEN'",
"'ELSE'",
"'FI'",
"'WHILE'",
"'DO'",
"'OD'",
"'WRITE'",
"'READ'",
"':='",
"'+' or '-'",
"'*' or '/'",
"comparison operator",
"'('",
"')'",
"';'",
"'++'",
"'--'",
};
\end{lstlisting}
Также была обновлена функция \texttt{nextToken()}. В ней были добавлены новые условия для распознавания инкремента и декремента. Код обновлённой функции представлен в листинге~\ref{lst:next_token}, изменения коснулись строк 158-181.
\begin{lstlisting}[language=C++, caption=Обновлённая функция \texttt{nextToken()}, label=lst:next_token]
void Scanner::nextToken()
{
skipSpace();
// Пропускаем комментарии
// Если встречаем "/", то за ним должна идти "*". Если "*" не встречена, считаем, что встретили операцию деления
// и лексему - операция типа умножения. Дальше смотрим все символы, пока не находим звездочку или символ конца файла.
// Если нашли * - проверяем на наличие "/" после нее. Если "/" не найден - ищем следующую "*".
while(ch_ == '/') {
nextChar();
if(ch_ == '*') {
nextChar();
bool inside = true;
while(inside) {
while(ch_ != '*' && !input_.eof()) {
nextChar();
}
if(input_.eof()) {
token_ = T_EOF;
return;
}
nextChar();
if(ch_ == '/') {
inside = false;
nextChar();
}
}
}
else {
token_ = T_MULOP;
arithmeticValue_ = A_DIVIDE;
return;
}
skipSpace();
}
//Если встречен конец файла, считаем за лексему конца файла.
if(input_.eof()) {
token_ = T_EOF;
return;
}
//Если встретили цифру, то до тех пока дальше идут цифры - считаем как продолжение числа.
//Запоминаем полученное целое, а за лексему считаем целочисленный литерал
if(isdigit(ch_)) {
int value = 0;
while(isdigit(ch_)) {
value = value * 10 + (ch_ - '0'); //поразрядное считывание, преобразуем символьное значение к числу.
nextChar();
}
token_ = T_NUMBER;
intValue_ = value;
}
//Если же следующий символ - буква ЛА - тогда считываем до тех пор, пока дальше буквы ЛА или цифры.
//Как только считали имя переменной, сравниваем ее со списком зарезервированных слов. Если не совпадает ни с одним из них,
//считаем, что получили переменную, имя которой запоминаем, а за текущую лексему считаем лексему идентификатора.
//Если совпадает с каким-либо словом из списка - считаем что получили лексему, соответствующую этому слову.
else if(isIdentifierStart(ch_)) {
string buffer;
while(isIdentifierBody(ch_)) {
buffer += ch_;
nextChar();
}
transform(buffer.begin(), buffer.end(), buffer.begin(), ::tolower);
map<string, Token>::iterator kwd = keywords_.find(buffer);
if(kwd == keywords_.end()) {
token_ = T_IDENTIFIER;
stringValue_ = buffer;
}
else {
token_ = kwd->second;
}
}
//Символ не является буквой, цифрой, "/" или признаком конца файла
else {
switch(ch_) {
//Признак лексемы открывающей скобки - встретили "("
case '(':
token_ = T_LPAREN;
nextChar();
break;
//Признак лексемы закрывающей скобки - встретили ")"
case ')':
token_ = T_RPAREN;
nextChar();
break;
//Признак лексемы ";" - встретили ";"
case ';':
token_ = T_SEMICOLON;
nextChar();
break;
//Если встречаем ":", то дальше смотрим наличие символа "=". Если находим, то считаем что нашли лексему присваивания
//Иначе - лексема ошибки.
case ':':
nextChar();
if(ch_ == '=') {
token_ = T_ASSIGN;
nextChar();
}
else {
token_ = T_ILLEGAL;
}
break;
//Если встретили символ "<", то либо следующий символ "=", тогда лексема нестрогого сравнения. Иначе - строгого.
case '<':
token_ = T_CMP;
nextChar();
if(ch_ == '=') {
cmpValue_ = C_LE;
nextChar();
}
else {
cmpValue_ = C_LT;
}
break;
//Аналогично предыдущему случаю
case '>':
token_ = T_CMP;
nextChar();
if(ch_ == '=') {
cmpValue_ = C_GE;
nextChar();
}
else {
cmpValue_ = C_GT;
}
break;
//Если встретим "!", то дальше должно быть "=", тогда считаем, что получили лексему сравнения
//и знак "!=" иначе считаем, что у нас лексема ошибки
case '!':
nextChar();
if(ch_ == '=') {
nextChar();
token_ = T_CMP;
cmpValue_ = C_NE;
}
else {
token_ = T_ILLEGAL;
}
break;
//Если встретим "=" - лексема сравнения и знак "="
case '=':
token_ = T_CMP;
cmpValue_ = C_EQ;
nextChar();
break;
//Знаки операций. Для "+"/"-" получим лексему операции типа сложнения, и соответствующую операцию.
//для "*" - лексему операции типа умножения
case '+':
nextChar();
// Ищем оператор инкремента
if(ch_ == '+') {
token_ = T_INC;
nextChar();
}
else {
token_ = T_ADDOP;
arithmeticValue_ = A_PLUS;
}
break;
case '-':
nextChar();
// Ищем оператор декремента
if(ch_ == '-') {
token_ = T_DEC;
nextChar();
}
else {
token_ = T_ADDOP;
arithmeticValue_ = A_MINUS;
}
break;
case '*':
token_ = T_MULOP;
arithmeticValue_ = A_MULTIPLY;
nextChar();
break;
//Иначе лексема ошибки.
default:
token_ = T_ILLEGAL;
nextChar();
break;
}
}
}
\end{lstlisting}
\subsection{Изменения в компиляторе}
\subsubsection{Файл \texttt{Parser.cpp}}
В метод \texttt{factor()} объекта \texttt{Parser} была добавлена логика генерации команд для префиксного и постфиксного инкремента и декремента. Код обновлённого метода представлен в листинге~\ref{lst:parser_cpp}, изменения коснулись строк 20-27 (постфиксный инкремент и декремент) и строк 29-41 (префиксный инкремент и декремент).
Для постфиксного инкремента и декремента генерируется следующая последовательность команд виртуальной машины языка MiLan:
\begin{itemize}
\item \texttt{LOAD <адрес переменной>} - загрузка значения переменной на вершину стека.
\item \texttt{DUP} - дублирование значения на вершине стека до выполнения операции инкремента или декремента, так как постфиксная операция возвращает старое значение переменной.
\item \texttt{PUSH 1} - загрузка константы 1 на вершину стека
\item \texttt{ADD} или \texttt{SUB} - сложение или вычитание значения на вершине стека с константой 1.
\item \texttt{STORE <адрес переменной>} - сохранение результата на вершине стека по адресу переменной.
\end{itemize}
Для префиксного инкремента и декремента генерируется следующая последовательность команд виртуальной машины языка MiLan:
\begin{itemize}
\item \texttt{LOAD <адрес переменной>} - загрузка значения переменной на вершину стека.
\item \texttt{PUSH 1} - загрузка константы 1 на вершину стека
\item \texttt{ADD} или \texttt{SUB} - сложение или вычитание значения на вершине стека с константой 1.
\item \texttt{DUP} - дублирование значения на вершине стека после выполнения операции инкремента или декремента, так как префиксная операция возвращает новое значение переменной.
\item \texttt{STORE <адрес переменной>} - сохранение результата на вершине стека по адресу переменной.
\end{itemize}
\begin{lstlisting}[language=C++, caption=Обновлённый метод \texttt{factor()}, label=lst:parser_cpp]
void Parser::factor()
{
/*
Множитель описывается следующими правилами:
<factor> -> number | identifier | -<factor> | (<expression>) | READ
| ++ identifier | -- identifier | identifier++ | identifier--
*/
if(see(T_NUMBER)) {
int value = scanner_->getIntValue();
next();
codegen_->emit(PUSH, value);
//Если встретили число, то преобразуем его в целое и записываем на вершину стека
}
else if(see(T_IDENTIFIER)) {
int varAddress = findOrAddVariable(scanner_->getStringValue());
next();
codegen_->emit(LOAD, varAddress);
//Если встретили переменную, то выгружаем значение, лежащее по ее адресу, на вершину стека
// Постфиксный инкремент или декремент
if(see(T_INC) || see(T_DEC)) {
codegen_->emit(DUP);
codegen_->emit(PUSH, 1);
codegen_->emit(see(T_INC) ? ADD : SUB);
codegen_->emit(STORE, varAddress);
next();
}
}
// Префиксный инкремент или декремент
else if(see(T_INC) || see(T_DEC)) {
bool isIncrement = see(T_INC);
next();
mustBe(T_IDENTIFIER);
int varAddress = findOrAddVariable(scanner_->getStringValue());
codegen_->emit(LOAD, varAddress);
codegen_->emit(PUSH, 1);
codegen_->emit(isIncrement ? ADD : SUB);
codegen_->emit(DUP);
codegen_->emit(STORE, varAddress);
}
else if(see(T_ADDOP) && scanner_->getArithmeticValue() == A_MINUS) {
next();
factor();
codegen_->emit(INVERT);
//Если встретили знак "-", и за ним <factor> то инвертируем значение, лежащее на вершине стека
}
else if(match(T_LPAREN)) {
expression();
mustBe(T_RPAREN);
//Если встретили открывающую скобку, тогда следом может идти любое арифметическое выражение и обязательно
//закрывающая скобка.
}
else if(match(T_READ)) {
codegen_->emit(INPUT);
//Если встретили зарезервированное слово READ, то записываем на вершину стека идет запись со стандартного ввода
}
else {
reportError("expression expected.");
}
}
\end{lstlisting}
\newpage
\section{Результаты работы программы}
Результаты работы программы представлены на Рис.~\ref{fig:result1}.
\subsection*{Программа №1}
Исходный код программы представлен в листинге~\ref{lst:program1}.
% \begin{figure}[h!]
% \centering
% \includegraphics[width=1\linewidth]{img/result1.png}
% \caption{Результаты работы программы.}
% \label{fig:result1}
% \end{figure}
\begin{lstlisting}[caption=Исходный код программы №1, label=lst:program1]
BEGIN
x := 0;
write(x++);
write(x);
write(++x);
write(x)
END
\end{lstlisting}
% % \newpage
Последовательность команд, сгенерированная компилятором для данной программы, представлена в листинге~\ref{lst:program1_commands}.
% \begin{figure}[h!]
% \centering
% \includegraphics[width=0.5\linewidth]{img/wrong.png}
% \caption{Реакция программы на некорректный пользовательский ввод.}
% \label{fig:wrong}
% \end{figure}
На Рис.~\ref{fig:wrong} представлена реакция программы на некорректный пользовательский ввод.
\begin{lstlisting}[caption={Последовательность команд, сгенерированная компилятором для программы №1.}, label={lst:program1_commands}, numbers=none]
0: PUSH 0
1: STORE 0
2: LOAD 0
3: DUP
4: PUSH 1
5: ADD
6: STORE 0
7: PRINT
8: LOAD 0
9: PRINT
10: LOAD 0
11: PUSH 1
12: ADD
13: DUP
14: STORE 0
15: PRINT
16: LOAD 0
17: PRINT
18: STOP
\end{lstlisting}
Результаты работы виртуальной машины для программы №1 представлены на Рис.~\ref{fig:result1}.
\begin{figure}[h!]
\centering
\includegraphics[width=0.4\linewidth]{img/result1.png}
\caption{Результаты работы виртуальной машины для программы №1.}
\label{fig:result1}
\end{figure}
\subsection*{Программа №2}
Исходный код программы представлен в листинге~\ref{lst:program2}.
\begin{lstlisting}[caption=Исходный код программы №2, label=lst:program2]
BEGIN
i := 1;
j := 2;
IF i < --j THEN WRITE(100) ELSE WRITE(-100) FI
END
\end{lstlisting}
Последовательность команд, сгенерированная компилятором для данной программы, представлена в листинге~\ref{lst:program2_commands}.
\begin{lstlisting}[caption={Последовательность команд, сгенерированная компилятором для программы №2.}, label={lst:program2_commands}, numbers=none]
0: PUSH 1
1: STORE 0
2: PUSH 2
3: STORE 1
4: LOAD 0
5: LOAD 1
6: PUSH 1
7: SUB
8: DUP
9: STORE 1
10: COMPARE 2
11: JUMP_NO 15
12: PUSH 100
13: PRINT
14: JUMP 18
15: PUSH 100
16: INVERT
17: PRINT
18: STOP
\end{lstlisting}
Результаты работы виртуальной машины для программы №2 представлены на Рис.~\ref{fig:result2}.
\begin{figure}[h!]
\centering
\includegraphics[width=0.4\linewidth]{img/result2.png}
\caption{Результаты работы виртуальной машины для программы №2.}
\label{fig:result2}
\end{figure}
\subsection*{Программа №3}
Исходный код программы представлен в листинге~\ref{lst:program3}.
\begin{lstlisting}[caption=Исходный код программы №3, label=lst:program3]
BEGIN
y := x++;
write(y);
write(x);
y := 10 - --x;
write(y);
write(x);
y := 10 -++x;
write(y);
write(x)
END
\end{lstlisting}
Последовательность команд, сгенерированная компилятором для данной программы, представлена в листинге~\ref{lst:program3_commands}.
\begin{lstlisting}[caption={Последовательность команд, сгенерированная компилятором для программы №3.}, label={lst:program3_commands}, numbers=none]
0: LOAD 1
1: DUP
2: PUSH 1
3: ADD
4: STORE 1
5: STORE 0
6: LOAD 0
7: PRINT
8: LOAD 1
9: PRINT
10: PUSH 10
11: LOAD 1
12: PUSH 1
13: SUB
14: DUP
15: STORE 1
16: SUB
17: STORE 0
18: LOAD 0
19: PRINT
20: LOAD 1
21: PRINT
22: PUSH 10
23: LOAD 1
24: PUSH 1
25: ADD
26: DUP
27: STORE 1
28: SUB
29: STORE 0
30: LOAD 0
31: PRINT
32: LOAD 1
33: PRINT
34: STOP
\end{lstlisting}
Результаты работы виртуальной машины для программы №3 представлены на Рис.~\ref{fig:result3}.
\begin{figure}[h!]
\centering
\includegraphics[width=0.4\linewidth]{img/result3.png}
\caption{Результаты работы виртуальной машины для программы №3.}
\label{fig:result3}
\end{figure}
\subsection*{Программа №4}
Исходный код программы представлен в листинге~\ref{lst:program4}.
\begin{lstlisting}[caption=Исходный код программы №4, label=lst:program4]
BEGIN
x := 10;
y := 10 - --x; /* Корректно: минус и декремент разделены пробелом */
y := 10 ---x; /* Некорректно: три минуса подряд */
END
\end{lstlisting}
Результат запуска компилятора для данной программы представлен на Рис.~\ref{fig:result4}.
\begin{figure}[h!]
\centering
\includegraphics[width=0.5\linewidth]{img/result4.png}
\caption{Результат запуска компилятора для программы №4.}
\label{fig:result4}
\end{figure}
\newpage
\section*{Заключение}
\addcontentsline{toc}{section}{Заключение}
В ходе выполнения лабораторной работы была построена контекстно-свободная грамматика для подмножества немецкого языка, описывающая простое прошедшее время Претерит. На основе разработанной грамматики была реализована программа, которая проверяет принадлежность входной строки заданному языку и генерирует случайные корректные предложения. Для анализа предложений использовался алгоритм LL(1)-разбора, основанный на построении множеств FIRST и FOLLOW для всех нетерминалов грамматики и создании таблицы синтаксического анализа.
В ходе выполнения лабораторной работы была успешно реализована поддержка операций инкремента и декремента в компиляторе языка MiLan. Были добавлены как префиксные (\texttt{++i}, \texttt{--i}), так и постфиксные (\texttt{i++}, \texttt{i--}) операторы с корректной семантикой их выполнения. Модификации затронули как лексический анализатор (добавление новых токенов \texttt{T\_INC} и \texttt{T\_DEC}), так и синтаксический анализатор (расширение метода \texttt{factor()} для обработки новых конструкций).
Из достоинств выполнения лабораторной работы можно выделить возможность задания грамматики в отдельном текстовом файле, что позволяет легко изменять и расширять её без модификации программного кода. Также программа автоматически проверяет, что введенная грамматика является LL(1)-грамматикой. В противном случае, программа выводит сообщение об ошибке, в указывается на конкретные правила грамматики, между выбором которых возникает неоднозначность.
Расширенная грамматика языка MiLan сохранила свойство LL(1), что позволило избежать значительных изменений в существующей архитектуре компилятора. Было добавлено лишь несколько новых правил в определение нетерминала \texttt{<factor>}.
К недостаткам текущей реализации можно отнести ограниченность словарного запаса, что сужает разнообразие генерируемых предложений. Также алгоритм генерации не контролирует длину предложений, что может приводить к избыточно длинным или коротким конструкциям. В текущей версии система не учитывает некоторые грамматические особенности немецкого языка, например, склонение прилагательных и согласование артиклей с родом существительных.
Из достоинств реализации можно отметить минимальность вносимых изменений в существующую архитектуру компилятора и сохранение всех ранее реализованных функций. При этом реализация выполняет поставленные задачи, корректно обрабатывая возможные случаи использования операторов инкремента и декремента.
Функционал программы несложно масштабировать. Грамматику легко расширять, добавляя новые слова и правила в текстовый файл без необходимости изменения программного кода. Класс Grammar может служить хорошей основой для создания полноценного LL(k) анализатора.
К недостаткам текущей реализации можно отнести отсутствие каких-либо оптимизаций при генерации команд виртуальной машины, что может приводить к избыточному количеству инструкций. Также в коде наблюдается некоторое дублирование логики между обработкой инкремента и декремента.
На выполнение лабораторной работы ушло около 12 часов. Работа была выполнена в среде разработки Visual Studio Code. Программа написана на Python версии 3.13.
В качестве направлений масштабирования можно предложить добавление составных операторов присваивания (\texttt{+=}, \texttt{-=}, \texttt{*=}, \texttt{/=}), для которых генерируется схожая последовательность низкоуровневых команд. Также возможна реализация оптимизаций генерируемого кода, таких как устранение избыточных команд в простых случаях.
На выполнение лабораторной работы ушло около 6 часов. Работа была выполнена в среде разработки Visual Studio Code.
\newpage
\section*{Список литературы}