625 lines
44 KiB
TeX
625 lines
44 KiB
TeX
\documentclass[a4paper, final]{article}
|
||
%\usepackage{literat} % Нормальные шрифты
|
||
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
|
||
\usepackage{tabularx}
|
||
\usepackage[T2A]{fontenc}
|
||
\usepackage[utf8]{inputenc}
|
||
\usepackage[russian]{babel}
|
||
\usepackage{amsmath}
|
||
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
|
||
\usepackage{ragged2e} %для растягивания по ширине
|
||
\usepackage{setspace} %для межстрочно го интервала
|
||
\usepackage{moreverb} %для работы с листингами
|
||
\usepackage{indentfirst} % для абзацного отступа
|
||
\usepackage{moreverb} %для печати в листинге исходного кода программ
|
||
\usepackage{pdfpages} %для вставки других pdf файлов
|
||
\usepackage{tikz}
|
||
\usepackage{graphicx}
|
||
\usepackage{afterpage}
|
||
\usepackage{longtable}
|
||
\usepackage{float}
|
||
|
||
|
||
|
||
% \usepackage[paper=A4,DIV=12]{typearea}
|
||
\usepackage{pdflscape}
|
||
% \usepackage{lscape}
|
||
|
||
\usepackage{array}
|
||
\usepackage{multirow}
|
||
|
||
\renewcommand\verbatimtabsize{4\relax}
|
||
\renewcommand\listingoffset{0.2em} %отступ от номеров строк в листинге
|
||
\renewcommand{\arraystretch}{1.4} % изменяю высоту строки в таблице
|
||
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} %для настройки заголовка таблицы
|
||
\usepackage{listings} %листинги
|
||
\usepackage{xcolor} % цвета
|
||
\usepackage{hyperref}% для гиперссылок
|
||
\usepackage{enumitem} %для перечислений
|
||
|
||
\newcommand{\specialcell}[2][l]{\begin{tabular}[#1]{@{}l@{}}#2\end{tabular}}
|
||
|
||
|
||
\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях
|
||
|
||
\hypersetup{colorlinks,
|
||
allcolors=[RGB]{010 090 200}} %красивые гиперссылки (не красные)
|
||
|
||
% подгружаемые языки — подробнее в документации listings (это всё для листингов)
|
||
\lstloadlanguages{ SQL}
|
||
% включаем кириллицу и добавляем кое−какие опции
|
||
\lstset{tabsize=2,
|
||
breaklines,
|
||
basicstyle=\footnotesize,
|
||
columns=fullflexible,
|
||
flexiblecolumns,
|
||
numbers=left,
|
||
numberstyle={\footnotesize},
|
||
keywordstyle=\color{blue},
|
||
inputencoding=cp1251,
|
||
extendedchars=true
|
||
}
|
||
\lstdefinelanguage{MyC}{
|
||
language=SQL,
|
||
% ndkeywordstyle=\color{darkgray}\bfseries,
|
||
% identifierstyle=\color{black},
|
||
% morecomment=[n]{/**}{*/},
|
||
% commentstyle=\color{blue}\ttfamily,
|
||
% stringstyle=\color{red}\ttfamily,
|
||
% morestring=[b]",
|
||
% showstringspaces=false,
|
||
% morecomment=[l][\color{gray}]{//},
|
||
keepspaces=true,
|
||
escapechar=\%,
|
||
texcl=true
|
||
}
|
||
|
||
\textheight=24cm % высота текста
|
||
\textwidth=16cm % ширина текста
|
||
\oddsidemargin=0pt % отступ от левого края
|
||
\topmargin=-1.5cm % отступ от верхнего края
|
||
\parindent=24pt % абзацный отступ
|
||
\parskip=5pt % интервал между абзацами
|
||
\tolerance=2000 % терпимость к "жидким" строкам
|
||
\flushbottom % выравнивание высоты страниц
|
||
|
||
|
||
% Настройка листингов
|
||
\lstset{
|
||
language=python,
|
||
extendedchars=\true,
|
||
inputencoding=utf8,
|
||
keepspaces=true,
|
||
% captionpos=b, % подписи листингов снизу
|
||
}
|
||
|
||
\begin{document} % начало документа
|
||
|
||
|
||
|
||
% НАЧАЛО ТИТУЛЬНОГО ЛИСТА
|
||
\begin{center}
|
||
\hfill \break
|
||
\hfill \break
|
||
\normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
|
||
федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
|
||
\normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt]
|
||
\normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt]
|
||
\normalsize{Направление: 02.03.01 <<Математика и компьютерные науки>>}\\
|
||
|
||
\hfill \break
|
||
\hfill \break
|
||
\hfill \break
|
||
\hfill \break
|
||
\large{Лабораторная работа №1}\\
|
||
\large{<<Создание лексического анализатора>>}\\
|
||
\large{по дисциплине}\\
|
||
\large{<<Математическая логика>>}\\
|
||
\large{Вариант 15}\\
|
||
|
||
% \hfill \break
|
||
\hfill \break
|
||
\end{center}
|
||
|
||
\small{
|
||
\begin{tabular}{lrrl}
|
||
\!\!\!Студент, & \hspace{2cm} & & \\
|
||
\!\!\!группы 5130201/20102 & \hspace{2cm} & \underline{\hspace{3cm}} &Тищенко А. А. \\\\
|
||
\!\!\!Преподаватель & \hspace{2cm} & \underline{\hspace{3cm}} & Востров А. В. \\\\
|
||
&&\hspace{4cm}
|
||
\end{tabular}
|
||
\begin{flushright}
|
||
<<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2025г.
|
||
\end{flushright}
|
||
}
|
||
|
||
\hfill \break
|
||
% \hfill \break
|
||
\begin{center} \small{Санкт-Петербург, 2025} \end{center}
|
||
\thispagestyle{empty} % выключаем отображение номера для этой страницы
|
||
|
||
% КОНЕЦ ТИТУЛЬНОГО ЛИСТА
|
||
\newpage
|
||
|
||
\tableofcontents
|
||
|
||
|
||
\newpage
|
||
|
||
\section*{Введение}
|
||
\addcontentsline{toc}{section}{Введение}
|
||
Лабораторная №1 по дисциплине <<Математическая логика>> заключается в следующем. Необходимо написать программу, которая выполняет лексический анализ входного текста в
|
||
соответствии с вариантом задания и порождает таблицу лексем с указанием их типов и значений.
|
||
Также необходимо подготовить несколько вариантов программы в виде текста на входном языке.
|
||
Программа должна выдавать сообщения о наличие во входном тексте ошибок, которые
|
||
могут быть обнаружены на этапе лексического анализа.
|
||
Длина идентификатора и строковых констант ограничена 16 символами, только
|
||
латиница. Программа должна допускать наличие комментариев неограниченной длины
|
||
во входном файле.
|
||
|
||
\textit{Вариант 15}. Входной язык содержит арифметические выражения, разделенные символом |
|
||
(вертикальная полоса). Арифметические выражения состоят из идентификаторов,
|
||
комплексных чисел (в показательной форме), знака присваивания (:=), знаков операций +,
|
||
–, *, /, \textasciicircum и круглых скобок.
|
||
|
||
|
||
\newpage
|
||
\section {Математическое описание}
|
||
|
||
\subsection{Структура транслятора}
|
||
|
||
Транслятор выполняет преобразование исходного текста $L$ на каком-либо языке в какую-либо структуру данных с сохранением смысла входного текста.
|
||
|
||
Транслятор можно разделить на три основные части (см. Рис.~\ref{fig:trs}):
|
||
|
||
\begin{itemize}
|
||
\item Лексический анализатор -- представляет исходную программу как последовательность лексем. Лексемы - минимальные единицы языка, имеющие смысл.
|
||
\item Распознаватель -- строит структуру исходного текста (например, в виде дерева) по полученной от лексического анализатора цепочке лексем.
|
||
\item Генератор -- использует построенную структуру для создания выходных данных, отражающих семантику входной цепочки.
|
||
\end{itemize}
|
||
|
||
\begin{figure}[h!]
|
||
\centering
|
||
\includegraphics[width=0.8\linewidth]{img/trs.png}
|
||
\caption{Структура транслятора.}
|
||
\label{fig:trs}
|
||
\end{figure}
|
||
|
||
В данной лабораторной работе необходимо написать программу лексический анализатор для заданного языка.
|
||
|
||
\subsection{Формальное определение лексического анализа}
|
||
|
||
Лексический анализ --- первая фаза трансляции программ на языках программирования, цель которой состоит в преобразовании входного текста программы в последовательность структурно значимых элементов --- \textit{лексем}.
|
||
|
||
Лексема --- это минимальная единица языка программирования, обладающая самостоятельным смыслом. В отличие от естественных языков, где лексемами выступают слова или словоформы, в языках программирования лексемами являются имена, ключевые (служебные) слова, числовые и строковые константы, а также операторы (в том числе составные, например, \verb|:=|).
|
||
|
||
Цели лексического анализа включают:
|
||
\begin{itemize}
|
||
\item определение класса каждой лексемы (например, идентификатор, число, строка, оператор и т.п.);
|
||
\item определение значения лексемы;
|
||
\item преобразование значений некоторых лексем (например, чисел) во внутреннее представление;
|
||
\item фильтрация и классификация символов для облегчения анализа.
|
||
\end{itemize}
|
||
|
||
В зависимости от класса, значение лексемы может быть преобразовано во внутреннее представление уже на этапе лексического анализа. Например, числа преобразуют в двоичное машинное представление, что обеспечивает более компактное хранение и проверку правильности диапазона на ранней стадии трансляции.
|
||
|
||
Перед лексическим анализом производится этап транслитерации, на котором каждому символу сопоставляется его класс. Типичные классы символов:
|
||
\begin{itemize}
|
||
\item \textit{буква} --- символы алфавита;
|
||
\item \textit{цифра} --- символы от \verb|0| до \verb|9|;
|
||
\item \textit{разделитель} --- пробел, перевод строки, табуляция и др.;
|
||
\item \textit{игнорируемый} --- символы, не несущие смысловой нагрузки (например, сигнальные коды);
|
||
\item \textit{запрещённый} --- символы, не входящие в алфавит языка;
|
||
\item \textit{прочие} --- остальные символы, не попавшие в предыдущие категории.
|
||
\end{itemize}
|
||
|
||
Для данного варианта лабораторной работы были выбраны следующие классы лексем:
|
||
|
||
\begin{enumerate}
|
||
\item \textbf{Идентификаторы} --- последовательности латинских букв, цифр и символов нижнего подчёркивания. Не могут начинаться с цифры. Максимальная длина идентификатора --- 16 символов. Примеры: \texttt{X}, \texttt{alpha1}, \texttt{\_private}.
|
||
|
||
\item \textbf{Комплексные числа в показательной форме} --- числа вида \texttt{aEb}, где \texttt{a} и \texttt{b} --- действительные числа (целые или с плавающей точкой), знак 'E' указывает на показатель степени. Примеры: \texttt{1.5E3}, \texttt{-2E-4}, \texttt{3.0E+2}.
|
||
|
||
\item \textbf{Оператор присваивания} --- составной символ \texttt{:=}, обозначающий операцию присваивания значения.
|
||
|
||
\item \textbf{Арифметические операторы} --- знаки \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{\^} для выполнения соответствующих математических операций.
|
||
|
||
\item \textbf{Скобки} --- круглые скобки \texttt{(} и \texttt{)}, используемые для задания порядка вычислений в арифметических выражениях.
|
||
|
||
\item \textbf{Разделитель выражений} --- символ вертикальной черты \texttt{|}, отделяющий одно арифметическое выражение от другого.
|
||
|
||
\item \textbf{Комментарии} --- начинаются с символа \texttt{\#} и продолжаются до конца строки. Комментарии могут быть произвольной длины и содержать любые символы кроме переноса строки. Пример: \texttt{\# это комментарий}.
|
||
\item \textbf{Ошибки} — это случаи, когда входной текст нарушает правила формирования лексем. К таким ошибкам относятся идентификаторы, длина которых превышает 16 символов, идентификаторы, начинающиеся с цифры, и прочие неопознанные лексемы.
|
||
\end{enumerate}
|
||
|
||
|
||
\subsection{Формальная модель лексического анализатора}
|
||
Лексический анализатор (лексер) — это конечный автомат, который преобразует входную строку символов в последовательность токенов. Формально его можно описать следующим образом:
|
||
|
||
Пусть заданы:
|
||
\begin{itemize}
|
||
\item $\Sigma$ — входной алфавит (множество допустимых символов)
|
||
\item $T$ — множество типов токенов
|
||
\item $D$ — множество допустимых значений токенов
|
||
\end{itemize}
|
||
|
||
Тогда лексический анализатор реализует отображение:
|
||
\[
|
||
F_{\text{lexer}} : \Sigma^* \rightarrow (T \times D)^*
|
||
\]
|
||
|
||
где:
|
||
\begin{itemize}
|
||
\item $\Sigma^*$ — множество всех возможных строк над алфавитом $\Sigma$
|
||
\item $(T \times D)^*$ — множество последовательностей пар (тип токена, значение)
|
||
\end{itemize}
|
||
|
||
Процесс лексического анализа можно представить как \textbf{детерминированный конечный автомат (ДКА)}:
|
||
\[
|
||
M = (Q, \Sigma, \delta, q_0, F),
|
||
\]
|
||
где:
|
||
\begin{itemize}
|
||
\item $Q$ — множество состояний автомата
|
||
\item $\delta : Q \times \Sigma \rightarrow Q$ — функция переходов
|
||
\item $q_0 \in Q$ — начальное состояние
|
||
\item $F \subseteq Q$ — множество конечных состояний
|
||
\end{itemize}
|
||
|
||
Для каждого распознанного токена $t_i$ выполняется:
|
||
\[
|
||
t_i = (\text{type}, \text{value}), \quad \text{где } \text{type} \in T, \text{value} \in D
|
||
\]
|
||
|
||
|
||
\subsection{Регулярные выражения}
|
||
Регулярные выражения -- формальный язык шаблонов для поиска и выполнения манипуляций с подстроками в тексте. Регулярное выражение - это формула (pattern, шаблон), задающая правило поиска подстрок в потоке символов.
|
||
|
||
Все лексемы языка можно описать регулярными выражениями и для каждой задать
|
||
семантику (какую функцию вызывать, если распознана эта лексема – например,
|
||
обратиться к таблице служебных слов и искать там, если нет, то ... ).
|
||
|
||
Классы лексем для этого варианта лабораторной работы определяются через регулярные выражения следующим образом.
|
||
|
||
\begin{enumerate}
|
||
\item \textbf{Идентификаторы.}
|
||
\begin{center}
|
||
$R_{\text{Identifier}}$ = [A-Za-z\_][A-Za-z\_0-9]\{0,15\}(?![A-Za-z\_0-9])
|
||
\end{center}
|
||
|
||
Первый символ — латинская буква или подчёркивание. Далее допускается до 15 символов, включая буквы, цифры и подчёркивания. В конце используется \textit{negative lookahead}, чтобы за идентификатором не следовал символ, допустимый внутри него (иначе это была бы часть более длинного идентификатора).
|
||
|
||
\item \textbf{Комплексные числа в показательной форме.}
|
||
\begin{center}
|
||
$R_{\text{Complex}}$ = [+-]?\textbackslash{}d+(?:\textbackslash{}.\textbackslash{}d+)?E[+-]?\textbackslash{}d+(?:\textbackslash{}.\textbackslash{}d+)?(?!E)
|
||
\end{center}
|
||
|
||
Опциональный знак в начале числа, целое или десятичное число до \texttt{E}, затем обязательный символ \texttt{E}, снова опциональный знак, и целое или десятичное число. В конце используется \textit{negative lookahead}, чтобы за числом не мог сразу следовать символ \texttt{E}.
|
||
|
||
\item \textbf{Оператор присваивания.}
|
||
\begin{center}
|
||
$R_{\text{Assign}}$ = \textbackslash{}:=(?![\textbackslash{}:=])
|
||
\end{center}
|
||
|
||
\item \textbf{Арифметические операторы.}
|
||
\begin{center}
|
||
$R_{\text{ArithmeticOp}}$ = [+\textbackslash{}-\textbackslash{}*/\textbackslash{}\^{}]
|
||
\end{center}
|
||
|
||
\item \textbf{Скобки.}
|
||
\begin{center}
|
||
$R_{\text{Paren}}$ = [()]
|
||
\end{center}
|
||
|
||
\item \textbf{Разделитель выражений.}
|
||
\begin{center}
|
||
$R_{\text{Separator}}$ = \textbackslash{}\textbar
|
||
\end{center}
|
||
|
||
\item \textbf{Комментарии.}
|
||
\begin{center}
|
||
$R_{\text{Comment}}$ = \textbackslash{}\#.*
|
||
\end{center}
|
||
\end{enumerate}
|
||
|
||
В класс ошибок попадают все лексемы, неподошедшие ни под какой другой класс. Некорректная лексема считывается с помощью следующего регулярного выражения:
|
||
\begin{center}
|
||
$R_{\text{Error}}$ = \textbackslash{}S+
|
||
\end{center}
|
||
Это регулярное выражение считывает все непробельные символы до первого пробельного символа.
|
||
|
||
\subsection{Алгоритм лексического анализа}
|
||
Алгоритм лексического анализа состоит из следующих шагов:
|
||
\begin{enumerate}
|
||
\item \textbf{Инициализация:} указатель устанавливается на начало строки.
|
||
\item \textbf{Определение типа лексемы:} регулярные выражения, соответствующие различным типам лексем, перебираются в цикле до первого совпадения. Если регулярное выражение не найдено, то применяется регулярное выражение $R_{\text{Error}}$.
|
||
\item \textbf{Извлечение лексемы:} с помощью регулярного выражения лексема извлекается из строки и сохраняется в списке лексем с указанием типа лексемы.
|
||
\item \textbf{Вычисление значения лексемы:} значение каждой лексемы определяется посредством вызова функции обработчика, соответствующей типу лексемы. Функция обработчик получает на вход текст лексемы и возвращает некоторый объект, представляющий её значение.
|
||
\item \textbf{Сохранение лексемы:} текст лексемы, её тип и значение сохраняются в итоговый список лексем.
|
||
\item \textbf{Сдвиг указателя:} указатель сдвигается на следующий непросмотренный символ.
|
||
\item \textbf{Завершение анализа:} алгоритм завершается, когда указатель сдвигается за пределы строки. То есть, когда вся строка разобрана на лексемы.
|
||
\end{enumerate}
|
||
|
||
Сложность такого алгортма без учёта сложности функций вычисления значений лексем -- $O(n)$, где $n$ -- количество символов в исходном тексте программы.
|
||
|
||
\subsection{Синтаксическая диаграмма}
|
||
|
||
Синтаксическая диаграмма реализованной программы представлена на Рис.~\ref{fig:diag}.
|
||
|
||
\begin{figure}[h!]
|
||
\centering
|
||
\includegraphics[width=0.9\linewidth]{img/diag.png}
|
||
\caption{Синтаксическая диаграмма реализованной программы.}
|
||
\label{fig:diag}
|
||
\end{figure}
|
||
|
||
\newpage
|
||
\phantom{text}
|
||
\newpage
|
||
\section{Особенности реализации}
|
||
\subsection{Общая структура программы}
|
||
Программа состоит из двух файлов:
|
||
\begin{itemize}
|
||
\item \texttt{lexer.py} -- содержит небольшую библиотеку для создания лексических анализаторов для произвольных языков. Файл содержит классы \texttt{Lexem}, \texttt{LexemeType} и \texttt{Lexer}.
|
||
\item \texttt{main.py} -- файл содержит список типов лексем и их регулярных выражений -- (\texttt{LEXEME\_TYPES}), а также два обработчика для вычисления значений лексем -- \texttt{exp\_form\_to\_complex} и \texttt{IdentifierMapper}. Также в файле определены функции: \texttt{analyze\_and\_print\_table} -- запускает лексический анализ и выводит результаты в консоль в виде таблицы, \texttt{main} -- обрабатывает пользовательский ввод.
|
||
\end{itemize}
|
||
|
||
\subsection{Класс Lexem}
|
||
|
||
Класс Lexem это простой датакласс для представления лексем. Код определения класса представлен в листинге~\ref{lst:Lexem}. В классе есть всего три поля строкового типа:
|
||
\texttt{text} -- текст лексемы, \texttt{type\_name} -- название типа лексемы, \texttt{value} -- значение лексемы.
|
||
|
||
\begin{lstlisting}[caption={Определение класса Lexem.}, label={lst:Lexem}]
|
||
@dataclass
|
||
class Lexem:
|
||
text: str
|
||
type_name: str
|
||
value: str
|
||
\end{lstlisting}
|
||
|
||
|
||
\subsection{Класс LexemeType}
|
||
Класс LexemeType представляет собой тип лексемы. Код определения класса представлен в листинге~\ref{lst:LexemeType}. В классе всего три поля:
|
||
\texttt{name} -- строка с названием типа лексемы, \texttt{regex} -- регулярное выражение, соответствующее типу лексемы, \texttt{value\_func} -- функция для вычисления значения лексемы.
|
||
|
||
В классе определён единственный метод \texttt{consume}. Он принимает два параметра: \texttt{self} -- ссылку на объект класса, и строку \texttt{text} с текстом программы, из которого нужно попытаться извлечь лексему. Возвращает кортеж из двух элементов: объект класса Lexem, если лексему удалось извлечь, иначе None, и строку с текстом программы, оставшимся после извлечения лексемы.
|
||
|
||
\begin{lstlisting}[caption={Определение класса LexemeType.}, label={lst:LexemeType}]
|
||
class LexemeType:
|
||
def __init__(
|
||
self,
|
||
name: str,
|
||
pattern: str,
|
||
value_func: Callable[[str], str] = lambda _: "",
|
||
):
|
||
self.name = name
|
||
self.regex = re.compile(r"\s*(" + pattern + ")")
|
||
self.value_func = value_func
|
||
|
||
def consume(self, text: str) -> tuple[Lexem | None, str]:
|
||
match = self.regex.match(text)
|
||
if match:
|
||
lexeme_text = match.group(1)
|
||
value = self.value_func(lexeme_text)
|
||
rest = text[match.end() :]
|
||
return Lexem(lexeme_text, self.name, value), rest
|
||
return None, text
|
||
\end{lstlisting}
|
||
|
||
\subsection{Класс Lexer}
|
||
Класс Lexer представляет собой лексический анализатор. Код определения класса представлен в листинге~\ref{lst:Lexer}. В классе всего одно поле:
|
||
\texttt{lexeme\_types} -- список объектов класса \texttt{LexemeType}.
|
||
|
||
В классе определено два метода: \texttt{analyze} и вспомогательный \texttt{\_consume\_error}.
|
||
|
||
Метод \texttt{analyze} выполняет лексический разбор входного текста. Он принимает строку \texttt{text}, содержащую текст программы, и возвращает список объектов типа \texttt{Lexem}. Метод поочерёдно применяет каждый тип лексемы из \texttt{lexeme\_types}, пытаясь извлечь очередную лексему. Если хотя бы один тип лексемы успешно извлекает лексему, она добавляется в результат, а оставшийся текст анализируется далее. Если ни одна лексема не подошла, вызывается метод \texttt{\_consume\_error} для обработки ошибки.
|
||
|
||
Метод \texttt{\_consume\_error} используется для обработки ситуаций, когда входной фрагмент не соответствует ни одному из допустимых шаблонов. Он находит первую непробельную последовательность символов, сообщает об ошибке в консоль и создаёт лексему с типом \texttt{"ERROR"}. Возвращает эту ошибочную лексему и оставшийся текст.
|
||
|
||
\begin{lstlisting}[caption={Определение класса Lexer.}, label={lst:Lexer}]
|
||
class Lexer:
|
||
def __init__(self, lexeme_types: Iterable[LexemeType]):
|
||
self.lexeme_types = lexeme_types
|
||
|
||
def analyze(self, text: str) -> list[Lexem]:
|
||
lexems: list[Lexem] = []
|
||
while text.strip():
|
||
for lex_type in self.lexeme_types:
|
||
lexem, new_text = lex_type.consume(text)
|
||
if lexem:
|
||
lexems.append(lexem)
|
||
text = new_text
|
||
break
|
||
else:
|
||
error_lexeme, text = self._consume_error(text)
|
||
lexems.append(error_lexeme)
|
||
return lexems
|
||
|
||
def _consume_error(self, text: str) -> tuple[Lexem, str]:
|
||
match = re.match(r"\s*(\S+)", text)
|
||
err_text = match.group(1) if match else text.strip()
|
||
print(f"Недопустимая лексема: {err_text}")
|
||
rest = text[match.end() :] if match else ""
|
||
return Lexem(err_text, "ERROR", ""), rest
|
||
\end{lstlisting}
|
||
|
||
\subsection{Класс \texttt{IdentifierMapper}}
|
||
Класс \texttt{IdentifierMapper} используется для сопоставления идентификаторов с уникальными значениями. Код класса приведён в листинге~\ref{lst:IdentifierMapper}. В классе определены два поля: \texttt{id\_table: dict[str, str]}~--- таблица, содержащая отображения идентификаторов на строки с их порядковыми номерами, и \texttt{counter: int}~--- счётчик, увеличиваемый при каждом новом идентификаторе.
|
||
|
||
Метод \texttt{\_\_call\_\_(self, lex\_text: str)~$\rightarrow$~str} проверяет, был ли ранее встречен переданный идентификатор. Если нет~--- добавляет его в таблицу, присваивая уникальный номер, и возвращает соответствующее строковое представление.
|
||
|
||
\begin{lstlisting}[caption={Класс IdentifierMapper.}, label={lst:IdentifierMapper}]
|
||
class IdentifierMapper:
|
||
def __init__(self):
|
||
self.id_table = {}
|
||
self.counter = 0
|
||
|
||
def __call__(self, lex_text: str) -> str:
|
||
if lex_text not in self.id_table:
|
||
self.id_table[lex_text] = f"{lex_text} : {self.counter}"
|
||
self.counter += 1
|
||
return self.id_table[lex_text]
|
||
\end{lstlisting}
|
||
|
||
\subsection{Функция \texttt{exp\_form\_to\_complex}}
|
||
Функция \texttt{exp\_form\_to\_complex}, показанная в листинге~\ref{lst:expToComplex}, принимает один параметр \texttt{exp\_str: str}~--- строку с числом в экспоненциальной форме. Возвращает строку \texttt{str}, представляющую это число в виде комплексного числа в алгебраической форме \( a + i \cdot b \), где \( a = r \cdot \cos(\varphi) \), \( b = r \cdot \sin(\varphi) \).
|
||
|
||
\begin{lstlisting}[caption={Функция exp\_form\_to\_complex.}, label={lst:expToComplex}]
|
||
def exp_form_to_complex(exp_str: str) -> str:
|
||
base, exponent = exp_str.split("E")
|
||
r = float(base)
|
||
phi = float(exponent)
|
||
|
||
a = r * math.cos(phi)
|
||
b = r * math.sin(phi)
|
||
|
||
return f"{a:.2f} + i * {b:.2f}"
|
||
\end{lstlisting}
|
||
|
||
\subsection{Список \texttt{LEXEME\_TYPES}}
|
||
Словарь \texttt{LEXEME\_TYPES} (листинг~\ref{lst:LexemeTypes}) содержит определения всех типов лексем, используемых в лексическом анализе. Ключами являются строковые названия типов, значениями~--- экземпляры класса \texttt{LexemeType}. Каждая лексема описывается с помощью регулярного выражения и, при необходимости, функцией обработки значения (например, \texttt{IdentifierMapper} или \texttt{exp\_form\_to\_complex}).
|
||
|
||
\begin{lstlisting}[caption={Определение лексем в LEXEME\_TYPES.}, label={lst:LexemeTypes}]
|
||
LEXEME_TYPES: dict[str, LexemeType] = {
|
||
"IDENTIFIER": LexemeType(
|
||
"IDENTIFIER", r"[A-Za-z_][A-Za-z_0-9]{0,15}(?![A-Za-z_0-9])", IdentifierMapper()
|
||
),
|
||
"COMPLEX": LexemeType(
|
||
"COMPLEX",
|
||
r"[+-]?\d+(?:\.\d+)?E[+-]?\d+(?:\.\d+)?(?!E)",
|
||
exp_form_to_complex,
|
||
),
|
||
"ASSIGN": LexemeType("ASSIGN", r"\:=(?![\:=])"),
|
||
"ARITHMETIC_OP": LexemeType("ARITHMETIC_OP", r"[+\-*/^]"),
|
||
"PAREN": LexemeType("PAREN", r"[()]"),
|
||
"SEPARATOR": LexemeType("SEPARATOR", r"\|"),
|
||
"COMMENT": LexemeType("COMMENT", r"\#.*"),
|
||
}
|
||
\end{lstlisting}
|
||
|
||
\subsection{Функция \texttt{analyze\_and\_print\_table}}
|
||
Функция \texttt{analyze\_and\_print\_table}, представленная в листинге~\ref{lst:AnalyzePrint}, принимает один параметр \texttt{code: str}~--- строку с текстом программы. Она создаёт объект \texttt{Lexer}, выполняет анализ текста на лексемы и выводит результат в виде таблицы, используя библиотеку \texttt{PrettyTable}. Каждая строка таблицы содержит текст лексемы, её тип и вычисленное значение.
|
||
|
||
\begin{lstlisting}[caption={Функция analyze\_and\_print\_table.}, label={lst:AnalyzePrint}]
|
||
def analyze_and_print_table(code: str):
|
||
lexer = Lexer(LEXEME_TYPES.values())
|
||
lexemes = lexer.analyze(code)
|
||
|
||
table = PrettyTable(["Лексема", "Тип лексемы", "Значение"])
|
||
for l in lexemes:
|
||
table.add_row([l.text, l.type_name, l.value])
|
||
|
||
print(table)
|
||
print()
|
||
\end{lstlisting}
|
||
|
||
\subsection{Функция \texttt{main}}
|
||
Функция \texttt{main} (листинг~\ref{lst:Main}) организует интерактивный интерфейс пользователя. Не принимает параметров. Пользователь вводит строку \texttt{file\_name}, содержащую путь к файлу. Если файл существует, его содержимое читается и передаётся в функцию \texttt{analyze\_and\_print\_table}. При вводе строки \texttt{"exit"} программа завершает выполнение. Если файл не найден, пользователю выводится сообщение об ошибке.
|
||
|
||
\begin{lstlisting}[caption={Функция main.}, label={lst:Main}]
|
||
def main():
|
||
while True:
|
||
file_name = input(
|
||
"Введите название файла для анализа (или 'exit' для выхода): "
|
||
)
|
||
|
||
if file_name.lower() == "exit":
|
||
print("Завершаю программу.")
|
||
break
|
||
|
||
if not os.path.isfile(file_name):
|
||
print(f"Файл '{file_name}' не найден. Попробуйте снова.")
|
||
continue
|
||
|
||
with open(file_name, "r", encoding="utf-8") as file:
|
||
code = file.read()
|
||
|
||
analyze_and_print_table(code)
|
||
\end{lstlisting}
|
||
|
||
\newpage
|
||
\section{Результаты работы программы}
|
||
На Рис.~\ref{fig:result1}-\ref{fig:result3} представлены результаты работы лексического анализатора, запущенного на файлах с тремя различными программами (см. листинги~\ref{lst:prog1}-\ref{lst:prog3}).
|
||
|
||
\begin{lstlisting}[caption={Программа 1.}, label={lst:prog1}]
|
||
alpha:=2E3.1415|beta:=4E0|theta:=alpha+beta+-3E-1.57
|
||
| dzeta := alpha + (2.1E2.1 - 22E2)
|
||
\end{lstlisting}
|
||
|
||
\begin{figure}[h!]
|
||
\centering
|
||
\includegraphics[width=0.6\linewidth]{img/result1.png}
|
||
\caption{Результат работы лексического анализатора на программе 1 (листинг~\ref{lst:prog1}).}
|
||
\label{fig:result1}
|
||
\end{figure}
|
||
|
||
\begin{lstlisting}[caption={Программа 2.}, label={lst:prog2}]
|
||
abc + +100E-3.1415
|
||
# some_id_ :=:= 100
|
||
ThisIdentifierIsTooLong :=:=
|
||
\end{lstlisting}
|
||
|
||
\begin{figure}[h!]
|
||
\centering
|
||
\includegraphics[width=0.8\linewidth]{img/result2.png}
|
||
\caption{Результат работы лексического анализатора на программе 2 (листинг~\ref{lst:prog2}).}
|
||
\label{fig:result2}
|
||
\end{figure}
|
||
|
||
\newpage
|
||
\begin{lstlisting}[caption={Программа 3.}, label={lst:prog3}]
|
||
x := 2.5E+3 + y1 |
|
||
z := 3.1E+ | # число с ошибкой
|
||
x := x + -2.5E+3 |
|
||
1_first # идентификатор с цифры
|
||
\end{lstlisting}
|
||
|
||
\begin{figure}[h!]
|
||
\centering
|
||
\includegraphics[width=0.8\linewidth]{img/result3.png}
|
||
\caption{Результат работы лексического анализатора на программе 3 (листинг~\ref{lst:prog3}).}
|
||
\label{fig:result3}
|
||
\end{figure}
|
||
|
||
\newpage
|
||
|
||
\begin{figure}[h!]
|
||
\centering
|
||
\includegraphics[width=0.9\linewidth]{img/wrong.png}
|
||
\caption{Реакция программы на некорректный пользовательский ввод.}
|
||
\label{fig:wrong}
|
||
\end{figure}
|
||
|
||
На Рис.~\ref{fig:wrong} представлена реакция программы на некорректный пользовательский ввод.
|
||
|
||
|
||
\newpage
|
||
\section*{Заключение}
|
||
\addcontentsline{toc}{section}{Заключение}
|
||
В ходе выполнения лабораторной работы была написана программа, которая выполняет лексический анализ входного текста в соответствии с вариантом лабораторной работы и выводит в консоль таблицу лексем с указанием их типов и значений. Также программа отдельно выводит сообщения об ошибочных лексемах.
|
||
|
||
Из достоинств выполнения лабораторной работы можно выделить структурирование кода за счёт использования ООП. Вся логика работы лексического анализатора вынесена в отдельный класс \texttt{Lexer}. Логика работы с типами лексем в класс \texttt{LexemeType}. Также в качестве достоинства можно отметить удобочитаемый вывод таблиц в консоли с помощью библиотеки \texttt{PrettyTable}.
|
||
|
||
Можно выделить два недостатка текущей реализации. Во-первых, классы \texttt{Lexer} и \texttt{LexemeType} не поддерживают лексемы, в состав которых входят пробельные символы. Это ограничение также касается и лексем из класса <<Ошибка>>, поэтому предложенный лексический анализатор не может обнаружить, например, следующую ошибку: <<\texttt{:= :=}>> -- между повторяющимися операторами присваивания находится пробельный символ. Во-вторых, алгоритм, лежащий в основе реализации лексического анализатора, не предусматривает анализ структуры входного текста, поэтому способен обнаружить ошибки только на уровне отдельных лексем.
|
||
|
||
Функционал программы несложно масштабировать. Классы \texttt{Lexer}, \texttt{LexemeType} и \texttt{Lexem} получились достаточно универсальными. Для того, чтобы добавить обработку дополнительных типов лексем, достаточно расширить словарь \texttt{LEXEME\_TYPES}. Кроме того, класс \texttt{LexemeType} позволяет использовать любые \texttt{Callable} объекты в качестве обработчиков для получения значений лексем. Таким образом код, написанный для решения конкретного варианта лабораторной работы, легко можно адаптировать для решения других вариантов.
|
||
|
||
На выполнение лабораторной работы ушло около 12 часов. Работа была выполнена в среде разработки Visual Studio Code. Программа написана на Python версии 3.13.
|
||
|
||
|
||
|
||
\newpage
|
||
\section*{Список литературы}
|
||
\addcontentsline{toc}{section}{Список литературы}
|
||
|
||
\vspace{-1.5cm}
|
||
\begin{thebibliography}{0}
|
||
\bibitem{vostrov}
|
||
Востров, А.В. Курс лекций по дисциплине <<Математическая логика>>. URL \url{https://tema.spbstu.ru/compiler/} (дата обращения 01.04.2025 г.)
|
||
\bibitem{lutz}
|
||
Лутц, М. Изучаем Python. 5-е изд. / М. Лутц. — СПб.: Питер, 2019. — 1216 с.
|
||
\bibitem{friedl}
|
||
Фридл, Дж. Регулярные выражения = Mastering Regular Expressions / Дж. Фридл. — СПб.: Питер, 2001. — 352 с. — (Библиотека программиста).
|
||
\end{thebibliography}
|
||
|
||
\end{document} |