Compare commits
10 Commits
lab1
...
a60d2a9db7
| Author | SHA1 | Date | |
|---|---|---|---|
| a60d2a9db7 | |||
| 96c9e6db53 | |||
| 681b56b31b | |||
| d7a0e400cd | |||
| 3c09cc0e9b | |||
| 529da8d98e | |||
| c186941dce | |||
| 20782a2473 | |||
| 3963d304ec | |||
| 7030059bbb |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 20 KiB |
@@ -34,8 +34,15 @@ class LexemeType:
|
||||
|
||||
|
||||
class Lexer:
|
||||
def __init__(self, lexeme_types: Iterable[LexemeType]):
|
||||
def __init__(
|
||||
self,
|
||||
lexeme_types: Iterable[LexemeType],
|
||||
error_regex: str,
|
||||
skip_types: Iterable[str] = [],
|
||||
):
|
||||
self.lexeme_types = lexeme_types
|
||||
self.skip_types = skip_types
|
||||
self.error_regex = re.compile(r"\s*(" + error_regex + ")")
|
||||
|
||||
def analyze(self, text: str) -> list[Lexem]:
|
||||
lexems: list[Lexem] = []
|
||||
@@ -43,16 +50,16 @@ class Lexer:
|
||||
for lex_type in self.lexeme_types:
|
||||
lexem, new_text = lex_type.consume(text)
|
||||
if lexem:
|
||||
lexems.append(lexem)
|
||||
if lexem.type_name not in self.skip_types:
|
||||
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)
|
||||
match = self.error_regex.match(text)
|
||||
err_text = match.group(1) if match else text.strip()
|
||||
print(f"Недопустимая лексема: {err_text}")
|
||||
rest = text[match.end() :] if match else ""
|
||||
|
||||
@@ -2,9 +2,8 @@ import math
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from lexer import LexemeType, Lexer
|
||||
from prettytable import PrettyTable
|
||||
|
||||
|
||||
class IdentifierMapper:
|
||||
@@ -35,30 +34,34 @@ def exp_form_to_complex(exp_str: str) -> str:
|
||||
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()
|
||||
"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)",
|
||||
r"[+-]?\d+(?:\.\d+)?E[+-]?\d+(?:\.\d+)?",
|
||||
exp_form_to_complex,
|
||||
),
|
||||
# 3. Оператор присваивания :=
|
||||
"ASSIGN": LexemeType("ASSIGN", r"\:=(?![\:=])"),
|
||||
"ASSIGN": LexemeType("ASSIGN", r"\:="),
|
||||
# 4. Арифметические операторы
|
||||
"ARITHMETIC_OP": LexemeType("ARITHMETIC_OP", r"[+\-*/^]"),
|
||||
# 5. Скобки
|
||||
"PAREN": LexemeType("PAREN", r"[()]"),
|
||||
# 6. Разделитель выражений |
|
||||
# 5. Левая скобка
|
||||
"LPAREN": LexemeType("LPAREN", r"\("),
|
||||
# 6. Правая скобка
|
||||
"RPAREN": LexemeType("RPAREN", r"\)"),
|
||||
# 7. Разделитель выражений |
|
||||
"SEPARATOR": LexemeType("SEPARATOR", r"\|"),
|
||||
# 7. Комментарии от # до конца строки
|
||||
# 8. Комментарии от # до конца строки
|
||||
"COMMENT": LexemeType("COMMENT", r"\#.*"),
|
||||
}
|
||||
|
||||
ERROR_REGEX = r".[^|()+\-*/^:=\s]*"
|
||||
|
||||
|
||||
def analyze_and_print_table(code: str):
|
||||
lexer = Lexer(LEXEME_TYPES.values())
|
||||
lexer = Lexer(LEXEME_TYPES.values(), ERROR_REGEX, skip_types=["COMMENT"])
|
||||
lexemes = lexer.analyze(code)
|
||||
|
||||
table = PrettyTable(["Лексема", "Тип лексемы", "Значение"])
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
\large{Лабораторная работа №1}\\
|
||||
\large{<<Создание лексического анализатора>>}\\
|
||||
\large{по дисциплине}\\
|
||||
\large{<<Математическая логика>>}\\
|
||||
\large{<<Математическая логика и теория автоматов>>}\\
|
||||
\large{Вариант 15}\\
|
||||
|
||||
% \hfill \break
|
||||
@@ -224,7 +224,9 @@
|
||||
|
||||
\item \textbf{Арифметические операторы} --- знаки \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{\^} для выполнения соответствующих математических операций.
|
||||
|
||||
\item \textbf{Скобки} --- круглые скобки \texttt{(} и \texttt{)}, используемые для задания порядка вычислений в арифметических выражениях.
|
||||
\item \textbf{Левая скобка} --- символ \texttt{(}, используемый для задания порядка вычислений в арифметических выражениях.
|
||||
|
||||
\item \textbf{Правая скобка} --- символ \texttt{)}, используемый для задания порядка вычислений в арифметических выражениях.
|
||||
|
||||
\item \textbf{Разделитель выражений} --- символ вертикальной черты \texttt{|}, отделяющий одно арифметическое выражение от другого.
|
||||
|
||||
@@ -284,21 +286,21 @@
|
||||
\begin{enumerate}
|
||||
\item \textbf{Идентификаторы.}
|
||||
\begin{center}
|
||||
$R_{\text{Identifier}}$ = [A-Za-z\_][A-Za-z\_0-9]\{0,15\}(?![A-Za-z\_0-9])
|
||||
$R_{\text{Identifier}}$ = [A-Za-z][A-Za-z\_0-9]\{0,15\}(?![A-Za-z\_0-9])
|
||||
\end{center}
|
||||
|
||||
Первый символ — латинская буква или подчёркивание. Далее допускается до 15 символов, включая буквы, цифры и подчёркивания. В конце используется \textit{negative lookahead}, чтобы за идентификатором не следовал символ, допустимый внутри него (иначе это была бы часть более длинного идентификатора).
|
||||
Первый символ — латинская буква. Далее допускается до 15 символов, включая буквы, цифры и подчёркивания. В конце используется \textit{negative lookahead}, чтобы за идентификатором не следовал символ, допустимый внутри него (иначе это была бы часть более длинного идентификатора).
|
||||
|
||||
\item \textbf{Комплексные числа в показательной форме.}
|
||||
\begin{center}
|
||||
$R_{\text{Complex}}$ = [+-]?\textbackslash{}d+(?:\textbackslash{}.\textbackslash{}d+)?E[+-]?\textbackslash{}d+(?:\textbackslash{}.\textbackslash{}d+)?(?!E)
|
||||
$R_{\text{Complex}}$ = [+-]?\textbackslash{}d+(?:\textbackslash{}.\textbackslash{}d+)?E[+-]?\textbackslash{}d+(?:\textbackslash{}.\textbackslash{}d+)?
|
||||
\end{center}
|
||||
|
||||
Опциональный знак в начале числа, целое или десятичное число до \texttt{E}, затем обязательный символ \texttt{E}, снова опциональный знак, и целое или десятичное число. В конце используется \textit{negative lookahead}, чтобы за числом не мог сразу следовать символ \texttt{E}.
|
||||
Опциональный знак в начале числа, целое или десятичное число до \texttt{E}, затем обязательный символ \texttt{E}, снова опциональный знак, и целое или десятичное число.
|
||||
|
||||
\item \textbf{Оператор присваивания.}
|
||||
\begin{center}
|
||||
$R_{\text{Assign}}$ = \textbackslash{}:=(?![\textbackslash{}:=])
|
||||
$R_{\text{Assign}}$ = \textbackslash{}:=
|
||||
\end{center}
|
||||
|
||||
\item \textbf{Арифметические операторы.}
|
||||
@@ -306,9 +308,14 @@
|
||||
$R_{\text{ArithmeticOp}}$ = [+\textbackslash{}-\textbackslash{}*/\textbackslash{}\^{}]
|
||||
\end{center}
|
||||
|
||||
\item \textbf{Скобки.}
|
||||
\item \textbf{Левая скобка.}
|
||||
\begin{center}
|
||||
$R_{\text{Paren}}$ = [()]
|
||||
$R_{\text{LeftParen}}$ = \textbackslash{}(
|
||||
\end{center}
|
||||
|
||||
\item \textbf{Правая скобка.}
|
||||
\begin{center}
|
||||
$R_{\text{RightParen}}$ = \textbackslash{})
|
||||
\end{center}
|
||||
|
||||
\item \textbf{Разделитель выражений.}
|
||||
@@ -324,9 +331,9 @@
|
||||
|
||||
В класс ошибок попадают все лексемы, неподошедшие ни под какой другой класс. Некорректная лексема считывается с помощью следующего регулярного выражения:
|
||||
\begin{center}
|
||||
$R_{\text{Error}}$ = \textbackslash{}S+
|
||||
$R_{\text{Error}}$ = .[\textasciicircum|()+\textbackslash{}-*/\textasciicircum{}:=\textbackslash{}s]*
|
||||
\end{center}
|
||||
Это регулярное выражение считывает все непробельные символы до первого пробельного символа.
|
||||
Это регулярное выражение считывает как минимум один символ, чтобы анализ текста не завис. Затем регулярное выражение считывает все символы до первого пробела, арифметического оператора, левой или правой скобки, или оператора присваивания.
|
||||
|
||||
\subsection{Алгоритм лексического анализа}
|
||||
Алгоритм лексического анализа состоит из следующих шагов:
|
||||
@@ -407,19 +414,30 @@ class LexemeType:
|
||||
\end{lstlisting}
|
||||
|
||||
\subsection{Класс Lexer}
|
||||
Класс Lexer представляет собой лексический анализатор. Код определения класса представлен в листинге~\ref{lst:Lexer}. В классе всего одно поле:
|
||||
\texttt{lexeme\_types} -- список объектов класса \texttt{LexemeType}.
|
||||
Класс Lexer представляет собой лексический анализатор. Код определения класса представлен в листинге~\ref{lst:Lexer}. В классе определены следующие поля:
|
||||
\begin{itemize}
|
||||
\item \texttt{lexeme\_types} -- список объектов класса \texttt{LexemeType}
|
||||
\item \texttt{error\_regex} -- строка, содержащая регулярное выражение для захвата ошибочных лексем
|
||||
\item \texttt{skip\_types} -- список строк с названиями типов лексем, которые следует пропускать при анализе (например, комментарии)
|
||||
\end{itemize}
|
||||
|
||||
В классе определено два метода: \texttt{analyze} и вспомогательный \texttt{\_consume\_error}.
|
||||
|
||||
Метод \texttt{analyze} выполняет лексический разбор входного текста. Он принимает строку \texttt{text}, содержащую текст программы, и возвращает список объектов типа \texttt{Lexem}. Метод поочерёдно применяет каждый тип лексемы из \texttt{lexeme\_types}, пытаясь извлечь очередную лексему. Если хотя бы один тип лексемы успешно извлекает лексему, она добавляется в результат, а оставшийся текст анализируется далее. Если ни одна лексема не подошла, вызывается метод \texttt{\_consume\_error} для обработки ошибки.
|
||||
Метод \texttt{analyze} выполняет лексический разбор входного текста. Он принимает строку \texttt{text}, содержащую текст программы, и возвращает список объектов типа \texttt{Lexem}. Метод поочерёдно применяет каждый тип лексемы из \texttt{lexeme\_types}, пытаясь извлечь очередную лексему. Если хотя бы один тип лексемы успешно извлекает лексему, она добавляется в результат (если её тип не входит в \texttt{skip\_types}), а оставшийся текст анализируется далее. Если ни одна лексема не подошла, вызывается метод \texttt{\_consume\_error} для обработки ошибки.
|
||||
|
||||
Метод \texttt{\_consume\_error} используется для обработки ситуаций, когда входной фрагмент не соответствует ни одному из допустимых шаблонов. Он находит первую непробельную последовательность символов, сообщает об ошибке в консоль и создаёт лексему с типом \texttt{"ERROR"}. Возвращает эту ошибочную лексему и оставшийся текст.
|
||||
Метод \texttt{\_consume\_error} используется для обработки ситуаций, когда входной фрагмент не соответствует ни одному из допустимых шаблонов. Он использует \texttt{error\_regex} для поиска ошибочной последовательности символов, сообщает об ошибке в консоль и создаёт лексему с типом \texttt{"ERROR"}. Возвращает эту ошибочную лексему и оставшийся текст.
|
||||
|
||||
\begin{lstlisting}[caption={Определение класса Lexer.}, label={lst:Lexer}]
|
||||
class Lexer:
|
||||
def __init__(self, lexeme_types: Iterable[LexemeType]):
|
||||
def __init__(
|
||||
self,
|
||||
lexeme_types: Iterable[LexemeType],
|
||||
error_regex: str,
|
||||
skip_types: Iterable[str] = [],
|
||||
):
|
||||
self.lexeme_types = lexeme_types
|
||||
self.skip_types = skip_types
|
||||
self.error_regex = re.compile(r"\s*(" + error_regex + ")")
|
||||
|
||||
def analyze(self, text: str) -> list[Lexem]:
|
||||
lexems: list[Lexem] = []
|
||||
@@ -427,16 +445,16 @@ class Lexer:
|
||||
for lex_type in self.lexeme_types:
|
||||
lexem, new_text = lex_type.consume(text)
|
||||
if lexem:
|
||||
lexems.append(lexem)
|
||||
if lexem.type_name not in self.skip_types:
|
||||
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)
|
||||
match = self.error_regex.match(text)
|
||||
err_text = match.group(1) if match else text.strip()
|
||||
print(f"Недопустимая лексема: {err_text}")
|
||||
rest = text[match.end() :] if match else ""
|
||||
@@ -482,16 +500,17 @@ def exp_form_to_complex(exp_str: str) -> str:
|
||||
\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()
|
||||
"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)",
|
||||
r"[+-]?\d+(?:\.\d+)?E[+-]?\d+(?:\.\d+)?",
|
||||
exp_form_to_complex,
|
||||
),
|
||||
"ASSIGN": LexemeType("ASSIGN", r"\:=(?![\:=])"),
|
||||
"ASSIGN": LexemeType("ASSIGN", r"\:="),
|
||||
"ARITHMETIC_OP": LexemeType("ARITHMETIC_OP", r"[+\-*/^]"),
|
||||
"PAREN": LexemeType("PAREN", r"[()]"),
|
||||
"LPAREN": LexemeType("LPAREN", r"\("),
|
||||
"RPAREN": LexemeType("RPAREN", r"\)"),
|
||||
"SEPARATOR": LexemeType("SEPARATOR", r"\|"),
|
||||
"COMMENT": LexemeType("COMMENT", r"\#.*"),
|
||||
}
|
||||
@@ -502,7 +521,7 @@ LEXEME_TYPES: dict[str, LexemeType] = {
|
||||
|
||||
\begin{lstlisting}[caption={Функция analyze\_and\_print\_table.}, label={lst:AnalyzePrint}]
|
||||
def analyze_and_print_table(code: str):
|
||||
lexer = Lexer(LEXEME_TYPES.values())
|
||||
lexer = Lexer(LEXEME_TYPES.values(), ERROR_REGEX, skip_types=["COMMENT"])
|
||||
lexemes = lexer.analyze(code)
|
||||
|
||||
table = PrettyTable(["Лексема", "Тип лексемы", "Значение"])
|
||||
@@ -511,6 +530,8 @@ def analyze_and_print_table(code: str):
|
||||
|
||||
print(table)
|
||||
print()
|
||||
|
||||
LEXEME_TYPES["IDENTIFIER"].value_func = IdentifierMapper()
|
||||
\end{lstlisting}
|
||||
|
||||
\subsection{Функция \texttt{main}}
|
||||
@@ -576,7 +597,7 @@ x := x + -2.5E+3 |
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.8\linewidth]{img/result3.png}
|
||||
\includegraphics[width=0.7\linewidth]{img/result3.png}
|
||||
\caption{Результат работы лексического анализатора на программе 3 (листинг~\ref{lst:prog3}).}
|
||||
\label{fig:result3}
|
||||
\end{figure}
|
||||
@@ -596,7 +617,11 @@ x := x + -2.5E+3 |
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
В ходе выполнения лабораторной работы была написана программа, которая выполняет лексический анализ входного текста в соответствии с вариантом лабораторной работы и выводит в консоль таблицу лексем с указанием их типов и значений. Также программа отдельно выводит сообщения об ошибочных лексемах.
|
||||
В ходе выполнения лабораторной работы был разработан лексический анализатор для языка, поддерживающего идентификаторы до 16 символов, комплексные числа в экспоненциальной форме, оператор присваивания (\texttt{:=}), арифметические операторы (\texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{\textasciicircum}), скобки (\texttt{(}, \texttt{)}), разделитель выражений (\texttt{|}), комментарии (\texttt{\#}). Для каждого типа лексем было разработано отдельное регулярное выражение. Предложенная программная реализация лексического анализатора выводит в консоль таблицу лексем с указанием их типов и значений. Также программа отдельно выводит сообщения об ошибочных лексемах.
|
||||
|
||||
Данный язык является автоматным, так как представим в виде синтаксической диаграммы. Автоматные грамматики — самые простые из формальных грамматик. Они являются подмножеством контекстно-свободных грамматик.
|
||||
|
||||
|
||||
|
||||
Из достоинств выполнения лабораторной работы можно выделить структурирование кода за счёт использования ООП. Вся логика работы лексического анализатора вынесена в отдельный класс \texttt{Lexer}. Логика работы с типами лексем в класс \texttt{LexemeType}. Также в качестве достоинства можно отметить удобочитаемый вывод таблиц в консоли с помощью библиотеки \texttt{PrettyTable}.
|
||||
|
||||
|
||||
8
lab2/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
**/*
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img
|
||||
!img/**
|
||||
!programm
|
||||
!programm/*.py
|
||||
!programm/*.txt
|
||||
BIN
lab2/img/ka.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
lab2/img/nka.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
lab2/img/result1.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
lab2/img/wrong.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
67
lab2/programm/finite_automaton.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import random
|
||||
|
||||
|
||||
class FiniteAutomaton:
|
||||
def __init__(
|
||||
self,
|
||||
transitions: dict[str, list[tuple[str, str]]],
|
||||
initial_state: str,
|
||||
final_states: set[str],
|
||||
alphabet: set[str],
|
||||
):
|
||||
self.transitions = transitions
|
||||
self.initial_state = initial_state
|
||||
self.final_states = final_states
|
||||
self.alphabet = alphabet
|
||||
|
||||
def _get_next_state(self, current_state: str, char: str) -> str | None:
|
||||
if current_state in self.transitions:
|
||||
for transition, next_state in self.transitions[current_state]:
|
||||
if char in transition:
|
||||
return next_state
|
||||
return None
|
||||
|
||||
def process_input(self, input_string: str) -> tuple[str, list[str]]:
|
||||
current_state = self.initial_state
|
||||
transitions_path = [current_state]
|
||||
|
||||
for char in input_string:
|
||||
if char not in self.alphabet:
|
||||
return f"Символ '{char}' не из алфавита ✗", transitions_path
|
||||
|
||||
next_state = self._get_next_state(current_state, char)
|
||||
|
||||
if next_state is None:
|
||||
return "Строка не соответствует ✗", transitions_path
|
||||
|
||||
transitions_path.append(next_state)
|
||||
current_state = next_state
|
||||
|
||||
return (
|
||||
"Строка соответствует ✓"
|
||||
if current_state in self.final_states
|
||||
else "Строка не соответствует ✗"
|
||||
), transitions_path
|
||||
|
||||
def generate_random_string(
|
||||
self, stop_probability: float = 0.3
|
||||
) -> tuple[str, list[str]]:
|
||||
result = []
|
||||
current_state = self.initial_state
|
||||
path = [current_state]
|
||||
|
||||
while True:
|
||||
if (
|
||||
current_state in self.final_states
|
||||
and random.random() < stop_probability
|
||||
):
|
||||
break
|
||||
|
||||
transition, next_state = random.choice(self.transitions[current_state])
|
||||
|
||||
char = random.choice(transition)
|
||||
result.append(char)
|
||||
current_state = next_state
|
||||
path.append(current_state)
|
||||
|
||||
return "".join(result), path
|
||||
85
lab2/programm/main.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from finite_automaton import FiniteAutomaton
|
||||
|
||||
|
||||
def main():
|
||||
alphabet = set("+-0123456789.eE")
|
||||
initial_state = "S0"
|
||||
final_states = {"S2", "S3", "S5", "S7", "S10"}
|
||||
|
||||
transitions = {
|
||||
"S0": [("+-", "S1"), ("123456789", "S2"), ("0", "S3"), (".", "S6")],
|
||||
"S1": [("123456789", "S2"), ("0", "S3"), (".", "S6")],
|
||||
"S2": [("0123456789", "S2"), (".", "S5"), ("eE", "S8")],
|
||||
"S3": [("0", "S3"), ("123456789", "S4"), (".", "S5"), ("eE", "S8")],
|
||||
"S4": [("0123456789", "S4"), (".", "S5"), ("eE", "S8")],
|
||||
"S5": [("0123456789", "S5"), ("eE", "S8")],
|
||||
"S6": [("0123456789", "S7")],
|
||||
"S7": [("0123456789", "S7"), ("eE", "S8")],
|
||||
"S8": [("+-", "S9"), ("0123456789", "S10")],
|
||||
"S9": [("0123456789", "S10")],
|
||||
"S10": [("0123456789", "S10")],
|
||||
}
|
||||
|
||||
automaton = FiniteAutomaton(
|
||||
transitions=transitions,
|
||||
initial_state=initial_state,
|
||||
final_states=final_states,
|
||||
alphabet=alphabet,
|
||||
)
|
||||
|
||||
print("Конечный автомат для распознавания форматов вещественных чисел")
|
||||
print("=" * 60)
|
||||
print("Варианты команд:")
|
||||
print(" - check <строка> - проверить, соответствует ли строка автомату")
|
||||
print(
|
||||
" - gen [<вероятность_остановки>] - сгенерировать случайную строку (по умолчанию 0.3)"
|
||||
)
|
||||
print(" - q - выход из программы")
|
||||
print("=" * 60)
|
||||
|
||||
while True:
|
||||
command = input("\nВведите команду: ").strip()
|
||||
|
||||
if not command:
|
||||
continue
|
||||
|
||||
parts = command.split()
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == "q":
|
||||
print("Выход из программы.")
|
||||
break
|
||||
|
||||
elif cmd == "check":
|
||||
input_string = ""
|
||||
if len(parts) > 1:
|
||||
input_string = " ".join(parts[1:]).strip()
|
||||
|
||||
message, transitions = automaton.process_input(input_string)
|
||||
|
||||
print(f"Результат: {message}")
|
||||
print("Путь переходов:", " -> ".join(transitions))
|
||||
|
||||
elif cmd == "gen":
|
||||
stop_prob = 0.3
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
stop_prob = float(parts[1])
|
||||
if not (0 < stop_prob <= 1):
|
||||
raise ValueError(
|
||||
"Вероятность должна быть больше 0 и меньше либо равна 1"
|
||||
)
|
||||
except ValueError as e:
|
||||
print(f"Ошибка: {e}")
|
||||
continue
|
||||
|
||||
random_string, path = automaton.generate_random_string(stop_prob)
|
||||
print(f"Сгенерированная строка: {random_string}")
|
||||
print("Путь переходов:", " -> ".join(path))
|
||||
|
||||
else:
|
||||
print(f"Неизвестная команда: {cmd}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
688
lab2/report.tex
Normal file
@@ -0,0 +1,688 @@
|
||||
\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{Лабораторная работа №2}\\
|
||||
\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}{Введение}
|
||||
Лабораторная №2 по дисциплине <<Математическая логика>> заключается в следующем. Необходимо построить регулярное выражение для заданного варианта, затем создать недетерминированный конечный автомат и детерминировать его. Реализовать программу, которая проверяет введённый текст через реализацию конечного автомата, с вариантами вывода: строка соответствует, не соответствует, символы не из алфавита. Также необходимо реализовать функцию случайной генерации верной строки по полученному конечному автомату.
|
||||
|
||||
\textit{Вариант 15}. Соответствие вещественного числа разным форматам представления.
|
||||
|
||||
В данной лабораторной работе рассматриваются следующие форматы представления вещественных чисел.
|
||||
\begin{itemize}
|
||||
\item Целые числа, например: \texttt{''123''}, \texttt{''-456''}, \texttt{''0''}, в т. ч. \texttt{''+0''}, \texttt{''-0''}.
|
||||
\item Десятичные числа с разделителем точка, например: \texttt{''123.456''}, \texttt{''-456.789''}, \texttt{''.5''}.
|
||||
\item Экспоненциальная форма (буква E может быть как в верхнем, так и в нижнем регистре), например: \texttt{''1.23E4''}, \texttt{''-4.56e-7''}, \texttt{''7e8''}.
|
||||
\end{itemize}
|
||||
|
||||
Формальное определение синтаксиса предложенного формата вещественных чисел в БНФ нотации:
|
||||
\begin{verbatim}
|
||||
real ::= [sign] (integer | float | exponentnumber)
|
||||
sign ::= "+" | "-"
|
||||
|
||||
integer ::= nonzerodigit {digit} | "0" {"0"}
|
||||
nonzerodigit ::= "1" | "2" | ... | "9"
|
||||
digit ::= "0" | nonzerodigit
|
||||
|
||||
float ::= {digit} "." digitpart | digitpart "."
|
||||
digitpart ::= digit {digit}
|
||||
|
||||
exponentnumber ::= (digitpart | float) exponent
|
||||
exponent ::= ("e" | "E") [sign] digitpart
|
||||
\end{verbatim}
|
||||
Где:
|
||||
\begin{itemize}
|
||||
\item \lbrack R\rbrack -- необязательный элемент (0 или 1 раз)
|
||||
\item \{R\} -- повторение элемента (0 или более раз)
|
||||
\item P|Q -- альтернатива (либо P, либо Q)
|
||||
\end{itemize}
|
||||
|
||||
|
||||
\newpage
|
||||
\section {Математическое описание}
|
||||
\subsection{Языки и грамматики}
|
||||
Языком над конечным словарем $\Sigma$ называется произвольное множество конечных цепочек над этим словарем.
|
||||
|
||||
\begin{itemize}
|
||||
\item Цепочки языка называются словами (предложениями).
|
||||
\item Над конечным непустым словарем можно определить бесконечное
|
||||
количество слов конечной длины (счетное множество).
|
||||
\item Над конечным
|
||||
непустым словарем можно определить бесконечное количество языков, т.е.
|
||||
подмножество множества всех возможных слов
|
||||
(континуум, как число подмножеств счетного множества).
|
||||
\end{itemize}
|
||||
|
||||
Языки могут быть конечными и бесконечными (содержать бесконечное число цепочек). Словарь всегда конечен.
|
||||
|
||||
Формальная грамматика – способ описания того, какие предложения возможны в языке. Существует два вида грамматик:
|
||||
\begin{itemize}
|
||||
\item порождающие грамматики – правила, позволяющие
|
||||
строить любое предложение языка,
|
||||
\item распознающие грамматики (алгоритмы) - позволяют
|
||||
определить, принадлежит ли данное предложение языку.
|
||||
\end{itemize}
|
||||
|
||||
Распознающая грамматика – это конечный набор
|
||||
правил, алгоритм, который по введенной цепочке
|
||||
определяет, принадлежит цепочка языку, или нет.
|
||||
|
||||
\subsection{Регулярные множества}
|
||||
Регулярные множества, как множества цепочек, построенные над конечным словарем (по определенным правилам) – это языки. Их называют регулярными языками.
|
||||
|
||||
Правила построения регулярных множеств:
|
||||
\begin{itemize}
|
||||
\item Объединение двух регулярных множеств $L_1$ и $L_2$ обозначается $L_1 \cup L_2$ и состоит из цепочек, которые принадлежат хотя бы одному из множеств $L_1$ или $L_2$.
|
||||
$$L_1 \cup L_2 = \{ \alpha \mid \alpha \in L_1 \text{ или } \alpha \in L_2 \}$$
|
||||
|
||||
\item Конкатенация (произведение) двух регулярных множеств $L_1$ и $L_2$ обозначается $L_1 \cdot L_2$ и состоит из цепочек, которые можно разбить на две части, одна из которых принадлежит $L_1$, а другая $L_2$.
|
||||
$$L_1 \cdot L_2 = \{ \alpha \beta \mid \alpha \in L_1 \text{ и } \beta \in L_2 \}$$
|
||||
|
||||
Обозначим $L^0 = \{ \varepsilon \}$, $L^1 = L$, $L^2 = L \cdot L$, $L^{k + 1} = L^k \cdot L$, и так далее. Обозначение $\varepsilon$ обозначает пустую цепочку.
|
||||
|
||||
\item Итерация регулярного множества $L$ обозначается $L^*$ и состоит из цепочек, которые можно разбить на произвольное количество повторений множества $L$.
|
||||
$$L^* = \{ \varepsilon \} \cup L \cup L^2 \cup L^3 \cup \ldots$$
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Регулярные выражения}
|
||||
Регулярные выражения -- формальный язык шаблонов для поиска и выполнения манипуляций с подстроками в тексте. Регулярное выражение -- это формула (pattern, шаблон), задающая правило поиска подстрок в потоке символов.
|
||||
|
||||
Регулярное выражение показывает, как можно построить регулярное множество цепочек из одноэлементных множеств с использованием трех операций: конкатенации, объединения и итерации.
|
||||
|
||||
Примеры регулярных выражений:
|
||||
\begin{itemize}
|
||||
\item $ab + ba^*$ -- представляет регулярное множество:
|
||||
$$\{ a\} \{b \} \cup \{ b \} \{ a \}^*$$
|
||||
\item $(ac)^*b+c^*$ -- представляет регулярное множество:
|
||||
$$\{b, acb, acacb, acacacb, \ldots, \varepsilon, c, cc, ccc, \ldots \}$$
|
||||
\end{itemize}
|
||||
|
||||
В реальных программах и языках программирования используется расширенный синтаксис регулярных выражений, который добавляет множество удобных конструкций. Среди дополнительных возможностей: классы символов (например, \verb|[a-z0-9]|), квантификаторы (\verb|?|, \verb|+|, \verb|{n,m}|), группировка с помощью скобок, обратные ссылки, опережающие и ретроспективные проверки. Эти расширения делают регулярные выражения более компактными и удобными для использования, но не меняют их выразительную мощность с теоретической точки зрения.
|
||||
|
||||
Если регулярные множества это языки, то регулярные выражения -- это распознающие грамматики этих языков.
|
||||
|
||||
\subsection{Регулярные выражения для заданного варианта}
|
||||
|
||||
Для разных форматов представления вещественных чисел были построены следующие регулярные выражения в соответствии с определённой БНФ нотацией:
|
||||
\begin{itemize}
|
||||
\item Целые числа: \\
|
||||
\texttt{[+-]?(0+|[1-9][0-9]*)}
|
||||
\item Десятичные числа: \\
|
||||
\texttt{[+-]?([0-9]*\textbackslash.[0-9]+|[0-9]+\textbackslash.)}
|
||||
\item Экспоненциальная форма: \\
|
||||
\texttt{[+-]?([0-9]+|[0-9]*\textbackslash.[0-9]+|[0-9]+\textbackslash.)[eE][+-]?[0-9]+}
|
||||
\end{itemize}
|
||||
|
||||
Объединяя, получаем следующее регулярное выражение, которое распознает все форматы вещественных чисел в соответствии с БНФ нотацией:
|
||||
\begin{verbatim}
|
||||
[+-]?(
|
||||
0+|
|
||||
[1-9][0-9]*|
|
||||
[0-9]*\.[0-9]+|
|
||||
[0-9]+\.|
|
||||
([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)[eE][+-]?[0-9]+
|
||||
)
|
||||
\end{verbatim}
|
||||
Разберём структуру этого выражения:
|
||||
\begin{itemize}
|
||||
\item \texttt{[+-]?} -- необязательный знак числа (плюс или минус)
|
||||
\item \texttt{0+|[1-9][0-9]*} -- целое число, которое может быть либо последовательностью нулей, либо цифрой от 1 до 9, за которой следует произвольное количество цифр.
|
||||
\item \texttt{[0-9]*\textbackslash.[0-9]+|[0-9]+\textbackslash.} -- десятичное число, которое может быть представлено произвольным количеством цифр до и как минимум одной цифрой после точки, либо произвольным количеством цифр и одной точкой в конце.
|
||||
\item \texttt{([0-9]+|[0-9]*\textbackslash.[0-9]+|[0-9]+\textbackslash.)[eE][+-]?[0-9]+} -- экспоненциальная форма числа, состоящая из произвольного количества цифр, либо десятичного числа, за которым следует буква E (в любом регистре), необязательный знак и как минимум одна цифра.
|
||||
\end{itemize}
|
||||
|
||||
Таким образом, полученное регулярное выражение распознаёт формат представления вещественных чисел, рассматриваемый в данной работе, и полностью соответствует формальному определению, представленному в БНФ нотации.
|
||||
|
||||
\subsection{Конечный автомат-распознаватель}
|
||||
Конечный автомат-распознаватель – это математическая модель, которая используется для распознавания цепочек символов в соответствии с заданным формальным языком.
|
||||
|
||||
Конечный автомат-распознаватель $A = (S, \Sigma, s_0, \delta, F)$, где:
|
||||
\begin{itemize}
|
||||
\item $S$ – конечное множество состояний
|
||||
\item $\Sigma$ – конечное множество входных символов
|
||||
\item $s_0 \in S$ – начальное состояние
|
||||
\item $\delta: S \times \Sigma \rightarrow S$ – функция переходов
|
||||
\item $F \subseteq S$ – множество финальных (допускающих) состояний
|
||||
\end{itemize}
|
||||
|
||||
Автомат $A$ допускает (распознает) цепочку, если эта цепочка переводит $A$ из начального в одно из финальных состояний. Автомат $A$ допускает язык $L$, если он допускает все цепочки этого языка – и только их.
|
||||
|
||||
Конечный автомат-распознаватель является распознающей грамматикой.
|
||||
|
||||
\subsection{Недетерминированный КА-распознаватель}
|
||||
Недетерминизм - очень удобное свойство формальной
|
||||
модели, его можно определенным образом трактовать,
|
||||
даже и не реализовывая, ограничиваясь только
|
||||
формальными аналитическими преобразованиями.
|
||||
|
||||
В недетерминированном конечном автомате-распознавателе могут быть следующие неоднозначности:
|
||||
\begin{itemize}
|
||||
\item несколько начальных состояний,
|
||||
\item несколько переходов, помеченных одним и тем
|
||||
же символом,
|
||||
\item переходы, помеченные пустым символом $\varepsilon$.
|
||||
\end{itemize}
|
||||
|
||||
Цепочка допускается конечным автоматом, если существует путь, по которому эта цепочка переводит автомат из какого-нибудь начального состояния в какое-нибудь финальное состояние.
|
||||
|
||||
Недетерминированный конечный автомат-распознаватель $A = (S, \Sigma, S_0, \delta, F)$, где:
|
||||
\begin{itemize}
|
||||
\item $S$ – конечное множество состояний
|
||||
\item $\Sigma$ – конечное множество входов
|
||||
\item $S_0 \subseteq S$ – множество начальных состояний
|
||||
\item $\delta: S \times \Sigma \rightarrow 2^S$ – функция переходов
|
||||
\item $F \subseteq S$ – множество финальных состояний
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Теорема Клини}
|
||||
\textbf{Теорема Клини}. Классы регулярных множеств и автоматных языков совпадают. Это значит, что:
|
||||
\begin{itemize}
|
||||
\item Любой язык, распознаваемый конечным автоматом, может быть задан регулярным выражением.
|
||||
\item Для любого регулярного выражения существует конечный автомат, распознающий соответствующий язык.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Недетерминированный КА-распознаватель для заданного варианта}
|
||||
|
||||
На Рис.~\ref{fig:nka} представлен недетерминированный КА-распознаватель, соответствующий регулярному выражению для заданного варианта.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/nka.png}
|
||||
\caption{Недетерминированный КА-распознаватель для заданного варианта.}
|
||||
\label{fig:nka}
|
||||
\end{figure}
|
||||
|
||||
Матрица переходов для данного автомата представлена в Таблице~\ref{tab:nka}.
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\caption{Таблица переходов для недетерминированного КА-распознавателя.}
|
||||
\footnotesize
|
||||
\begin{tabularx}{\textwidth}{|c|X|X|X|X|X|}
|
||||
\hline
|
||||
\textbf{Состояние\textbackslash Вход} & \textbf{+-} & \textbf{0} & \textbf{1-9} & \textbf{.} & \textbf{eE} \\
|
||||
\hline
|
||||
$S_0$ & $S_1$ & $S_3$ & $S_2$ & $S_6$ & -- \\
|
||||
\hline
|
||||
$S_1$ & -- & $S_3$ & $S_2$ & $S_6$ & -- \\
|
||||
\hline
|
||||
$S_2$ & -- & $S_2$ & $S_2$ & $S_5$ & $S_8$ \\
|
||||
\hline
|
||||
$S_3$ & -- & $S_3$ & $S_4$ & $S_5$ & $S_8$ \\
|
||||
\hline
|
||||
$S_4$ & -- & $S_4$ & $S_4$ & $S_5$ & $S_8$ \\
|
||||
\hline
|
||||
$S_5$ & -- & $S_5$ & $S_5$ & -- & $S_8$ \\
|
||||
\hline
|
||||
$S_6$ & -- & $S_7$ & $S_7$ & -- & -- \\
|
||||
\hline
|
||||
$S_7$ & -- & $S_7$ & $S_7$ & -- & $S_8$ \\
|
||||
\hline
|
||||
$S_8$ & $S_9$ & $S_{10}$ & $S_{10}$ & -- & -- \\
|
||||
\hline
|
||||
$S_9$ & -- & $S_{10}$ & $S_{10}$ & -- & -- \\
|
||||
\hline
|
||||
$S_{10}$ & -- & $S_{10}$ & $S_{10}$ & -- & -- \\
|
||||
\hline
|
||||
\end{tabularx}
|
||||
\label{tab:nka}
|
||||
\end{table}
|
||||
|
||||
\subsection{Детерминированный КА-распознаватель для заданного варианта}
|
||||
|
||||
Для того, чтобы преобразовать недетерминированный КА-распознаватель (Рис.~\ref{fig:nka}) в детерминированный, достаточно добавить ещё одно состояние $S_E$, соответствующее недопустимой цепочке символов, и переходы в него из всех остальных состояний.
|
||||
|
||||
На Рис.~\ref{fig:ka} представлен детерминированный КА-распознаватель. Символом \textit{C} (от англ. Complement -- дополнение) обозначены переходы, соответствующие любым символам, кроме тех, по которым уже есть переходы в другие состояния. Символом \textit{A} (от англ. Any -- любой) обозначен переход, соответствующий любому символу.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/ka.png}
|
||||
\caption{Детерминированный КА-распознаватель для заданного варианта.}
|
||||
\label{fig:ka}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\caption{Таблица переходов для детерминированного КА-распознавателя.}
|
||||
\footnotesize
|
||||
\begin{tabularx}{\textwidth}{|c|X|X|X|X|X|}
|
||||
\hline
|
||||
\textbf{Состояние\textbackslash Вход} & \textbf{+-} & \textbf{0} & \textbf{1-9} & \textbf{.} & \textbf{eE} \\
|
||||
\hline
|
||||
$S_0$ & $S_1$ & $S_3$ & $S_2$ & $S_6$ & $S_E$ \\
|
||||
\hline
|
||||
$S_1$ & $S_E$ & $S_3$ & $S_2$ & $S_6$ & $S_E$ \\
|
||||
\hline
|
||||
$S_2$ & $S_E$ & $S_2$ & $S_2$ & $S_5$ & $S_8$ \\
|
||||
\hline
|
||||
$S_3$ & $S_E$ & $S_3$ & $S_4$ & $S_5$ & $S_8$ \\
|
||||
\hline
|
||||
$S_4$ & $S_E$ & $S_4$ & $S_4$ & $S_5$ & $S_8$ \\
|
||||
\hline
|
||||
$S_5$ & $S_E$ & $S_5$ & $S_5$ & $S_E$ & $S_8$ \\
|
||||
\hline
|
||||
$S_6$ & $S_E$ & $S_7$ & $S_7$ & $S_E$ & $S_E$ \\
|
||||
\hline
|
||||
$S_7$ & $S_E$ & $S_7$ & $S_7$ & $S_E$ & $S_8$ \\
|
||||
\hline
|
||||
$S_8$ & $S_9$ & $S_{10}$ & $S_{10}$ & $S_E$ & $S_E$ \\
|
||||
\hline
|
||||
$S_9$ & $S_E$ & $S_{10}$ & $S_{10}$ & $S_E$ & $S_E$ \\
|
||||
\hline
|
||||
$S_{10}$ & $S_E$ & $S_{10}$ & $S_{10}$ & $S_E$ & $S_E$ \\
|
||||
\hline
|
||||
\end{tabularx}
|
||||
\label{tab:ka}
|
||||
\end{table}
|
||||
|
||||
|
||||
\newpage
|
||||
Матрица переходов для данного автомата представлена в Таблице~\ref{tab:ka}.
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
\subsection{Общая структура программы}
|
||||
Программа состоит из двух файлов:
|
||||
\begin{itemize}
|
||||
\item \texttt{finite\_automaton.py} -- содержит класс \texttt{FiniteAutomaton} для создания конечных автоматов.
|
||||
\item \texttt{main.py} -- содержит определение конечного автомата для распознавания вещественных чисел, а также функцию \texttt{main}, которая реализует интерактивный интерфейс для проверки и генерации строк.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Класс \texttt{FiniteAutomaton}}
|
||||
|
||||
Класс \texttt{FiniteAutomaton} это класс для представления конечных автоматов. Код конструктора класса представлен в листинге~\ref{lst:FiniteAutomaton}.
|
||||
|
||||
В классе определены четыре поля.
|
||||
\begin{itemize}
|
||||
\item \texttt{transitions} -- \texttt{dict[str, list[tuple[str, str]]]} -- словарь, определяющий переходы между состояниями. Ключами словаря являются состояния, значениями -- списки кортежей вида \texttt{(transition, next\_state)}, определяющие переходы из данного состояния. \texttt{transition} это строка, содержащая символы, по которым можно перейти из данного состояния в следующее состояние -- \texttt{next\_state}.
|
||||
\item \texttt{initial\_state} -- \texttt{str} -- начальное состояние.
|
||||
\item \texttt{final\_states} -- \texttt{set[str]} -- множество финальных состояний.
|
||||
\item \texttt{alphabet} -- \texttt{set[str]} -- множество символов, которые может содержать входная строка.
|
||||
\end{itemize}
|
||||
|
||||
\begin{lstlisting}[caption={Код конструктора класса FiniteAutomaton.}, label={lst:FiniteAutomaton}]
|
||||
class FiniteAutomaton:
|
||||
def __init__(
|
||||
self,
|
||||
transitions: dict[str, list[tuple[str, str]]],
|
||||
initial_state: str,
|
||||
final_states: set[str],
|
||||
alphabet: set[str],
|
||||
):
|
||||
self.transitions = transitions
|
||||
self.initial_state = initial_state
|
||||
self.final_states = final_states
|
||||
self.alphabet = alphabet
|
||||
\end{lstlisting}
|
||||
|
||||
Класс \texttt{FiniteAutomaton} содержит три метода: \texttt{\_get\_next\_state}, \texttt{process\_input} и \texttt{generate\_random\_string}.
|
||||
|
||||
\subsection{Метод \texttt{\_get\_next\_state}}
|
||||
Приватный метод \texttt{\_get\_next\_state} принимает три параметра: \texttt{self} -- ссылку на объект класса, \texttt{current\_state} (\texttt{str}) -- текущее состояние, и \texttt{char} (\texttt{str}) -- символ, по которому нужно перейти из текущего состояния. Возвращает следующее состояние (\texttt{str}), либо \texttt{None}, если переход невозможен.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{\_get\_next\_state}.}, label={lst:get_next_state}]
|
||||
def _get_next_state(self, current_state: str, char: str) -> str | None:
|
||||
if current_state in self.transitions:
|
||||
for transition, next_state in self.transitions[current_state]:
|
||||
if char in transition:
|
||||
return next_state
|
||||
return None
|
||||
\end{lstlisting}
|
||||
|
||||
|
||||
\subsection{Метод \texttt{process\_input}}
|
||||
Метод \texttt{process\_input} принимает два параметра: \texttt{self} -- ссылку на объект класса, и входную строку \texttt{input\_string}. Возвращает кортеж из двух элементов: строку с сообщением о результате проверки, и список пройденных состояний.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{process\_input}.}, label={lst:process_input}]
|
||||
def process_input(self, input_string: str) -> tuple[str, list[str]]:
|
||||
current_state = self.initial_state
|
||||
transitions_path = [current_state]
|
||||
|
||||
for char in input_string:
|
||||
if char not in self.alphabet:
|
||||
return f"Символ '{char}' не из алфавита", transitions_path
|
||||
|
||||
next_state = self._get_next_state(current_state, char)
|
||||
|
||||
if next_state is None:
|
||||
return "Строка не соответствует", transitions_path
|
||||
|
||||
transitions_path.append(next_state)
|
||||
current_state = next_state
|
||||
|
||||
return (
|
||||
"Строка соответствует"
|
||||
if current_state in self.final_states
|
||||
else "Строка не соответствует"
|
||||
), transitions_path
|
||||
\end{lstlisting}
|
||||
|
||||
|
||||
\subsection{Метод \texttt{generate\_random\_string}}
|
||||
Метод \texttt{generate\_random\_string} принимает два параметра: \texttt{self} -- ссылку на объект класса, и вероятность остановки в финальном состоянии -- \texttt{stop\_probability} (\texttt{float}). Возвращает сгенерированную строку.
|
||||
|
||||
Шаг алгоритма заключается в следующем:
|
||||
\begin{enumerate}
|
||||
\item Проверяем условие остановки: если текущее состояние является финальным и случайное число оказывается меньше заданной вероятности остановки, то генерация завершается.
|
||||
\item Случайным образом выбираем переход из текущего состояния в следующее.
|
||||
\item Случайным образом выбираем символ из множества символов выбранного перехода.
|
||||
\item Добавляем выбранный символ в результат и переходим в следующее состояние.
|
||||
\end{enumerate}
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{generate\_random\_string}.}, label={lst:generate_random_string}]
|
||||
def generate_random_string(
|
||||
self, stop_probability: float = 0.3
|
||||
) -> tuple[str, list[str]]:
|
||||
result = []
|
||||
current_state = self.initial_state
|
||||
path = [current_state]
|
||||
|
||||
while True:
|
||||
if (
|
||||
current_state in self.final_states
|
||||
and random.random() < stop_probability
|
||||
):
|
||||
break
|
||||
|
||||
transition, next_state = random.choice(self.transitions[current_state])
|
||||
|
||||
char = random.choice(transition)
|
||||
result.append(char)
|
||||
current_state = next_state
|
||||
path.append(current_state)
|
||||
|
||||
return "".join(result), path
|
||||
\end{lstlisting}
|
||||
|
||||
|
||||
|
||||
\subsection{Функция \texttt{main}}
|
||||
В функции \texttt{main} (листинг~\ref{lst:Main}) заданы параметры конечного автомата и реализован интерактивный интерфейс пользователя. Функция не принимает параметров и ничего не возвращает.
|
||||
|
||||
Пользователю доступны следующие команды:
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{check <строка>} -- проверяет, соответствует ли строка автомату.
|
||||
\item \texttt{gen [<вероятность\_остановки>]} -- сгенерировать случайную строку.
|
||||
\item \texttt{q} -- выход из программы.
|
||||
\end{itemize}
|
||||
|
||||
\begin{lstlisting}[caption={Функция main.}, label={lst:Main}]
|
||||
def main():
|
||||
alphabet = set("+-0123456789.eE")
|
||||
initial_state = "S0"
|
||||
final_states = {"S2", "S3", "S5", "S7", "S10"}
|
||||
|
||||
transitions = {
|
||||
"S0": [("+-", "S1"), ("123456789", "S2"), ("0", "S3"), (".", "S6")],
|
||||
"S1": [("123456789", "S2"), ("0", "S3"), (".", "S6")],
|
||||
"S2": [("0123456789", "S2"), (".", "S5"), ("eE", "S8")],
|
||||
"S3": [("0", "S3"), ("123456789", "S4"), (".", "S5"), ("eE", "S8")],
|
||||
"S4": [("0123456789", "S4"), (".", "S5"), ("eE", "S8")],
|
||||
"S5": [("0123456789", "S5"), ("eE", "S8")],
|
||||
"S6": [("0123456789", "S7")],
|
||||
"S7": [("0123456789", "S7"), ("eE", "S8")],
|
||||
"S8": [("+-", "S9"), ("0123456789", "S10")],
|
||||
"S9": [("0123456789", "S10")],
|
||||
"S10": [("0123456789", "S10")],
|
||||
}
|
||||
|
||||
automaton = FiniteAutomaton(
|
||||
transitions=transitions,
|
||||
initial_state=initial_state,
|
||||
final_states=final_states,
|
||||
alphabet=alphabet,
|
||||
)
|
||||
|
||||
print("Конечный автомат для распознавания форматов вещественных чисел")
|
||||
print("=" * 60)
|
||||
print("Варианты команд:")
|
||||
print(" - check <строка> - проверить, соответствует ли строка автомату")
|
||||
print(
|
||||
" - gen [<вероятность_остановки>] - сгенерировать случайную строку (по умолчанию 0.3)"
|
||||
)
|
||||
print(" - q - выход из программы")
|
||||
print("=" * 60)
|
||||
|
||||
while True:
|
||||
command = input("\nВведите команду: ").strip()
|
||||
|
||||
if not command:
|
||||
continue
|
||||
|
||||
parts = command.split()
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == "q":
|
||||
print("Выход из программы.")
|
||||
break
|
||||
|
||||
elif cmd == "check":
|
||||
input_string = ""
|
||||
if len(parts) > 1:
|
||||
input_string = " ".join(parts[1:]).strip()
|
||||
|
||||
message, transitions = automaton.process_input(input_string)
|
||||
|
||||
print(f"Результат: {message}")
|
||||
print("Путь переходов:", " -> ".join(transitions))
|
||||
|
||||
elif cmd == "gen":
|
||||
stop_prob = 0.3
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
stop_prob = float(parts[1])
|
||||
if not (0 < stop_prob <= 1):
|
||||
raise ValueError(
|
||||
"Вероятность должна быть больше 0 и меньше либо равна 1"
|
||||
)
|
||||
except ValueError as e:
|
||||
print(f"Ошибка: {e}")
|
||||
continue
|
||||
|
||||
random_string, path = automaton.generate_random_string(stop_prob)
|
||||
print(f"Сгенерированная строка: {random_string}")
|
||||
print("Путь переходов:", " -> ".join(path))
|
||||
|
||||
else:
|
||||
print(f"Неизвестная команда: {cmd}")
|
||||
\end{lstlisting}
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы программы}
|
||||
Результаты работы программы представлены на Рис.~\ref{fig:result1}.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/result1.png}
|
||||
\caption{Результаты работы программы.}
|
||||
\label{fig:result1}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.8\linewidth]{img/wrong.png}
|
||||
\caption{Реакция программы на некорректный пользовательский ввод.}
|
||||
\label{fig:wrong}
|
||||
\end{figure}
|
||||
|
||||
На Рис.~\ref{fig:wrong} представлена реакция программы на некорректный пользовательский ввод.
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
В ходе выполнения лабораторной работы было построено регулярное выражение для распознавания различных форматов вещественных чисел. В соответствии с теоремой Клини по заданному регулярному выражению, задающему регулярный
|
||||
язык, был построен недетерминированный конечный автомат-распознаватель. Затем полученный конечный автомат был детерминирован. На основе разработанного автомата была реализована программа, которая проверяет соответствие входной строки заданному формату и генерирует случайные корректные строки.
|
||||
|
||||
Из достоинств выполнения лабораторной работы можно выделить структурирование кода за счёт использования ООП. Вся логика работы с конечными автоматами вынесена в отдельный класс \texttt{FiniteAutomaton} с четко разделенными методами для проверки строк и генерации случайных строк. Создана удобная интерактивная консольная оболочка для взаимодействия с пользователем, позволяющая выполнять различные команды.
|
||||
|
||||
К недостаткам текущей реализации можно отнести следующие аспекты. Во-первых, переходы в автомате представлены в виде строк, содержащих допустимые символы, такой способ представления переходов не является самым оптимальным с точки зрения производительности. Во-вторых, в реализации генерации случайных строк вероятность остановки одинакова для всех финальных состояний, что может приводить к неравномерному распределению различных форматов чисел в генерируемых строках.
|
||||
|
||||
Функционал программы несложно масштабировать. Класс \texttt{FiniteAutomaton} может быть использован для работы с различными конечными автоматами-распознавателями. Для изменения распознаваемого языка достаточно задать новые параметры для автомата, не меняя базовую логику программы. Однако, текущая реализация работает только с символьными переходами, поэтому задать строчные переходы в виде, например, регулярных выражений не представляется возможным. Однако подобный функционал также несложно реализовать, взяв за основу существующий код.
|
||||
|
||||
На выполнение лабораторной работы ушло около 10 часов. Работа была выполнена в среде разработки Visual Studio Code. Программа написана на Python версии 3.10.
|
||||
|
||||
\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}
|
||||
8
lab3/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
**/*
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img
|
||||
!img/**
|
||||
!programm
|
||||
!programm/*.py
|
||||
!programm/*.txt
|
||||
BIN
lab3/img/result1.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
lab3/img/wrong.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
3
lab3/programm/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
grammar_*
|
||||
analysis_*
|
||||
generation_*
|
||||
409
lab3/programm/grammar.py
Normal file
@@ -0,0 +1,409 @@
|
||||
import random
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
from prettytable import PrettyTable
|
||||
|
||||
|
||||
class Grammar:
|
||||
EPSILON: str = "epsilon"
|
||||
|
||||
def __init__(self, text: str):
|
||||
self.productions: OrderedDict[str, list[list[str]]] = OrderedDict()
|
||||
self.start_symbol: str = ""
|
||||
self._parse_productions(text)
|
||||
|
||||
self.terminals: set[str] = set()
|
||||
self._find_terminals()
|
||||
|
||||
self.first_sets: dict[str, set[str]] = {}
|
||||
self._calculate_first_sets()
|
||||
|
||||
self.follow_sets: dict[str, set[str]] = {}
|
||||
self._calculate_follow_sets()
|
||||
|
||||
self.lookup_table: dict[str, dict[str, list[str]]] = {}
|
||||
self._fill_lookup_table()
|
||||
|
||||
# Сопостовляем уникальный номер с каждым правилом
|
||||
self.rule_numbers = {}
|
||||
rule_idx = 1
|
||||
for nt, rules in self.productions.items():
|
||||
for rule in rules:
|
||||
self.rule_numbers[(nt, tuple(rule))] = rule_idx
|
||||
rule_idx += 1
|
||||
|
||||
def _parse_productions(self, text: str):
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
non_terminal, rule = line.split("->")
|
||||
if self.start_symbol == "":
|
||||
self.start_symbol = non_terminal.strip()
|
||||
|
||||
non_terminal = non_terminal.strip()
|
||||
rules = [
|
||||
[
|
||||
symbol.strip('"')
|
||||
for symbol in re.findall(r"\".*?\"|\S+", rule.strip())
|
||||
if symbol.strip('"') != self.EPSILON
|
||||
]
|
||||
for rule in rule.split("|")
|
||||
]
|
||||
|
||||
if non_terminal not in self.productions:
|
||||
self.productions[non_terminal] = []
|
||||
|
||||
self.productions[non_terminal].extend(rules)
|
||||
|
||||
def _find_terminals(self):
|
||||
for rules in self.productions.values():
|
||||
for rule in rules:
|
||||
for symbol in rule:
|
||||
if symbol not in self.productions:
|
||||
self.terminals.add(symbol)
|
||||
|
||||
def _calculate_first_sets(self):
|
||||
# Инициализация FIRST для всех символов
|
||||
for non_terminal in self.productions:
|
||||
self.first_sets[non_terminal] = set()
|
||||
|
||||
# Для терминалов FIRST содержит только сам терминал
|
||||
for terminal in self.terminals:
|
||||
self.first_sets[terminal] = {terminal}
|
||||
|
||||
# Вычисление FIRST для нетерминалов
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for non_terminal, rules in self.productions.items():
|
||||
for rule in rules:
|
||||
if not rule: # Пустое правило (эпсилон)
|
||||
if "" not in self.first_sets[non_terminal]:
|
||||
self.first_sets[non_terminal].add("")
|
||||
changed = True
|
||||
else:
|
||||
all_can_derive_epsilon = True
|
||||
for i, symbol in enumerate(rule):
|
||||
# Добавляем все символы из FIRST(symbol) кроме эпсилон
|
||||
first_without_epsilon = self.first_sets[symbol] - {""}
|
||||
old_size = len(self.first_sets[non_terminal])
|
||||
self.first_sets[non_terminal].update(first_without_epsilon)
|
||||
if len(self.first_sets[non_terminal]) > old_size:
|
||||
changed = True
|
||||
|
||||
# Если symbol не может порождать эпсилон, прерываем
|
||||
if "" not in self.first_sets[symbol]:
|
||||
all_can_derive_epsilon = False
|
||||
break
|
||||
|
||||
# Если все символы в правиле могут порождать эпсилон,
|
||||
# то и нетерминал может порождать эпсилон
|
||||
if (
|
||||
all_can_derive_epsilon
|
||||
and "" not in self.first_sets[non_terminal]
|
||||
):
|
||||
self.first_sets[non_terminal].add("")
|
||||
changed = True
|
||||
|
||||
def _calculate_follow_sets(self):
|
||||
# инициализировать Fo(S) = { $ }, а все остальные Fo(Ai) пустыми множествами
|
||||
for non_terminal in self.productions:
|
||||
self.follow_sets[non_terminal] = set()
|
||||
|
||||
# Добавляем символ конца строки $ для начального символа
|
||||
self.follow_sets[self.start_symbol].add("$")
|
||||
|
||||
# Повторяем, пока в наборах Follow происходят изменения
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
|
||||
# Для каждого нетерминала Aj в грамматике
|
||||
for non_terminal_j, rules in self.productions.items():
|
||||
|
||||
# Для каждого правила Aj → w
|
||||
for rule in rules:
|
||||
|
||||
# Для каждого символа в правиле
|
||||
for i, symbol in enumerate(rule):
|
||||
|
||||
# Если символ - нетерминал Ai
|
||||
if symbol in self.productions:
|
||||
# w' - остаток правила после Ai
|
||||
remainder = rule[i + 1 :] if i + 1 < len(rule) else []
|
||||
|
||||
# Если есть терминалы после Ai (w' не пусто)
|
||||
if remainder:
|
||||
# Вычисляем First(w')
|
||||
first_of_remainder = self._first_of_sequence(remainder)
|
||||
|
||||
# Если терминал a находится в First(w'), то добавляем a к Follow(Ai)
|
||||
for terminal in first_of_remainder - {""}:
|
||||
if terminal not in self.follow_sets[symbol]:
|
||||
self.follow_sets[symbol].add(terminal)
|
||||
changed = True
|
||||
|
||||
# Если ε находится в First(w'), то добавляем Follow(Aj) к Follow(Ai)
|
||||
if "" in first_of_remainder:
|
||||
old_size = len(self.follow_sets[symbol])
|
||||
self.follow_sets[symbol].update(
|
||||
self.follow_sets[non_terminal_j]
|
||||
)
|
||||
if len(self.follow_sets[symbol]) > old_size:
|
||||
changed = True
|
||||
|
||||
# Если w' пусто (Ai в конце правила), то добавляем Follow(Aj) к Follow(Ai)
|
||||
else:
|
||||
old_size = len(self.follow_sets[symbol])
|
||||
self.follow_sets[symbol].update(
|
||||
self.follow_sets[non_terminal_j]
|
||||
)
|
||||
if len(self.follow_sets[symbol]) > old_size:
|
||||
changed = True
|
||||
|
||||
def _first_of_sequence(self, sequence):
|
||||
"""Вычисляет множество FIRST для последовательности символов"""
|
||||
if not sequence:
|
||||
return {""}
|
||||
|
||||
result = set()
|
||||
all_can_derive_epsilon = True
|
||||
|
||||
for symbol in sequence:
|
||||
# Добавляем First(symbol) без эпсилон к результату
|
||||
result.update(self.first_sets[symbol] - {""})
|
||||
|
||||
# Если symbol не может порождать эпсилон, останавливаемся
|
||||
if "" not in self.first_sets[symbol]:
|
||||
all_can_derive_epsilon = False
|
||||
break
|
||||
|
||||
# Если все символы могут порождать эпсилон, добавляем эпсилон к результату
|
||||
if all_can_derive_epsilon:
|
||||
result.add("")
|
||||
|
||||
return result
|
||||
|
||||
def _fill_lookup_table(self):
|
||||
# Для каждого нетерминала A
|
||||
for non_terminal, rules in self.productions.items():
|
||||
# Формируем таблицу синтаксического анализа
|
||||
self.lookup_table[non_terminal] = {}
|
||||
|
||||
# Для каждого правила A → w
|
||||
for rule in rules:
|
||||
# Вычисляем First(w)
|
||||
first_of_rule = self._first_of_sequence(rule)
|
||||
|
||||
# Для каждого терминала a в First(w)
|
||||
for terminal in first_of_rule - {""}:
|
||||
# Если T[A,a] уже содержит правило, грамматика не LL(1)
|
||||
if terminal in self.lookup_table[non_terminal]:
|
||||
raise ValueError(
|
||||
"Грамматика не является LL(1)-грамматикой.\n"
|
||||
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||
)
|
||||
|
||||
# Добавляем правило в таблицу
|
||||
self.lookup_table[non_terminal][terminal] = rule
|
||||
|
||||
# Если эпсилон в First(w), то для каждого b в Follow(A)
|
||||
if "" in first_of_rule:
|
||||
for terminal in self.follow_sets[non_terminal]:
|
||||
# Если T[A,b] уже содержит правило, грамматика не LL(1)
|
||||
if terminal in self.lookup_table[non_terminal]:
|
||||
raise ValueError(
|
||||
"Грамматика не является LL(1)-грамматикой.\n"
|
||||
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||
)
|
||||
|
||||
# Добавляем правило в таблицу
|
||||
self.lookup_table[non_terminal][terminal] = rule
|
||||
|
||||
def format_rules(self) -> str:
|
||||
result = []
|
||||
sorted_rules = sorted(self.rule_numbers.items(), key=lambda x: x[1])
|
||||
|
||||
for rule, number in sorted_rules:
|
||||
non_terminal, symbols = rule
|
||||
rule_text = f"{number}: {non_terminal} -> {' '.join(symbols)}"
|
||||
result.append(rule_text)
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
def format_lookup_table(self) -> str:
|
||||
table = PrettyTable()
|
||||
terminals = list(self.terminals)
|
||||
table.field_names = [""] + terminals + ["$"]
|
||||
|
||||
for non_terminal in self.productions:
|
||||
row = [non_terminal]
|
||||
for terminal in terminals + ["$"]:
|
||||
if terminal in self.lookup_table[non_terminal]:
|
||||
rule = self.lookup_table[non_terminal][terminal]
|
||||
rule_num = self.rule_numbers.get((non_terminal, tuple(rule)), "")
|
||||
row.append(f"{rule_num}: {' '.join(rule)}")
|
||||
else:
|
||||
row.append(" - ")
|
||||
table.add_row(row)
|
||||
return str(table)
|
||||
|
||||
def format_first_sets(self) -> str:
|
||||
"""Форматирует множества FIRST в читаемый вид."""
|
||||
result = []
|
||||
result.append("Множества FIRST:")
|
||||
result.append("=" * 40)
|
||||
|
||||
# Сортируем для гарантии порядка вывода
|
||||
for symbol in sorted(self.first_sets.keys()):
|
||||
# Заменяем пустую строку на эпсилон для лучшей читаемости
|
||||
first_set = {
|
||||
self.EPSILON if item == "" else item for item in self.first_sets[symbol]
|
||||
}
|
||||
result.append(f"FIRST({symbol}) = {{{', '.join(sorted(first_set))}}}")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
def format_follow_sets(self) -> str:
|
||||
"""Форматирует множества FOLLOW в читаемый вид."""
|
||||
result = []
|
||||
result.append("Множества FOLLOW:")
|
||||
result.append("=" * 40)
|
||||
|
||||
# Обрабатываем только нетерминалы
|
||||
for non_terminal in sorted(self.productions.keys()):
|
||||
follow_set = self.follow_sets.get(non_terminal, set())
|
||||
result.append(
|
||||
f"FOLLOW({non_terminal}) = {{{', '.join(sorted(follow_set))}}}"
|
||||
)
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
def analyze(self, input_tokens: list[str]) -> list[int]:
|
||||
input_tokens = input_tokens.copy()
|
||||
input_tokens += ["$"]
|
||||
input_pos = 0
|
||||
|
||||
# Инициализируем стек с терминальным символом и начальным символом
|
||||
stack = ["$", self.start_symbol]
|
||||
|
||||
rules_applied = []
|
||||
|
||||
while stack:
|
||||
top = stack[-1]
|
||||
|
||||
current_symbol = (
|
||||
input_tokens[input_pos] if input_pos < len(input_tokens) else "$"
|
||||
)
|
||||
|
||||
# Случай 1: Верхний символ стека - нетерминал
|
||||
if top in self.productions:
|
||||
# Ищем правило в таблице синтаксического анализа
|
||||
if current_symbol in self.lookup_table[top]:
|
||||
# Получаем правило
|
||||
production = self.lookup_table[top][current_symbol]
|
||||
|
||||
# Удаляем нетерминал из стека
|
||||
stack.pop()
|
||||
|
||||
# Добавляем правило в rules_applied
|
||||
rule_number = self.rule_numbers[(top, tuple(production))]
|
||||
rules_applied.append(rule_number)
|
||||
|
||||
# Добавляем правило в стек в обратном порядке
|
||||
for symbol in reversed(production):
|
||||
stack.append(symbol)
|
||||
else:
|
||||
expected_symbols = list(self.lookup_table[top].keys())
|
||||
raise ValueError(
|
||||
f"Syntax error: expected one of {expected_symbols}, got '{current_symbol}'"
|
||||
)
|
||||
|
||||
# Случай 2: Верхний символ стека - терминал
|
||||
elif top != "$":
|
||||
if top == current_symbol:
|
||||
# Удаляем терминал из стека
|
||||
stack.pop()
|
||||
# Переходим к следующему символу ввода
|
||||
input_pos += 1
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Syntax error: expected '{top}', got '{current_symbol}'"
|
||||
)
|
||||
|
||||
# Случай 3: Верхний символ стека - $
|
||||
else: # top == "$"
|
||||
if current_symbol == "$":
|
||||
# Успешный синтаксический анализ
|
||||
stack.pop() # Удаляем $ из стека
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Syntax error: unexpected symbols at end of input: '{current_symbol}'"
|
||||
)
|
||||
|
||||
return rules_applied
|
||||
|
||||
def generate(self, symbol: str | None = None) -> tuple[list[str], list[int]]:
|
||||
"""Генерирует предложение по заданной грамматике. Возвращает список терминалов
|
||||
и список номеров применённых правил."""
|
||||
|
||||
if symbol is None:
|
||||
return self.generate(self.start_symbol)
|
||||
|
||||
# Если символ - терминал, возвращаем его
|
||||
if symbol not in self.productions:
|
||||
return [symbol], []
|
||||
|
||||
# Выбираем случайное правило для нетерминала
|
||||
rules = self.productions[symbol]
|
||||
chosen_rule = random.choice(rules)
|
||||
|
||||
# Получаем номер выбранного правила
|
||||
rule_number = self.rule_numbers[(symbol, tuple(chosen_rule))]
|
||||
|
||||
# Инициализируем результаты
|
||||
terminals = []
|
||||
rule_numbers = [rule_number]
|
||||
|
||||
# Разворачиваем каждый символ в правой части правила
|
||||
for s in chosen_rule:
|
||||
sub_terminals, sub_rules = self.generate(s)
|
||||
terminals.extend(sub_terminals)
|
||||
rule_numbers.extend(sub_rules)
|
||||
|
||||
return terminals, rule_numbers
|
||||
|
||||
def generate_derivation_steps(self, rule_numbers: list[int]) -> list[str]:
|
||||
"""Преобразует список номеров правил в последовательность шагов вывода.
|
||||
Возвращает список строк, представляющих каждый шаг вывода."""
|
||||
|
||||
# Получаем соответствие между номерами правил и самими правилами
|
||||
rule_details = {num: rule for rule, num in self.rule_numbers.items()}
|
||||
|
||||
# Начинаем с начального символа
|
||||
current = self.start_symbol
|
||||
steps = [current]
|
||||
|
||||
# Применяем каждое правило по порядку
|
||||
for rule_num in rule_numbers:
|
||||
if rule_num in rule_details:
|
||||
non_terminal, replacement = rule_details[rule_num]
|
||||
|
||||
# Находим первое вхождение нетерминала и заменяем его
|
||||
words = current.split()
|
||||
for i, word in enumerate(words):
|
||||
if word == non_terminal:
|
||||
words[i : i + 1] = replacement
|
||||
break
|
||||
|
||||
current = " ".join(words)
|
||||
steps.append(current)
|
||||
|
||||
return steps
|
||||
26
lab3/programm/grammar.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
Предложение -> Повествовательное "."
|
||||
Повествовательное -> ПрямойПорядок | Инверсия
|
||||
ПрямойПорядок -> Подлежащее ДополнениеКПодлежащему Глагол ВторостепенныеЧлены Отрицание
|
||||
Инверсия -> Обстоятельство Глагол Подлежащее ВторостепенныеЧлены Отрицание
|
||||
Подлежащее -> ИменнаяГруппа ПридаточноеПредложение | Местоимение
|
||||
ПридаточноеПредложение -> "," Союз Подлежащее Глагол Отрицание "," | epsilon
|
||||
ВторостепенныеЧлены -> ВторостепенныйЧлен ВторостепенныеЧлены | epsilon
|
||||
ВторостепенныйЧлен -> Обстоятельство | Дополнение
|
||||
Обстоятельство -> ОбстоятельствоВремени | ОбстоятельствоМеста | ОбстоятельствоОбразаДействия
|
||||
ИменнаяГруппа -> АртикльЛибоМестоимение Прилагательные Существительное
|
||||
АртикльЛибоМестоимение -> Артикль | ПритяжательноеМестоимение | epsilon
|
||||
Прилагательные -> Прилагательное Прилагательные | epsilon
|
||||
ДополнениеКПодлежащему -> Предлог Существительное | epsilon
|
||||
Дополнение -> АртикльЛибоМестоимение Прилагательные Существительное
|
||||
ОбстоятельствоМеста -> Предлог АртикльЛибоМестоимение Существительное
|
||||
Союз -> "welcher" | "welche" | "welches"
|
||||
Отрицание -> "nicht" | epsilon
|
||||
Местоимение -> "ich" | "du" | "er" | "sie" | "es" | "wir" | "ihr" | "Sie"
|
||||
ПритяжательноеМестоимение -> "mein" | "dein" | "sein" | "unser" | "euer" | "ihre"
|
||||
Артикль -> "der" | "die" | "das" | "ein" | "eine" | "einen" | "einem" | "einer"
|
||||
Прилагательное -> "alt" | "jung" | "groß" | "klein" | "schön" | "freundlich" | "süß" | "ruhig"
|
||||
Существительное -> "Mann" | "Frau" | "Kind" | "Buch" | "Brief" | "Freund" | "Abendessen" | "Suppe"
|
||||
Предлог -> "in" | "auf" | "unter" | "aus" | "mit" | "für" | "zu" | "am"
|
||||
ОбстоятельствоВремени -> "gestern" | "heute" | "morgen" | "damals" | "jetzt" | "früh" | "spät" | "immer"
|
||||
ОбстоятельствоОбразаДействия -> "schnell" | "langsam" | "gut" | "schlecht" | "laut" | "leise" | "gern" | "fleißig"
|
||||
Глагол -> "las" | "schrieb" | "kochte" | "aß" | "ging" | "kam" | "sagte" | "machte" | "liebte"
|
||||
174
lab3/programm/main.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from grammar import Grammar
|
||||
|
||||
|
||||
def load_grammar(filename: str = "grammar.txt") -> Grammar | None:
|
||||
try:
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
text = file.read()
|
||||
grammar = Grammar(text)
|
||||
|
||||
# Сохраняем информацию о грамматике в файлы
|
||||
with open("grammar_rules.txt", "w", encoding="utf-8") as output_file:
|
||||
output_file.write(grammar.format_rules())
|
||||
print("Правила грамматики с номерами сохранены в grammar_rules.txt")
|
||||
|
||||
with open("grammar_lookup_table.txt", "w", encoding="utf-8") as output_file:
|
||||
output_file.write(grammar.format_lookup_table())
|
||||
print(
|
||||
"Таблица синтаксического анализа сохранена в grammar_lookup_table.txt"
|
||||
)
|
||||
|
||||
with open("grammar_first.txt", "w", encoding="utf-8") as output_file:
|
||||
output_file.write(grammar.format_first_sets())
|
||||
print("Множества FIRST сохранены в grammar_first.txt")
|
||||
|
||||
with open("grammar_follow.txt", "w", encoding="utf-8") as output_file:
|
||||
output_file.write(grammar.format_follow_sets())
|
||||
print("Множества FOLLOW сохранены в grammar_follow.txt")
|
||||
|
||||
print(f"Грамматика успешно загружена из файла {filename}")
|
||||
return grammar
|
||||
except FileNotFoundError:
|
||||
print(f"Ошибка: Файл {filename} не найден")
|
||||
return None
|
||||
except ValueError as e:
|
||||
print(f"Ошибка при загрузке грамматики: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Неизвестная ошибка: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def tokenize_string(input_string: str) -> list[str]:
|
||||
input_string = input_string.replace(",", " , ").replace(".", " . ")
|
||||
return input_string.split()
|
||||
|
||||
|
||||
def check_string(grammar: Grammar | None, input_string: str) -> None:
|
||||
if not grammar:
|
||||
print("Ошибка: Грамматика не загружена")
|
||||
return
|
||||
|
||||
print(f"Проверка строки: '{input_string}'")
|
||||
try:
|
||||
input_tokens = tokenize_string(input_string)
|
||||
|
||||
if not input_tokens:
|
||||
parse_result = grammar.analyze(input_tokens)
|
||||
else:
|
||||
try:
|
||||
input_tokens[0] = input_tokens[0][0].lower() + input_tokens[0][1:]
|
||||
parse_result = grammar.analyze(input_tokens)
|
||||
except ValueError as e:
|
||||
input_tokens[0] = input_tokens[0][0].upper() + input_tokens[0][1:]
|
||||
parse_result = grammar.analyze(input_tokens)
|
||||
|
||||
print(f"Результат: Строка соответствует грамматике")
|
||||
print(f"Применённые правила: {parse_result}")
|
||||
|
||||
# Сохраняем результат анализа в файл
|
||||
with open("analysis_result.txt", "w", encoding="utf-8") as f:
|
||||
f.write(f"Input: {input_string}\n")
|
||||
f.write("Applied rules: ")
|
||||
f.write(str(parse_result))
|
||||
f.write("\n\n")
|
||||
f.write("Derivation steps:\n")
|
||||
|
||||
derivation_steps = grammar.generate_derivation_steps(parse_result)
|
||||
for step in derivation_steps:
|
||||
f.write(f"{step}\n")
|
||||
|
||||
print("Подробный результат анализа сохранен в analysis_result.txt")
|
||||
except ValueError as e:
|
||||
print(f"Результат: Строка не соответствует грамматике")
|
||||
print(f"Ошибка: {e}")
|
||||
except Exception as e:
|
||||
print(f"Произошла ошибка при анализе: {e}")
|
||||
|
||||
|
||||
def post_process_string(string: str) -> str:
|
||||
if string:
|
||||
string = string[0].upper() + string[1:]
|
||||
string = string.replace(" ,", ",")
|
||||
string = string.replace(" .", ".")
|
||||
string = string.replace(",.", ".")
|
||||
return string
|
||||
|
||||
|
||||
def generate_string(grammar: Grammar | None) -> None:
|
||||
if not grammar:
|
||||
print("Ошибка: Грамматика не загружена")
|
||||
return
|
||||
|
||||
try:
|
||||
terminals, rules = grammar.generate()
|
||||
generated_string = " ".join(terminals)
|
||||
generated_string = post_process_string(generated_string)
|
||||
print(f"Сгенерированная строка: {generated_string}")
|
||||
print(f"Применённые правила: {rules}")
|
||||
|
||||
# Сохраняем результат генерации в файл
|
||||
with open("generation_result.txt", "w", encoding="utf-8") as f:
|
||||
f.write(f"Generated string: {generated_string}\n")
|
||||
f.write("Applied rules: ")
|
||||
f.write(str(rules))
|
||||
f.write("\n\n")
|
||||
f.write("Derivation steps:\n")
|
||||
|
||||
derivation_steps = grammar.generate_derivation_steps(rules)
|
||||
for step in derivation_steps:
|
||||
f.write(f"{step}\n")
|
||||
|
||||
print("Подробный результат генерации сохранен в generation_result.txt")
|
||||
except Exception as e:
|
||||
print(f"Произошла ошибка при генерации: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
print("Программа для работы с LL(1)-грамматиками")
|
||||
print("=" * 60)
|
||||
print("Варианты команд:")
|
||||
print(" - load <файл> - загрузить грамматику из файла (по умолчанию grammar.txt)")
|
||||
print(" - check <строка> - проверить, соответствует ли строка грамматике")
|
||||
print(" - generate - сгенерировать случайную строку по грамматике")
|
||||
print(" - exit - выход из программы")
|
||||
print("=" * 60)
|
||||
|
||||
# Загружаем грамматику по умолчанию при старте
|
||||
grammar = load_grammar()
|
||||
|
||||
while True:
|
||||
command = input("\nВведите команду: ").strip()
|
||||
|
||||
if not command:
|
||||
continue
|
||||
|
||||
parts = command.split(maxsplit=1)
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == "exit":
|
||||
print("Выход из программы.")
|
||||
break
|
||||
|
||||
elif cmd == "load":
|
||||
filename = "grammar.txt"
|
||||
if len(parts) > 1:
|
||||
filename = parts[1].strip()
|
||||
grammar = load_grammar(filename)
|
||||
|
||||
elif cmd == "check":
|
||||
input_string = ""
|
||||
if len(parts) > 1:
|
||||
input_string = parts[1].strip()
|
||||
check_string(grammar, input_string)
|
||||
|
||||
elif cmd == "generate":
|
||||
generate_string(grammar)
|
||||
|
||||
else:
|
||||
print(f"Неизвестная команда: {cmd}")
|
||||
print("Доступные команды: load, check, generate, exit")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
999
lab3/report.tex
Normal file
@@ -0,0 +1,999 @@
|
||||
\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{Лабораторная работа №3}\\
|
||||
\large{<<Грамматика простого прошедшего времени}\\
|
||||
\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}{Введение}
|
||||
Лабораторная №3 заключается в построении контексно-свободной грамматики подмножества естественного языка и написании программы для генерации предложений языка и проверки предложений на принадлежность языку. В данном варианте рассматривается грамматика простого прошедшего времени немецкого языка.
|
||||
|
||||
Простое прошедшее время в немецком языке называется \textit{Претерит} или \textit{Претеритум} (нем. \textit{Präteritum} или нем. \textit{Imperfekt}). Это время не требует образования сложных конструкций, что позволяет относить его к простым формам.
|
||||
|
||||
|
||||
\newpage
|
||||
\section {Математическое описание}
|
||||
\subsection{Особенности грамматики}
|
||||
Грамматика, рассматриваемая в рамках данной лабораторной работы, имеет следующие особенности:
|
||||
\begin{itemize}
|
||||
\item Рассматриваются только повествовательные и повествовательные отрицательные предложения.
|
||||
\item Учтены два возможных порядка членов предложения: прямой и обратный. В прямом порядке сначала идет подлежащее, потом сказуемое, а затем второстепенные члены предложения. В обратном порядке на первом месте стоит обстоятельство, на втором сказуемое, на третьем подлежащее, а затем второстепенные члены предложения. В \textit{Претеритуме} сказуемое всегда стоит на втором месте.
|
||||
\item Придаточные предложения могут относится только к подлежащему и начинаться с союзов \textit{welcher} (который), \textit{welche} (которая), \textit{welches} (которое). При этом вложенность придаточных предложений не ограничена.
|
||||
\item Порядок второстепеннных членов не фиксирован. Несмотря на то, что в немецком языке существует предпочтительный порядок слов, в данной грамматике он не учитывается, потому что порядок не явлется строгим и реальные предложения языка далеко не всегда ему соответствуют.
|
||||
\item Рассматриваются только простые сказуемые, не содержащие вспомогательных глаголов.
|
||||
\item Рассматриваются три вида обстоятельств: обстоятельства времени, места и образа действия. В реальных предложениях языка могут встречаться и другие виды, но перечисленные являются наиболее часто встречающимися.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Грамматика в виде списка продукций}
|
||||
\label{subsec:grammar}
|
||||
|
||||
Формально можно описать грамматику в виде списка продукций:
|
||||
|
||||
\begin{verbatim}
|
||||
Предложение -> Повествовательное "."
|
||||
Повествовательное -> ПрямойПорядок | Инверсия
|
||||
ПрямойПорядок -> Подлежащее ДополнениеКПодлежащему Глагол
|
||||
ВторостепенныеЧлены Отрицание
|
||||
Инверсия -> Обстоятельство Глагол Подлежащее
|
||||
ВторостепенныеЧлены Отрицание
|
||||
Подлежащее -> ИменнаяГруппа ПридаточноеПредложение | Местоимение
|
||||
ПридаточноеПредложение -> "," Союз Подлежащее Глагол Отрицание "," |
|
||||
epsilon
|
||||
ВторостепенныеЧлены -> ВторостепенныйЧлен ВторостепенныеЧлены |
|
||||
epsilon
|
||||
ВторостепенныйЧлен -> Обстоятельство | Дополнение
|
||||
Обстоятельство -> ОбстоятельствоВремени | ОбстоятельствоМеста |
|
||||
ОбстоятельствоОбразаДействия
|
||||
ИменнаяГруппа -> АртикльЛибоМестоимение Прилагательные
|
||||
Существительное
|
||||
АртикльЛибоМестоимение -> Артикль | ПритяжательноеМестоимение |
|
||||
epsilon
|
||||
Прилагательные -> Прилагательное Прилагательные | epsilon
|
||||
ДополнениеКПодлежащему -> Предлог Существительное | epsilon
|
||||
Дополнение -> АртикльЛибоМестоимение Прилагательные Существительное
|
||||
ОбстоятельствоМеста -> Предлог АртикльЛибоМестоимение Существительное
|
||||
Союз -> "welcher" | "welche" | "welches"
|
||||
Отрицание -> "nicht" | epsilon
|
||||
Местоимение -> "ich" | "du" | "er" | "sie" | "es" | "wir" |
|
||||
"ihr" | "Sie"
|
||||
ПритяжательноеМестоимение -> "mein" | "dein" | "sein" |
|
||||
"unser" | "euer" | "ihre"
|
||||
Артикль -> "der" | "die" | "das" | "ein" | "eine" | "einen" |
|
||||
"einem" | "einer"
|
||||
Прилагательное -> "alt" | "jung" | "groß" | "klein" | "schön" |
|
||||
"freundlich" | "süß" | "ruhig"
|
||||
Существительное -> "Mann" | "Frau" | "Kind" | "Buch" | "Brief" |
|
||||
"Freund" | "Abendessen" | "Suppe"
|
||||
Предлог -> "in" | "auf" | "unter" | "aus" | "mit" | "für" |
|
||||
"zu" | "am"
|
||||
ОбстоятельствоВремени -> "gestern" | "heute" | "morgen" | "damals" |
|
||||
"jetzt" | "früh" | "spät" | "immer"
|
||||
ОбстоятельствоОбразаДействия -> "schnell" | "langsam" | "gut" |
|
||||
"schlecht" | "laut" | "leise" |
|
||||
"gern" | "fleißig"
|
||||
Глагол -> "las" | "schrieb" | "kochte" | "aß" | "ging" |
|
||||
"kam" | "sagte" | "machte"
|
||||
\end{verbatim}
|
||||
|
||||
Терминальными являются все символы в кавычках, все остальные символы являются нетерминальными.
|
||||
|
||||
Язык, пораждаемый данной грамматикой, как и сама грамматика, является контекстно-свободным, так как:
|
||||
\begin{itemize}
|
||||
\item В левых частях всех продукций присутствует только один нетерминальный символ, а значит язык не является контекстно-зависимым.
|
||||
\item Язык не является автоматным из-за наличия придаточных предложений неограниченной вложенности. Придаточные предложения являются аналогом скобочной структуры, которая не может быть описана конечным автоматом.
|
||||
\end{itemize}
|
||||
|
||||
Также эта грамматика является LL(1)-грамматикой. Множества FIRST и FOLLOW для всех нетерминальных символов не пересекаются. В дальнейшем это особенность используется для распознавания предложений языка.
|
||||
|
||||
|
||||
\subsection{Грамматика в БНФ}
|
||||
|
||||
\vspace{-0.2cm}
|
||||
Грамматика в БНФ имеет следующий вид:
|
||||
|
||||
\vspace{-0.5cm}
|
||||
\begin{verbatim}
|
||||
<Предложение> ::= <Повествовательное> "."
|
||||
<Повествовательное> ::= <ПрямойПорядок> | <Инверсия>
|
||||
<ПрямойПорядок> ::= <Подлежащее> [ДополнениеКПодлежащему] <Глагол>
|
||||
{ВторостепенныйЧлен} [Отрицание]
|
||||
<Инверсия> ::= <Обстоятельство> <Глагол> <Подлежащее>
|
||||
{ВторостепенныйЧлен} [Отрицание]
|
||||
<Подлежащее> ::= <ИменнаяГруппа> [ПридаточноеПредложение] | Местоимение
|
||||
<ПридаточноеПредложение> ::= "," <Союз> <Подлежащее> <Глагол> [Отрицание] ","
|
||||
<ВторостепенныйЧлен> ::= <Обстоятельство> | <Дополнение>
|
||||
<Обстоятельство> ::= <ОбстоятельствоВремени> | <ОбстоятельствоМеста> |
|
||||
<ОбстоятельствоОбразаДействия>
|
||||
<ИменнаяГруппа> ::= [Артикль | ПритяжательноеМестоимение]
|
||||
{Прилагательное} <Существительное>
|
||||
<ДополнениеКПодлежащему> ::= <Предлог> <Существительное>
|
||||
<Дополнение> ::= [Артикль | ПритяжательноеМестоимение]
|
||||
{Прилагательное} <Существительное>
|
||||
<ОбстоятельствоМеста> ::= <Предлог>
|
||||
[Артикль | ПритяжательноеМестоимение] <Существительное>
|
||||
<Союз> ::= "welcher" | "welche" | "welches"
|
||||
<Отрицание> ::= "nicht"
|
||||
<Местоимение> ::= "ich" | "du" | "er" | "sie" | "es" | "wir" |
|
||||
"ihr" | "Sie"
|
||||
<ПритяжательноеМестоимение> ::= "mein" | "dein" | "sein" |
|
||||
"unser" | "euer" | "ihre"
|
||||
<Артикль> ::= "der" | "die" | "das" | "ein" | "eine" | "einen" |
|
||||
"einem" | "einer"
|
||||
<Прилагательное> ::= "alt" | "jung" | "groß" | "klein" | "schön" |
|
||||
"freundlich" | "süß" | "ruhig"
|
||||
<Существительное> ::= "Mann" | "Frau" | "Kind" | "Buch" | "Brief" |
|
||||
"Freund" | "Abendessen" | "Suppe"
|
||||
<Предлог> ::= "in" | "auf" | "unter" | "aus" | "mit" | "für" |
|
||||
"zu" | "am"
|
||||
<ОбстоятельствоВремени> ::= "gestern" | "heute" | "morgen" |
|
||||
"damals" | "jetzt" | "früh" | "spät" | "immer"
|
||||
<ОбстоятельствоОбразаДействия> ::= "schnell" | "langsam" | "gut" |
|
||||
"schlecht" | "laut" | "leise" |
|
||||
"gern" | "fleißig"
|
||||
<Глагол> ::= "las" | "schrieb" | "kochte" | "aß" | "ging" |
|
||||
"kam" | "sagte" | "machte" | "liebte"
|
||||
\end{verbatim}
|
||||
|
||||
\subsection{Пример вывода предложения}
|
||||
На Рис.~1 представлено дерево вывода предложения \textit{<<Mein alt Freund, welcher ich liebte, schrieb gestern einen schön Brief.>>}
|
||||
|
||||
\addtocounter{figure}{1}
|
||||
\includepdf[pages={1}, fitpaper, pagecommand={
|
||||
\thispagestyle{empty}
|
||||
\begin{tikzpicture}[remember picture, overlay]
|
||||
\node at (current page.south) [anchor=north, yshift=45pt] {\large{Рис 1. Пример дерева вывода предложения.}};
|
||||
\end{tikzpicture}
|
||||
}]{pdf/tree.pdf}
|
||||
|
||||
\subsection{Алгоритм построения LL(1) таблицы синтаксического анализа}
|
||||
Чтобы заполнить таблицу синтаксического анализа, необходимо установить, какое правило грамматики синтаксический анализатор должен выбрать, если нетерминальное $A$ находится на вершине его стека и символ $a$ — во входном потоке. Легко заметить, что такое правило должно иметь форму $A \rightarrow w$ и что у языка, соответствующего $w$, должна быть по крайней мере одна строка, начинающаяся с $a$.
|
||||
|
||||
С этой целью мы определяем \textbf{множество \texttt{FIRST()} для $w$}, обозначенное как $\texttt{FIRST}(w)$, как множество терминалов, которые могут быть найдены в начале любой строки в $w$, и $\varepsilon$, если пустая строка также принадлежит $w$.
|
||||
|
||||
Учитывая грамматику с правилами $A_1 \rightarrow w_1, \dots, A_n \rightarrow w_n$, можно вычислить $\texttt{FIRST}(w_i)$ и $\texttt{FIRST}(A_i)$ для каждого правила следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Инициализировать каждое множество $\texttt{FIRST}(A_i)$ пустым множеством.
|
||||
\item Добавить $\texttt{FIRST}(w_i)$ к $\texttt{FIRST}(A_i)$ для каждого правила $A_i \rightarrow w_i$, где $\texttt{FIRST}(w_i)$ определяется следующим образом:
|
||||
\begin{itemize}
|
||||
\item $\texttt{FIRST}(a w') = \{ a \}$ для каждого терминала $a$
|
||||
\item $\texttt{FIRST}(A w') = \texttt{FIRST}(A)$ для каждого нетерминального $A$ с $\varepsilon \notin \texttt{FIRST}(A)$
|
||||
\item $\texttt{FIRST}(A w') = \left( \texttt{FIRST}(A) \setminus \{ \varepsilon \} \right) \cup \texttt{FIRST}(w')$ для каждого нетерминального $A$ с $\varepsilon \in \texttt{FIRST}(A)$, включая случай $A_i \rightarrow A$, то есть $w' = \varepsilon$, в этом случае $\texttt{FIRST}(A w') = \texttt{FIRST}(A)$
|
||||
\item $\texttt{FIRST}(\varepsilon) = \{ \varepsilon \}$
|
||||
\end{itemize}
|
||||
\item Повторять шаг 2, пока в множествах $\texttt{FIRST}$ происходят изменения.
|
||||
\end{enumerate}
|
||||
|
||||
Однако множеств \texttt{FIRST()} недостаточно, чтобы построить таблицу синтаксического анализа. Так происходит, потому что правая сторона $w$ правила могла бы в конечном счёте быть приведена к пустой строке. Таким образом синтаксический анализатор должен также использовать правило $A \rightarrow w$, если $\varepsilon \in \texttt{FIRST}(w)$ и во входном потоке символ, который может следовать за $A$. Поэтому также необходимо построить \textbf{множество \texttt{FOLLOW()} для $A$}, которое определяется как множество терминалов $a$, таких что существует строка символов $\alpha A a \beta$, которая может быть получена из начального символа.
|
||||
|
||||
Вычисление множеств \texttt{FOLLOW()} для нетерминалов в грамматике выполняется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Инициализировать $\texttt{FOLLOW}(S) = \{ \$ \}$, а все остальные множества $\texttt{FOLLOW}(A_i)$ пустыми.
|
||||
\item Если есть правило формы $A_j \rightarrow w A_i w'$, тогда:
|
||||
\begin{itemize}
|
||||
\item Если терминал $a \in \texttt{FIRST}(w')$, то добавить $a$ в $\texttt{FOLLOW}(A_i)$
|
||||
\item Если $\varepsilon \in \texttt{FIRST}(w')$, то добавить $\texttt{FOLLOW}(A_j)$ в $\texttt{FOLLOW}(A_i)$
|
||||
\item Если $w'$ имеет длину 0, то добавить $\texttt{FOLLOW}(A_j)$ в $\texttt{FOLLOW}(A_i)$
|
||||
\end{itemize}
|
||||
\item Повторять шаг 2, пока в множествах \texttt{FOLLOW()} происходят изменения.
|
||||
\end{enumerate}
|
||||
|
||||
Теперь можно точно определить, какие правила будут содержаться в таблице синтаксического анализа. Если $T[A, a]$ обозначает ячейку в таблице для нетерминального $A$ и терминала $a$, то:
|
||||
|
||||
\begin{itemize}
|
||||
\item $T[A,a]$ содержит правило $A \rightarrow w$ тогда и только тогда, когда:
|
||||
\begin{itemize}
|
||||
\item $a \in \texttt{FIRST}(w)$ при проходе правила $A \rightarrow w$, или
|
||||
\item $\varepsilon \in \texttt{FIRST}(w)$ и $a \in \texttt{FOLLOW}(A)$
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
|
||||
Если таблица будет содержать не более одного правила в каждой ячейке, то синтаксический анализатор сможет однозначно определить, какое правило необходимо применить на каждом шаге разбора. В этом случае грамматику является \textbf{LL(1)} грамматикой.
|
||||
|
||||
\subsection{Алгоритм разбора предложений}
|
||||
|
||||
Перед запуском алгоритма разбора в стек помещаются два символа:
|
||||
\begin{itemize}
|
||||
\item Специальный символ~\$ — признак конца входной цепочки.
|
||||
\item Начальный символ грамматики (на вершину стека).
|
||||
\end{itemize}
|
||||
|
||||
Cинтаксический анализатор выполняет три различных вида действий в зависимости от того, находится ли на вершине стека нетерминал, терминал или специальный символ~\$.
|
||||
|
||||
\begin{enumerate}
|
||||
\item \textbf{Если на вершине стека — нетерминал}, то в таблице синтаксического анализа ищется правило грамматики, находящееся на пересечении строки и столбца, соответствующих этому нетерминалу на вершине стека и текущему входному символу. Если же в указанной ячейке таблицы правило отсутствует — синтаксический анализатор останавливается и сообщает об ошибке.
|
||||
|
||||
\item \textbf{Если на вершине стека — терминал}, то синтаксический анализатор сравнивает его с текущим входным символом. Если они равны, то входной символ считывается, а соответствующий символ из вершины стека — удаляется.
|
||||
|
||||
\item \textbf{Если на вершине стека — специальный символ~\$}, и текущий символ на ленте также равен~\$, то синтаксический анализатор сообщает об успешном распознавании цепочки. В противном случае — сообщает об ошибке. В обоих случаях происходит останов анализатора.
|
||||
\end{enumerate}
|
||||
|
||||
Эти шаги повторяются до тех пор, пока не произойдёт останов. После останова мы получаем либо сообщение об ошибке, либо сообщение об успешном распознавании цепочки.
|
||||
|
||||
\subsection{Алгоритм генерации предложения}
|
||||
|
||||
Генерация предложения происходит следующим образом:
|
||||
\begin{enumerate}
|
||||
\item В список помещается начальный символ грамматики.
|
||||
\item Осуществляется проход по списку. Первый встреченный нетерминал заменяется на его случайно выбранную продукцию, после чего проход начинается заново.
|
||||
\item Алгоритм завершается, когда в списке не осталось нетерминалов.
|
||||
\end{enumerate}
|
||||
|
||||
Этот алгоритм не позволяет контролировать длину предложения. Также алгоритм может не сходиться в общем случае. Но вероятность того, что алгоритм попадет в петлю убывает геометрически с ростом длины цепочки, так что на практике этот метод работает.
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
|
||||
\subsection{Задание правил грамматики}
|
||||
|
||||
Грамматика задаётся в виде текстового файла, в котором каждая строка соответствует одной продукции. Синтаксис задания продукций соответствует общепринятому, за исключением того, что вместо $\varepsilon$ используется слово <<\texttt{epsilon}>>. Терминалы записываются в кавычках, нетерминалы — без кавычек. Левая и правая части продукции разделяются двумя символами -- <<\texttt{->}>>. Таким образом содержимое файла, соответствует тексту описания грамматики в разделе \ref{subsec:grammar}.
|
||||
|
||||
|
||||
\subsection{Класс \texttt{Grammar}}
|
||||
|
||||
Класс \texttt{Grammar} содержит в себе всю информацию о грамматике, а также методы для работы с ней. Код конструктора класса представлен в листинге \ref{lst:grammar_constructor}. Конструктор класса принимает на вход строку, содержащую описание грамматики. Вызывает методы для парсинга продукций, нахождения терминалов, вычисления множеств FIRST и FOLLOW, заполнения таблицы синтаксического анализа.
|
||||
|
||||
\begin{lstlisting}[caption={Код конструктора класса \texttt{Grammar}.}, label={lst:grammar_constructor}]
|
||||
class Grammar:
|
||||
EPSILON: str = "epsilon"
|
||||
|
||||
def __init__(self, text: str):
|
||||
self.productions: OrderedDict[str, list[list[str]]] = OrderedDict()
|
||||
self.start_symbol: str = ""
|
||||
self._parse_productions(text)
|
||||
|
||||
self.terminals: set[str] = set()
|
||||
self._find_terminals()
|
||||
|
||||
self.first_sets: dict[str, set[str]] = {}
|
||||
self._calculate_first_sets()
|
||||
|
||||
self.follow_sets: dict[str, set[str]] = {}
|
||||
self._calculate_follow_sets()
|
||||
|
||||
self.lookup_table: dict[str, dict[str, list[str]]] = {}
|
||||
self._fill_lookup_table()
|
||||
|
||||
# Сопостовляем уникальный номер с каждым правилом
|
||||
self.rule_numbers = {}
|
||||
rule_idx = 1
|
||||
for nt, rules in self.productions.items():
|
||||
for rule in rules:
|
||||
self.rule_numbers[(nt, tuple(rule))] = rule_idx
|
||||
rule_idx += 1
|
||||
\end{lstlisting}
|
||||
|
||||
|
||||
\subsubsection{Метод \texttt{\_parse\_productions}}
|
||||
|
||||
Метод \texttt{\_parse\_productions} выполняет разбор текстового описания грамматики и создаёт внутреннее представление продукций. Код метода представлен в листинге~\ref{lst:parse_productions}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar и строку text типа str, содержащую текстовое представление грамматики.
|
||||
|
||||
Метод не возвращает значений, но заполняет словарь productions и устанавливает переменную start\_symbol в объекте класса.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{\_parse\_productions}.}, label={lst:parse_productions}]
|
||||
def _parse_productions(self, text: str):
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
non_terminal, rule = line.split("->")
|
||||
if self.start_symbol == "":
|
||||
self.start_symbol = non_terminal.strip()
|
||||
|
||||
non_terminal = non_terminal.strip()
|
||||
rules = [
|
||||
[
|
||||
symbol.strip('"')
|
||||
for symbol in re.findall(r"\".*?\"|\S+", rule.strip())
|
||||
if symbol.strip('"') != self.EPSILON
|
||||
]
|
||||
for rule in rule.split("|")
|
||||
]
|
||||
|
||||
if non_terminal not in self.productions:
|
||||
self.productions[non_terminal] = []
|
||||
|
||||
self.productions[non_terminal].extend(rules)
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{\_calculate\_first\_sets}}
|
||||
|
||||
Метод \texttt{\_calculate\_first\_sets} вычисляет множества FIRST для всех символов грамматики. Код метода представлен в листинге~\ref{lst:calculate_first_sets}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar.
|
||||
|
||||
Метод не возвращает значений, но заполняет словарь first\_sets в объекте класса.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{\_calculate\_first\_sets}.}, label={lst:calculate_first_sets}]
|
||||
def _calculate_first_sets(self):
|
||||
# Инициализация FIRST для всех символов
|
||||
for non_terminal in self.productions:
|
||||
self.first_sets[non_terminal] = set()
|
||||
|
||||
# Для терминалов FIRST содержит только сам терминал
|
||||
for terminal in self.terminals:
|
||||
self.first_sets[terminal] = {terminal}
|
||||
|
||||
# Вычисление FIRST для нетерминалов
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for non_terminal, rules in self.productions.items():
|
||||
for rule in rules:
|
||||
if not rule: # Пустое правило (эпсилон)
|
||||
if "" not in self.first_sets[non_terminal]:
|
||||
self.first_sets[non_terminal].add("")
|
||||
changed = True
|
||||
else:
|
||||
all_can_derive_epsilon = True
|
||||
for i, symbol in enumerate(rule):
|
||||
# Добавляем все символы из FIRST(symbol) кроме эпсилон
|
||||
first_without_epsilon = self.first_sets[symbol] - {""}
|
||||
old_size = len(self.first_sets[non_terminal])
|
||||
self.first_sets[non_terminal].update(first_without_epsilon)
|
||||
if len(self.first_sets[non_terminal]) > old_size:
|
||||
changed = True
|
||||
|
||||
# Если symbol не может порождать эпсилон, прерываем
|
||||
if "" not in self.first_sets[symbol]:
|
||||
all_can_derive_epsilon = False
|
||||
break
|
||||
|
||||
# Если все символы в правиле могут порождать эпсилон,
|
||||
# то и нетерминал может порождать эпсилон
|
||||
if (
|
||||
all_can_derive_epsilon
|
||||
and "" not in self.first_sets[non_terminal]
|
||||
):
|
||||
self.first_sets[non_terminal].add("")
|
||||
changed = True
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{\_calculate\_follow\_sets}}
|
||||
|
||||
Метод \texttt{\_calculate\_follow\_sets} вычисляет множества FOLLOW для нетерминальных символов грамматики. Код метода представлен в листинге~\ref{lst:calculate_follow_sets}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar.
|
||||
|
||||
Метод не возвращает значений, но заполняет словарь follow\_sets в объекте класса.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{\_calculate\_follow\_sets}.}, label={lst:calculate_follow_sets}]
|
||||
def _calculate_follow_sets(self):
|
||||
# инициализировать Fo(S) = { $ }, а все остальные Fo(Ai) пустыми множествами
|
||||
for non_terminal in self.productions:
|
||||
self.follow_sets[non_terminal] = set()
|
||||
|
||||
# Добавляем символ конца строки $ для начального символа
|
||||
self.follow_sets[self.start_symbol].add("$")
|
||||
|
||||
# Повторяем, пока в наборах Follow происходят изменения
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
|
||||
# Для каждого нетерминала Aj в грамматике
|
||||
for non_terminal_j, rules in self.productions.items():
|
||||
|
||||
# Для каждого правила Aj → w
|
||||
for rule in rules:
|
||||
|
||||
# Для каждого символа в правиле
|
||||
for i, symbol in enumerate(rule):
|
||||
|
||||
# Если символ - нетерминал Ai
|
||||
if symbol in self.productions:
|
||||
# w' - остаток правила после Ai
|
||||
remainder = rule[i + 1 :] if i + 1 < len(rule) else []
|
||||
|
||||
# Если есть терминалы после Ai (w' не пусто)
|
||||
if remainder:
|
||||
# Вычисляем First(w')
|
||||
first_of_remainder = self._first_of_sequence(remainder)
|
||||
|
||||
# Если терминал a находится в First(w'), то добавляем a к Follow(Ai)
|
||||
for terminal in first_of_remainder - {""}:
|
||||
if terminal not in self.follow_sets[symbol]:
|
||||
self.follow_sets[symbol].add(terminal)
|
||||
changed = True
|
||||
|
||||
# Если epsilon находится в First(w'), то добавляем Follow(Aj) к Follow(Ai)
|
||||
if "" in first_of_remainder:
|
||||
old_size = len(self.follow_sets[symbol])
|
||||
self.follow_sets[symbol].update(
|
||||
self.follow_sets[non_terminal_j]
|
||||
)
|
||||
if len(self.follow_sets[symbol]) > old_size:
|
||||
changed = True
|
||||
|
||||
# Если w' пусто (Ai в конце правила), то добавляем Follow(Aj) к Follow(Ai)
|
||||
else:
|
||||
old_size = len(self.follow_sets[symbol])
|
||||
self.follow_sets[symbol].update(
|
||||
self.follow_sets[non_terminal_j]
|
||||
)
|
||||
if len(self.follow_sets[symbol]) > old_size:
|
||||
changed = True
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{\_first\_of\_sequence}}
|
||||
|
||||
Метод \texttt{\_first\_of\_sequence} вычисляет множество FIRST для последовательности символов. Код метода представлен в листинге~\ref{lst:first_of_sequence}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar и список символов sequence.
|
||||
|
||||
Метод возвращает множество (set) строк, представляющих FIRST для заданной последовательности.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{\_first\_of\_sequence}.}, label={lst:first_of_sequence}]
|
||||
def _first_of_sequence(self, sequence):
|
||||
"""Вычисляет множество FIRST для последовательности символов"""
|
||||
if not sequence:
|
||||
return {""}
|
||||
|
||||
result = set()
|
||||
all_can_derive_epsilon = True
|
||||
|
||||
for symbol in sequence:
|
||||
# Добавляем First(symbol) без эпсилон к результату
|
||||
result.update(self.first_sets[symbol] - {""})
|
||||
|
||||
# Если symbol не может порождать эпсилон, останавливаемся
|
||||
if "" not in self.first_sets[symbol]:
|
||||
all_can_derive_epsilon = False
|
||||
break
|
||||
|
||||
# Если все символы могут порождать эпсилон, добавляем эпсилон к результату
|
||||
if all_can_derive_epsilon:
|
||||
result.add("")
|
||||
|
||||
return result
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{\_fill\_lookup\_table}}
|
||||
|
||||
Метод \texttt{\_fill\_lookup\_table} заполняет таблицу синтаксического анализа для LL(1)-грамматики. Код метода представлен в листинге~\ref{lst:fill_lookup_table}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar.
|
||||
|
||||
Метод не возвращает значений, но заполняет словарь lookup\_table в объекте класса или вызывает исключение, если грамматика не является LL(1).
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{\_fill\_lookup\_table}.}, label={lst:fill_lookup_table}]
|
||||
def _fill_lookup_table(self):
|
||||
# Для каждого нетерминала A
|
||||
for non_terminal, rules in self.productions.items():
|
||||
# Формируем таблицу синтаксического анализа
|
||||
self.lookup_table[non_terminal] = {}
|
||||
|
||||
# Для каждого правила A → w
|
||||
for rule in rules:
|
||||
# Вычисляем First(w)
|
||||
first_of_rule = self._first_of_sequence(rule)
|
||||
|
||||
# Для каждого терминала a в First(w)
|
||||
for terminal in first_of_rule - {""}:
|
||||
# Если T[A,a] уже содержит правило, грамматика не LL(1)
|
||||
if terminal in self.lookup_table[non_terminal]:
|
||||
raise ValueError(
|
||||
"Грамматика не является LL(1)-грамматикой.\n"
|
||||
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||
)
|
||||
|
||||
# Добавляем правило в таблицу
|
||||
self.lookup_table[non_terminal][terminal] = rule
|
||||
|
||||
# Если эпсилон в First(w), то для каждого b в Follow(A)
|
||||
if "" in first_of_rule:
|
||||
for terminal in self.follow_sets[non_terminal]:
|
||||
# Если T[A,b] уже содержит правило, грамматика не LL(1)
|
||||
if terminal in self.lookup_table[non_terminal]:
|
||||
raise ValueError(
|
||||
"Грамматика не является LL(1)-грамматикой.\n"
|
||||
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||
)
|
||||
|
||||
# Добавляем правило в таблицу
|
||||
self.lookup_table[non_terminal][terminal] = rule
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{format\_rules}}
|
||||
|
||||
Метод \texttt{format\_rules} форматирует все правила грамматики для удобного представления. Код метода представлен в листинге~\ref{lst:format_rules}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar.
|
||||
|
||||
Метод возвращает строку (str), содержащую все правила грамматики с их номерами.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{format\_rules}.}, label={lst:format_rules}]
|
||||
def format_rules(self) -> str:
|
||||
result = []
|
||||
sorted_rules = sorted(self.rule_numbers.items(), key=lambda x: x[1])
|
||||
|
||||
for rule, number in sorted_rules:
|
||||
non_terminal, symbols = rule
|
||||
rule_text = f"{number}: {non_terminal} -> {' '.join(symbols)}"
|
||||
result.append(rule_text)
|
||||
|
||||
return "\n".join(result)
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{format\_lookup\_table}}
|
||||
|
||||
Метод \texttt{format\_lookup\_table} форматирует таблицу синтаксического анализа для удобного представления. Код метода представлен в листинге~\ref{lst:format_lookup_table}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar.
|
||||
|
||||
Метод возвращает строку (str), содержащую таблицу синтаксического анализа в виде таблицы с использованием библиотеки PrettyTable.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{format\_lookup\_table}.}, label={lst:format_lookup_table}]
|
||||
def format_lookup_table(self) -> str:
|
||||
table = PrettyTable()
|
||||
terminals = list(self.terminals)
|
||||
table.field_names = [""] + terminals + ["$"]
|
||||
|
||||
for non_terminal in self.productions:
|
||||
row = [non_terminal]
|
||||
for terminal in terminals + ["$"]:
|
||||
if terminal in self.lookup_table[non_terminal]:
|
||||
rule = self.lookup_table[non_terminal][terminal]
|
||||
rule_num = self.rule_numbers.get((non_terminal, tuple(rule)), "")
|
||||
row.append(f"{rule_num}: {' '.join(rule)}")
|
||||
else:
|
||||
row.append(" - ")
|
||||
table.add_row(row)
|
||||
return str(table)
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{format\_first\_sets}}
|
||||
|
||||
Метод \texttt{format\_first\_sets} форматирует множества FIRST для удобного представления. Код метода представлен в листинге~\ref{lst:format_first_sets}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar.
|
||||
|
||||
Метод возвращает строку (str), содержащую множества FIRST для всех символов грамматики.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{format\_first\_sets}.}, label={lst:format_first_sets}]
|
||||
def format_first_sets(self) -> str:
|
||||
"""Форматирует множества FIRST в читаемый вид."""
|
||||
result = []
|
||||
result.append("Множества FIRST:")
|
||||
result.append("=" * 40)
|
||||
|
||||
# Сортируем для гарантии порядка вывода
|
||||
for symbol in sorted(self.first_sets.keys()):
|
||||
# Заменяем пустую строку на эпсилон для лучшей читаемости
|
||||
first_set = {
|
||||
self.EPSILON if item == "" else item for item in self.first_sets[symbol]
|
||||
}
|
||||
result.append(f"FIRST({symbol}) = {{{', '.join(sorted(first_set))}}}")
|
||||
|
||||
return "\n".join(result)
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{format\_follow\_sets}}
|
||||
|
||||
Метод \texttt{format\_follow\_sets} форматирует множества FOLLOW для удобного представления. Код метода представлен в листинге~\ref{lst:format_follow_sets}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar.
|
||||
|
||||
Метод возвращает строку (str), содержащую множества FOLLOW для всех нетерминалов грамматики.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{format\_follow\_sets}.}, label={lst:format_follow_sets}]
|
||||
def format_follow_sets(self) -> str:
|
||||
"""Форматирует множества FOLLOW в читаемый вид."""
|
||||
result = []
|
||||
result.append("Множества FOLLOW:")
|
||||
result.append("=" * 40)
|
||||
|
||||
# Обрабатываем только нетерминалы
|
||||
for non_terminal in sorted(self.productions.keys()):
|
||||
follow_set = self.follow_sets.get(non_terminal, set())
|
||||
result.append(
|
||||
f"FOLLOW({non_terminal}) = {{{', '.join(sorted(follow_set))}}}"
|
||||
)
|
||||
|
||||
return "\n".join(result)
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{analyze}}
|
||||
|
||||
Метод \texttt{analyze} выполняет синтаксический анализ входной последовательности токенов с использованием LL(1)-алгоритма. Код метода представлен в листинге~\ref{lst:analyze}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar и список input\_tokens типа list[str], представляющий последовательность входных токенов.
|
||||
|
||||
Метод возвращает список (list) целых чисел, содержащий номера применённых правил в процессе анализа.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{analyze}.}, label={lst:analyze}]
|
||||
def analyze(self, input_tokens: list[str]) -> list[int]:
|
||||
input_tokens = input_tokens.copy()
|
||||
input_tokens += ["$"]
|
||||
input_pos = 0
|
||||
|
||||
# Инициализируем стек с терминальным символом и начальным символом
|
||||
stack = ["$", self.start_symbol]
|
||||
|
||||
rules_applied = []
|
||||
|
||||
while stack:
|
||||
top = stack[-1]
|
||||
|
||||
current_symbol = (
|
||||
input_tokens[input_pos] if input_pos < len(input_tokens) else "$"
|
||||
)
|
||||
|
||||
# Случай 1: Верхний символ стека - нетерминал
|
||||
if top in self.productions:
|
||||
# Ищем правило в таблице синтаксического анализа
|
||||
if current_symbol in self.lookup_table[top]:
|
||||
# Получаем правило
|
||||
production = self.lookup_table[top][current_symbol]
|
||||
|
||||
# Удаляем нетерминал из стека
|
||||
stack.pop()
|
||||
|
||||
# Добавляем правило в rules_applied
|
||||
rule_number = self.rule_numbers[(top, tuple(production))]
|
||||
rules_applied.append(rule_number)
|
||||
|
||||
# Добавляем правило в стек в обратном порядке
|
||||
for symbol in reversed(production):
|
||||
stack.append(symbol)
|
||||
else:
|
||||
expected_symbols = list(self.lookup_table[top].keys())
|
||||
raise ValueError(
|
||||
f"Syntax error: expected one of {expected_symbols}, got '{current_symbol}'"
|
||||
)
|
||||
|
||||
# Случай 2: Верхний символ стека - терминал
|
||||
elif top != "$":
|
||||
if top == current_symbol:
|
||||
# Удаляем терминал из стека
|
||||
stack.pop()
|
||||
# Переходим к следующему символу ввода
|
||||
input_pos += 1
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Syntax error: expected '{top}', got '{current_symbol}'"
|
||||
)
|
||||
|
||||
# Случай 3: Верхний символ стека - $
|
||||
else: # top == "$"
|
||||
if current_symbol == "$":
|
||||
# Успешный синтаксический анализ
|
||||
stack.pop() # Удаляем $ из стека
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Syntax error: unexpected symbols at end of input: '{current_symbol}'"
|
||||
)
|
||||
|
||||
return rules_applied
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{generate}}
|
||||
|
||||
Метод \texttt{generate} генерирует случайное предложение по заданной грамматике. Код метода представлен в листинге~\ref{lst:generate}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar и необязательный параметр symbol типа str или None, указывающий начальный символ для генерации.
|
||||
|
||||
Метод возвращает кортеж (tuple), содержащий список терминалов и список номеров применённых правил.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{generate}.}, label={lst:generate}]
|
||||
def generate(self, symbol: str | None = None) -> tuple[list[str], list[int]]:
|
||||
"""Генерирует предложение по заданной грамматике. Возвращает список терминалов
|
||||
и список номеров применённых правил."""
|
||||
|
||||
if symbol is None:
|
||||
return self.generate(self.start_symbol)
|
||||
|
||||
# Если символ - терминал, возвращаем его
|
||||
if symbol not in self.productions:
|
||||
return [symbol], []
|
||||
|
||||
# Выбираем случайное правило для нетерминала
|
||||
rules = self.productions[symbol]
|
||||
chosen_rule = random.choice(rules)
|
||||
|
||||
# Получаем номер выбранного правила
|
||||
rule_number = self.rule_numbers[(symbol, tuple(chosen_rule))]
|
||||
|
||||
# Инициализируем результаты
|
||||
terminals = []
|
||||
rule_numbers = [rule_number]
|
||||
|
||||
# Разворачиваем каждый символ в правой части правила
|
||||
for s in chosen_rule:
|
||||
sub_terminals, sub_rules = self.generate(s)
|
||||
terminals.extend(sub_terminals)
|
||||
rule_numbers.extend(sub_rules)
|
||||
|
||||
return terminals, rule_numbers
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Метод \texttt{generate\_derivation\_steps}}
|
||||
|
||||
Метод \texttt{generate\_derivation\_steps} преобразует список номеров правил в последовательность шагов вывода. Код метода представлен в листинге~\ref{lst:generate_derivation_steps}.
|
||||
|
||||
Метод принимает ссылку на объект класса Grammar и список rule\_numbers типа list[int], содержащий номера правил для применения.
|
||||
|
||||
Метод возвращает список (list) строк, представляющий каждый шаг вывода предложения.
|
||||
|
||||
\begin{lstlisting}[caption={Код метода \texttt{generate\_derivation\_steps}.}, label={lst:generate_derivation_steps}]
|
||||
def generate_derivation_steps(self, rule_numbers: list[int]) -> list[str]:
|
||||
"""Преобразует список номеров правил в последовательность шагов вывода.
|
||||
Возвращает список строк, представляющих каждый шаг вывода."""
|
||||
|
||||
# Получаем соответствие между номерами правил и самими правилами
|
||||
rule_details = {num: rule for rule, num in self.rule_numbers.items()}
|
||||
|
||||
# Начинаем с начального символа
|
||||
current = self.start_symbol
|
||||
steps = [current]
|
||||
|
||||
# Применяем каждое правило по порядку
|
||||
for rule_num in rule_numbers:
|
||||
if rule_num in rule_details:
|
||||
non_terminal, replacement = rule_details[rule_num]
|
||||
|
||||
# Находим первое вхождение нетерминала и заменяем его
|
||||
words = current.split()
|
||||
for i, word in enumerate(words):
|
||||
if word == non_terminal:
|
||||
words[i : i + 1] = replacement
|
||||
break
|
||||
|
||||
current = " ".join(words)
|
||||
steps.append(current)
|
||||
|
||||
return steps
|
||||
\end{lstlisting}
|
||||
|
||||
\subsection{Функция \texttt{main}}
|
||||
|
||||
Функция \texttt{main} реализует интерактивную консольную оболочку для взаимодействия с пользователем. Код функции представлен в листинге~\ref{lst:main}.
|
||||
|
||||
\begin{lstlisting}[caption={Код функции \texttt{main}.}, label={lst:main}]
|
||||
def main():
|
||||
print("Программа для работы с LL(1)-грамматиками")
|
||||
print("=" * 60)
|
||||
print("Варианты команд:")
|
||||
print(" - load <файл> - загрузить грамматику из файла (по умолчанию grammar.txt)")
|
||||
print(" - check <строка> - проверить, соответствует ли строка грамматике")
|
||||
print(" - generate - сгенерировать случайную строку по грамматике")
|
||||
print(" - exit - выход из программы")
|
||||
print("=" * 60)
|
||||
|
||||
# Загружаем грамматику по умолчанию при старте
|
||||
grammar = load_grammar()
|
||||
|
||||
while True:
|
||||
command = input("\nВведите команду: ").strip()
|
||||
|
||||
if not command:
|
||||
continue
|
||||
|
||||
parts = command.split(maxsplit=1)
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == "exit":
|
||||
print("Выход из программы.")
|
||||
break
|
||||
|
||||
elif cmd == "load":
|
||||
filename = "grammar.txt"
|
||||
if len(parts) > 1:
|
||||
filename = parts[1].strip()
|
||||
grammar = load_grammar(filename)
|
||||
|
||||
elif cmd == "check":
|
||||
input_string = ""
|
||||
if len(parts) > 1:
|
||||
input_string = parts[1].strip()
|
||||
check_string(grammar, input_string)
|
||||
|
||||
elif cmd == "generate":
|
||||
generate_string(grammar)
|
||||
|
||||
else:
|
||||
print(f"Неизвестная команда: {cmd}")
|
||||
print("Доступные команды: load, check, generate, exit")
|
||||
\end{lstlisting}
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы программы}
|
||||
Результаты работы программы представлены на Рис.~\ref{fig:result1}.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/result1.png}
|
||||
\caption{Результаты работы программы.}
|
||||
\label{fig:result1}
|
||||
\end{figure}
|
||||
|
||||
% \newpage
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/wrong.png}
|
||||
\caption{Реакция программы на некорректный пользовательский ввод.}
|
||||
\label{fig:wrong}
|
||||
\end{figure}
|
||||
|
||||
На Рис.~\ref{fig:wrong} представлена реакция программы на некорректный пользовательский ввод.
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
В ходе выполнения лабораторной работы была построена контекстно-свободная грамматика для подмножества немецкого языка, описывающая простое прошедшее время Претерит. На основе разработанной грамматики была реализована программа, которая проверяет принадлежность входной строки заданному языку и генерирует случайные корректные предложения. Для анализа предложений использовался алгоритм LL(1)-разбора, основанный на построении множеств FIRST и FOLLOW для всех нетерминалов грамматики и создании таблицы синтаксического анализа.
|
||||
|
||||
Из достоинств выполнения лабораторной работы можно выделить возможность задания грамматики в отдельном текстовом файле, что позволяет легко изменять и расширять её без модификации программного кода. Использование объектно-ориентированного подхода в реализации обеспечило хорошую структуру кода. Вся логика работы с грамматикой реализована в классе Grammar, а обработка пользовательского ввода вынесена в функцию main.
|
||||
|
||||
К недостаткам текущей реализации можно отнести ограниченность словарного запаса, что сужает разнообразие генерируемых предложений. Также алгоритм генерации не контролирует длину предложений, что может приводить к избыточно длинным или коротким конструкциям. В текущей версии система не учитывает некоторые грамматические особенности немецкого языка, например, склонение прилагательных и согласование артиклей с родом существительных.
|
||||
|
||||
Функционал программы несложно масштабировать. Грамматику легко расширять, добавляя новые слова и правила в текстовый файл без необходимости изменения программного кода. Класс Grammar может служить хорошей основой для создания полноценного LL(k) анализатора.
|
||||
|
||||
На выполнение лабораторной работы ушло около 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 с.
|
||||
\end{thebibliography}
|
||||
|
||||
\end{document}
|
||||