commit 19159eb98d54b0cb30090cd63ec47e29aeb02774 Author: Arity-T Date: Tue Apr 15 15:46:24 2025 +0300 lab1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f7e924 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/*.pdf \ No newline at end of file diff --git a/lab1/.gitignore b/lab1/.gitignore new file mode 100644 index 0000000..96dc833 --- /dev/null +++ b/lab1/.gitignore @@ -0,0 +1,8 @@ +**/* +!.gitignore +!report.tex +!img +!img/** +!programm +!programm/*.py +!programm/*.txt \ No newline at end of file diff --git a/lab1/img/diag.png b/lab1/img/diag.png new file mode 100644 index 0000000..10a8e9c Binary files /dev/null and b/lab1/img/diag.png differ diff --git a/lab1/img/result1.png b/lab1/img/result1.png new file mode 100644 index 0000000..b7176bb Binary files /dev/null and b/lab1/img/result1.png differ diff --git a/lab1/img/result2.png b/lab1/img/result2.png new file mode 100644 index 0000000..da696cb Binary files /dev/null and b/lab1/img/result2.png differ diff --git a/lab1/img/result3.png b/lab1/img/result3.png new file mode 100644 index 0000000..04365a8 Binary files /dev/null and b/lab1/img/result3.png differ diff --git a/lab1/img/trs.png b/lab1/img/trs.png new file mode 100644 index 0000000..efff394 Binary files /dev/null and b/lab1/img/trs.png differ diff --git a/lab1/img/wrong.png b/lab1/img/wrong.png new file mode 100644 index 0000000..126ad73 Binary files /dev/null and b/lab1/img/wrong.png differ diff --git a/lab1/programm/example.txt b/lab1/programm/example.txt new file mode 100644 index 0000000..d17cebc --- /dev/null +++ b/lab1/programm/example.txt @@ -0,0 +1,35 @@ +# Описание языка + +# Комментарии начинаются с # + +# Идентификаторы +abcdefg SomeThing1 _yet_another__id + +# Но не больше 16 символов +ThisIdentifierIsTooLong + +# И нельзя наичнать с цифры +123NotAllowed + +# Комплексные числа в показательной форме +1.5E3 -2E-4 3.0E+2 +2E3.14 +2E2E22E2 + +# Оператор присваивания +:= + +# Нельзя дублировать операторы присваивания +:=:= :== + +# Арифметические операторы ++ - * / ^ + +# Скобки +() + +# Разделитель выражений +| + +# Пример программы +alpha:=2E3.1415|beta:=4E0|theta:=alpha+beta+-3E-1.57 + | dzeta := alpha + (2.1E2.1 - 22E2) \ No newline at end of file diff --git a/lab1/programm/lexeme_types_test.py b/lab1/programm/lexeme_types_test.py new file mode 100644 index 0000000..8a0ef44 --- /dev/null +++ b/lab1/programm/lexeme_types_test.py @@ -0,0 +1,73 @@ +import pytest + +from main import LEXEME_TYPES + + +@pytest.mark.parametrize( + "text,expected", + [ + ("X", "X"), + ("alpha1", "alpha1"), + (" result123", "result123"), + (" __private", "__private"), + ], +) +def test_identifier_valid(text, expected): + lexeme_type = LEXEME_TYPES["IDENTIFIER"] + lexem, rest = lexeme_type.consume(text) + assert lexem is not None + assert lexem.type_name == "IDENTIFIER" + assert lexem.text == expected + assert rest == "" + + +@pytest.mark.parametrize( + "text", + [ + "1alpha", + " 1alpha", + "alphatoolongidentifier123", + ":=", + "234E+234", + ], +) +def test_identifier_invalid(text): + lexeme_type = LEXEME_TYPES["IDENTIFIER"] + lexem, _ = lexeme_type.consume(text) + assert lexem is None + + +@pytest.mark.parametrize( + "text,expected", + [ + ("1.5E3", "1.5E3"), + ("1.5E3.14", "1.5E3.14"), + ("-2E-4", "-2E-4"), + (" 3.0E+2", "3.0E+2"), + ], +) +def test_exp_number_valid(text, expected): + lexeme_type = LEXEME_TYPES["EXP_NUMBER"] + lexem, rest = lexeme_type.consume(text) + assert lexem is not None + assert lexem.type_name == "EXP_NUMBER" + assert lexem.text == expected + assert rest == "" + + +@pytest.mark.parametrize( + "text", + [ + "1.5e3", + "1alpha", + ":=", + "234E++234", + "--234E234", + "234EE234", + "234E2E34", + ], +) +def test_exp_number_invalid(text): + lexeme_type = LEXEME_TYPES["EXP_NUMBER"] + lexem, _ = lexeme_type.consume(text) + assert lexem is None diff --git a/lab1/programm/lexer.py b/lab1/programm/lexer.py new file mode 100644 index 0000000..ca371c5 --- /dev/null +++ b/lab1/programm/lexer.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Callable, Iterable + + +@dataclass +class Lexem: + text: str + type_name: str + value: str + + +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 + + +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 diff --git a/lab1/programm/main.py b/lab1/programm/main.py new file mode 100644 index 0000000..2dcc1ea --- /dev/null +++ b/lab1/programm/main.py @@ -0,0 +1,98 @@ +import math +import os +from typing import Callable + +from prettytable import PrettyTable + +from lexer import LexemeType, Lexer + + +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] + + +def exp_form_to_complex(exp_str: str) -> str: + # Разделяем строку по 'E' + 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}" + + +LEXEME_TYPES: dict[str, LexemeType] = { + # 1. Идентификаторы + "IDENTIFIER": LexemeType( + "IDENTIFIER", r"[A-Za-z_][A-Za-z_0-9]{0,15}(?![A-Za-z_0-9])", IdentifierMapper() + ), + # 2. Комплексные числа в показательной форме (на самом деле — числа в экспоненциальной форме) + "COMPLEX": LexemeType( + # "COMPLEX", r"[+-]?\d+(?:\.\d+)?E[+-]?\d+(?!E)", exp_form_to_complex + "COMPLEX", + r"[+-]?\d+(?:\.\d+)?E[+-]?\d+(?:\.\d+)?(?!E)", + exp_form_to_complex, + ), + # 3. Оператор присваивания := + "ASSIGN": LexemeType("ASSIGN", r"\:=(?![\:=])"), + # 4. Арифметические операторы + "ARITHMETIC_OP": LexemeType("ARITHMETIC_OP", r"[+\-*/^]"), + # 5. Скобки + "PAREN": LexemeType("PAREN", r"[()]"), + # 6. Разделитель выражений | + "SEPARATOR": LexemeType("SEPARATOR", r"\|"), + # 7. Комментарии от # до конца строки + "COMMENT": LexemeType("COMMENT", r"\#.*"), +} + + +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() + + LEXEME_TYPES["IDENTIFIER"].value_func = IdentifierMapper() + + +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) + + +if __name__ == "__main__": + main() diff --git a/lab1/programm/requirements.txt b/lab1/programm/requirements.txt new file mode 100644 index 0000000..c7fc368 Binary files /dev/null and b/lab1/programm/requirements.txt differ diff --git a/lab1/programm/test1.txt b/lab1/programm/test1.txt new file mode 100644 index 0000000..129c178 --- /dev/null +++ b/lab1/programm/test1.txt @@ -0,0 +1,2 @@ +alpha:=2E3.1415|beta:=4E0|theta:=alpha+beta+-3E-1.57 + | dzeta := alpha + (2.1E2.1 - 22E2) \ No newline at end of file diff --git a/lab1/programm/test2.txt b/lab1/programm/test2.txt new file mode 100644 index 0000000..e81333e --- /dev/null +++ b/lab1/programm/test2.txt @@ -0,0 +1,3 @@ +abc + +100E-3.1415 +# some_id_ :=:= 100 +ThisIdentifierIsTooLong :=:= \ No newline at end of file diff --git a/lab1/programm/test3.txt b/lab1/programm/test3.txt new file mode 100644 index 0000000..8da9f78 --- /dev/null +++ b/lab1/programm/test3.txt @@ -0,0 +1,4 @@ +x := 2.5E+3 + y1 | +z := 3.1E+ | # число с ошибкой +x := x + -2.5E+3 | +1_first # идентификатор с цифры \ No newline at end of file diff --git a/lab1/report.tex b/lab1/report.tex new file mode 100644 index 0000000..3c4043f --- /dev/null +++ b/lab1/report.tex @@ -0,0 +1,625 @@ +\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} \ No newline at end of file