diff --git a/lab4/cmilan/test/incdec/inc.mil b/lab4/cmilan/test/incdec/inc.mil new file mode 100644 index 0000000..b73fbc5 --- /dev/null +++ b/lab4/cmilan/test/incdec/inc.mil @@ -0,0 +1,7 @@ +BEGIN + x := 0; + write(x++); + write(x); + write(++x); + write(x) +END \ No newline at end of file diff --git a/lab4/cmilan/test/incdec/incif.mil b/lab4/cmilan/test/incdec/incif.mil new file mode 100644 index 0000000..120c991 --- /dev/null +++ b/lab4/cmilan/test/incdec/incif.mil @@ -0,0 +1,6 @@ +BEGIN + i := 1; + j := 2; + + IF i < --j THEN WRITE(100) ELSE WRITE(-100) FI +END \ No newline at end of file diff --git a/lab4/cmilan/test/incdec/incxy.mil b/lab4/cmilan/test/incdec/incxy.mil new file mode 100644 index 0000000..f31ce47 --- /dev/null +++ b/lab4/cmilan/test/incdec/incxy.mil @@ -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 \ No newline at end of file diff --git a/lab4/cmilan/test/incdec/invalid.mil b/lab4/cmilan/test/incdec/invalid.mil new file mode 100644 index 0000000..03ab663 --- /dev/null +++ b/lab4/cmilan/test/incdec/invalid.mil @@ -0,0 +1,5 @@ +BEGIN + x := 10; + y := 10 - --x; /* Корректно: минус и декремент разделены пробелом */ + y := 10 ---x; /* Некорректно: три минуса подряд */ +END \ No newline at end of file diff --git a/lab4/report/img/result1.png b/lab4/report/img/result1.png new file mode 100644 index 0000000..80a0504 Binary files /dev/null and b/lab4/report/img/result1.png differ diff --git a/lab4/report/img/result2.png b/lab4/report/img/result2.png new file mode 100644 index 0000000..27f4991 Binary files /dev/null and b/lab4/report/img/result2.png differ diff --git a/lab4/report/img/result3.png b/lab4/report/img/result3.png new file mode 100644 index 0000000..90f83c8 Binary files /dev/null and b/lab4/report/img/result3.png differ diff --git a/lab4/report/img/result4.png b/lab4/report/img/result4.png new file mode 100644 index 0000000..eb1327f Binary files /dev/null and b/lab4/report/img/result4.png differ diff --git a/lab4/report/report.tex b/lab4/report/report.tex index 1518846..227ce6d 100644 --- a/lab4/report/report.tex +++ b/lab4/report/report.tex @@ -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::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() +{ + /* + Множитель описывается следующими правилами: + -> number | identifier | - | () | 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); + //Если встретили знак "-", и за ним то инвертируем значение, лежащее на вершине стека + } + 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{}. - К недостаткам текущей реализации можно отнести ограниченность словарного запаса, что сужает разнообразие генерируемых предложений. Также алгоритм генерации не контролирует длину предложений, что может приводить к избыточно длинным или коротким конструкциям. В текущей версии система не учитывает некоторые грамматические особенности немецкого языка, например, склонение прилагательных и согласование артиклей с родом существительных. + Из достоинств реализации можно отметить минимальность вносимых изменений в существующую архитектуру компилятора и сохранение всех ранее реализованных функций. При этом реализация выполняет поставленные задачи, корректно обрабатывая возможные случаи использования операторов инкремента и декремента. - Функционал программы несложно масштабировать. Грамматику легко расширять, добавляя новые слова и правила в текстовый файл без необходимости изменения программного кода. Класс Grammar может служить хорошей основой для создания полноценного LL(k) анализатора. + К недостаткам текущей реализации можно отнести отсутствие каких-либо оптимизаций при генерации команд виртуальной машины, что может приводить к избыточному количеству инструкций. Также в коде наблюдается некоторое дублирование логики между обработкой инкремента и декремента. - На выполнение лабораторной работы ушло около 12 часов. Работа была выполнена в среде разработки Visual Studio Code. Программа написана на Python версии 3.13. + В качестве направлений масштабирования можно предложить добавление составных операторов присваивания (\texttt{+=}, \texttt{-=}, \texttt{*=}, \texttt{/=}), для которых генерируется схожая последовательность низкоуровневых команд. Также возможна реализация оптимизаций генерируемого кода, таких как устранение избыточных команд в простых случаях. + + На выполнение лабораторной работы ушло около 6 часов. Работа была выполнена в среде разработки Visual Studio Code. \newpage \section*{Список литературы}