diff --git a/lab3/img/result1.png b/lab3/img/result1.png new file mode 100644 index 0000000..3ed3e8c Binary files /dev/null and b/lab3/img/result1.png differ diff --git a/lab3/img/wrong.png b/lab3/img/wrong.png new file mode 100644 index 0000000..16acfe6 Binary files /dev/null and b/lab3/img/wrong.png differ diff --git a/lab3/programm/grammar.txt b/lab3/programm/grammar.txt index 0458c02..91b664b 100644 --- a/lab3/programm/grammar.txt +++ b/lab3/programm/grammar.txt @@ -23,4 +23,4 @@ Предлог -> "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" \ No newline at end of file +Глагол -> "las" | "schrieb" | "kochte" | "aß" | "ging" | "kam" | "sagte" | "machte" | "liebte" \ No newline at end of file diff --git a/lab3/report.tex b/lab3/report.tex index 113645b..b896c9f 100644 --- a/lab3/report.tex +++ b/lab3/report.tex @@ -149,25 +149,38 @@ \section*{Введение} \addcontentsline{toc}{section}{Введение} - Лабораторная №3 по дисциплине <<Математическая логика>> заключается в построении контексно-свободной грамматики подмножества естественного языка. В данном варианте рассматривается грамматика простого прошедшего времени немецкого языка. + Лабораторная №3 заключается в построении контексно-свободной грамматики подмножества естественного языка и написании программы для генерации предложений языка и проверки предложений на принадлежность языку. В данном варианте рассматривается грамматика простого прошедшего времени немецкого языка. - Простое прошедшее время в немецком языке называется \textit{Претерит} или \textit{Претеритум} (нем. \textit{Präteritum} или нем. \textit{Imperfekt}). \textit{Претеритум} служит для передачи действия в прошлом. Это время не требует образования сложных конструкций, что позволяет относить его к простым формам. - + Простое прошедшее время в немецком языке называется \textit{Претерит} или \textit{Претеритум} (нем. \textit{Präteritum} или нем. \textit{Imperfekt}). Это время не требует образования сложных конструкций, что позволяет относить его к простым формам. \newpage \section {Математическое описание} - \subsection{КС-грамматика} + \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 @@ -205,245 +218,771 @@ "kam" | "sagte" | "machte" \end{verbatim} - \subsection{БНФ-нотация} + Терминальными являются все символы в кавычках, все остальные символы являются нетерминальными. - % \begin{verbatim} - % Предложение ::= ПрямойПорядок | Инверсия - - % ПрямойПорядок ::= Подлежащее [ПридаточноеПредложение] - % Глагол {ВторостепенныйЧлен} [Отрицание] + Язык, пораждаемый данной грамматикой, как и сама грамматика, является контекстно-свободным, так как: + \begin{itemize} + \item В левых частях всех продукций присутствует только один нетерминальный символ, а значит язык не является контекстно-зависимым. + \item Язык не является автоматным из-за наличия придаточных предложений неограниченной вложенности. Придаточные предложения являются аналогом скобочной структуры, которая не может быть описана конечным автоматом. + \end{itemize} - % ПридаточноеПредложение ::= ", " Союз Подлежащее [ПридаточноеПредложение] - % Глагол [Отрицание] ", " - - % Инверсия ::= Обстоятельство Глагол Подлежащее - % [ПридаточноеПредложение] {ВторостепенныйЧлен} [Отрицание] - - % Обстоятельство ::= ОбстоятельствоВремени | - % ОбстоятельствоМеста | - % ОбстоятельствоОбразаДействия - - % ВторостепенныйЧлен ::= Обстоятельство | - % КосвенноеДополнение | - % ПрямоеДополнение - - % Подлежащее ::= Местоимение | ИменнаяГруппа - - % ИменнаяГруппа ::= [Артикль | ПритяжательноеМестоимение] - % {Прилагательное} Существительное - % [ДополнениеКПодлежащему] - - % ДополнениеКПодлежащему ::= Предлог Существительное - - % КосвенноеДополнение ::= Предлог ИменнаяГруппа - - % ПрямоеДополнение ::= [Артикль | ПритяжательноеМестоимение] - % {Прилагательное} Существительное - - % ОбстоятельствоМеста ::= Предлог [Артикль | ПритяжательноеМестоимение] - % Существительное - - % Союз ::= "welcher" (которого) | "welche" (которую) - % | "welches" (которого, ср. р) - - % Отрицание ::= "nicht" - - % Местоимение ::= "ich" (я) | "du" (ты) | "er" (он) | "sie" (она/они) | - % "es" (оно) | "wir" (мы) | "ihr" (вы) | "Sie" (Вы) - - % ПритяжательноеМестоимение ::= "mein" (мой) | "dein" (твой) | - % "sein" (его) | "ihr" (её) | "unser" (наш) | - % "euer" (ваш) | "ihre" (их) - - % Артикль ::= "der" | "die" | "das" | "ein" | - % "eine" | "einen" | "einem" | "einer" - - % Прилагательное ::= "alt" (старый) | "jung" (молодой) | - % "groß" (большой) | "klein" (маленький) | "gut" (хороший) | - % "schlecht" (плохой) | "schnell" (быстрый) | "langsam" (медленный) - - % Существительное ::= "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" (делал) - - % Инфинитив ::= "lesen" (читать) | "schreiben" (писать) | - % "kochen" (готовить) | "essen" (есть) | - % "gehen" (идти) | "kommen" (приходить) | - % "sagen" (говорить) | "machen" (делать) - - % Причастие ::= "gelesen" (прочитанный) | "geschrieben" (написанный) | - % "gekocht" (приготовленный) | "gegessen" (съеденный) | - % "gegangen" (ушедший) | "gekommen" (пришедший) | - % "gesagt" (сказанный) | "gemacht" (сделанный) - - % \end{verbatim} - - \subsection{Примеры предложений} - - Ich las gestern ein altes Buch - - Der alte Mann las ein Buch - - + Также эта грамматика является LL(1)-грамматикой. Множества FIRST и FOLLOW для всех нетерминальных символов не пересекаются. В дальнейшем это особенность используется для распознавания предложений языка. + \subsection{Грамматика в БНФ} - \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} - \textbf{Пример 1: Der alte Mann las ein Buch.} (Старый мужчина читал книгу.) + \subsection{Пример вывода предложения} + На Рис.~1 представлено дерево вывода предложения \textit{<>} + + \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$. - \begin{tabularx}{\textwidth}{|l|l|l|X|} - \hline - Позиция & Компонент & Элемент & Разбор по БНФ \\ - \hline - 1 & Подлежащее & der alte Mann & ИменнаяГруппа → Артикль + Прилагательное + Существительное \\ - \hline - 2 & Сказуемое & las & Глагол \\ - \hline - 3 & Второстепенные члены & ein Buch & ПрямоеДополнение → Артикль + Существительное \\ - \hline - \end{tabularx} + Учитывая грамматику с правилами $A_1 \rightarrow w_1, \dots, A_n \rightarrow w_n$, можно вычислить $\texttt{FIRST}(w_i)$ и $\texttt{FIRST}(A_i)$ для каждого правила следующим образом: - \vspace{0.5cm} + \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} - \textbf{Пример 2: Gestern las der alte Mann ein Buch.} (Вчера старый мужчина читал книгу.) + Однако множеств \texttt{FIRST()} недостаточно, чтобы построить таблицу синтаксического анализа. Так происходит, потому что правая сторона $w$ правила могла бы в конечном счёте быть приведена к пустой строке. Таким образом синтаксический анализатор должен также использовать правило $A \rightarrow w$, если $\varepsilon \in \texttt{FIRST}(w)$ и во входном потоке символ, который может следовать за $A$. Поэтому также необходимо построить \textbf{множество \texttt{FOLLOW()} для $A$}, которое определяется как множество терминалов $a$, таких что существует строка символов $\alpha A a \beta$, которая может быть получена из начального символа. - \begin{tabularx}{\textwidth}{|l|l|l|X|} - \hline - Позиция & Компонент & Элемент & Разбор по БНФ \\ - \hline - 1 & Обстоятельство & Gestern & ОбстоятельствоВремени → НаречиеВремени \\ - \hline - 2 & Сказуемое & las & Глагол \\ - \hline - 3 & Подлежащее & der alte Mann & ИменнаяГруппа → Артикль + Прилагательное + Существительное \\ - \hline - 4 & Второстепенные члены & ein Buch & ПрямоеДополнение → Артикль + Существительное \\ - \hline - \end{tabularx} + Вычисление множеств \texttt{FOLLOW()} для нетерминалов в грамматике выполняется следующим образом: - \vspace{0.5cm} + \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} - \textbf{Пример 3: Mein Freund aus Berlin schrieb mir gestern einen Brief.} + Теперь можно точно определить, какие правила будут содержаться в таблице синтаксического анализа. Если $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} - \begin{tabularx}{\textwidth}{|l|l|l|X|} - \hline - Позиция & Компонент & Элемент & Разбор по БНФ \\ - \hline - 1 & Подлежащее & Mein Freund aus Berlin & ИменнаяГруппа → ПритяжательноеМестоимение + Существительное + ДополнениеКПодлежащему (Предлог + Существительное) \\ - \hline - 2 & Сказуемое & schrieb & ПростойГлагол ВПретерите \\ - \hline - 3 & Второстепенные члены & mir & Косвенное Дополнение → Местоимение \\ - \hline - 4 & Второстепенные члены & gestern & Обстоятельство Времени → НаречиеВремени \\ - \hline - 5 & Второстепенные члены & einen Brief & ПрямоеДополнение → Артикль + Существительное \\ - \hline - \end{tabularx} + Если таблица будет содержать не более одного правила в каждой ячейке, то синтаксический анализатор сможет однозначно определить, какое правило необходимо применить на каждом шаге разбора. В этом случае грамматику является \textbf{LL(1)} грамматикой. + + \subsection{Алгоритм разбора предложений} + + Перед запуском алгоритма разбора в стек помещаются два символа: + \begin{itemize} + \item Специальный символ~\$ — признак конца входной цепочки. + \item Начальный символ грамматики (на вершину стека). + \end{itemize} + + Cинтаксический анализатор выполняет три различных вида действий в зависимости от того, находится ли на вершине стека нетерминал, терминал или специальный символ~\$. - \vspace{0.5cm} + \begin{enumerate} + \item \textbf{Если на вершине стека — нетерминал}, то в таблице синтаксического анализа ищется правило грамматики, находящееся на пересечении строки и столбца, соответствующих этому нетерминалу на вершине стека и текущему входному символу. Если же в указанной ячейке таблицы правило отсутствует — синтаксический анализатор останавливается и сообщает об ошибке. - \textbf{Пример 4: Am Abend kochte sie schnell das Abendessen in der Küche.} + \item \textbf{Если на вершине стека — терминал}, то синтаксический анализатор сравнивает его с текущим входным символом. Если они равны, то входной символ считывается, а соответствующий символ из вершины стека — удаляется. - (Вечером она быстро приготовила ужин на кухне.) + \item \textbf{Если на вершине стека — специальный символ~\$}, и текущий символ на ленте также равен~\$, то синтаксический анализатор сообщает об успешном распознавании цепочки. В противном случае — сообщает об ошибке. В обоих случаях происходит останов анализатора. + \end{enumerate} - \begin{tabularx}{\textwidth}{|l|l|l|X|} - \hline - Позиция & Компонент & Элемент & Разбор по БНФ \\ - \hline - 1 & Обстоятельство & Am Abend & ОбстоятельствоВремени → Предлог + Существительное \\ - \hline - 2 & Сказуемое & kochte & Глагол \\ - \hline - 3 & Подлежащее & sie & Местоимение \\ - \hline - 4 & Второстепенные члены & schnell & ОбстоятельствоОбразаДействия \\ - \hline - 5 & Второстепенные члены & das Abendessen & ПрямоеДополнение → Артикль + Существительное \\ - \hline - 6 & Второстепенные члены & in der Küche & ОбстоятельствоМеста → Предлог + Артикль + Существительное \\ - \hline - \end{tabularx} + Эти шаги повторяются до тех пор, пока не произойдёт останов. После останова мы получаем либо сообщение об ошибке, либо сообщение об успешном распознавании цепочки. - \vspace{0.5cm} - - \textbf{Пример 5: Er wollte ein Buch lesen.} (Он хотел прочитать книгу.) - - \begin{tabularx}{\textwidth}{|l|l|l|X|} - \hline - Позиция & Компонент & Элемент & Разбор по БНФ \\ - \hline - 1 & Подлежащее & Er & Местоимение \\ - \hline - 2 & Сказуемое & wollte lesen & СоставноеСказуемое → МодальныйГлаголВПретерите + Инфинитив \\ - \hline - 3 & Второстепенные члены & ein Buch & ПрямоеДополнение → Артикль + Существительное \\ - \hline - \end{tabularx} + \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}. + Результаты работы программы представлены на Рис.~\ref{fig:result1}. - % \begin{figure}[h!] - % \centering - % \includegraphics[width=1\linewidth]{img/result1.png} - % \caption{Результаты работы программы.} - % \label{fig:result1} - % \end{figure} + \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} + \begin{figure}[h!] + \centering + \includegraphics[width=0.5\linewidth]{img/wrong.png} + \caption{Реакция программы на некорректный пользовательский ввод.} + \label{fig:wrong} + \end{figure} - % На Рис.~\ref{fig:wrong} представлена реакция программы на некорректный пользовательский ввод. + На Рис.~\ref{fig:wrong} представлена реакция программы на некорректный пользовательский ввод. \newpage \section*{Заключение} \addcontentsline{toc}{section}{Заключение} - В ходе выполнения лабораторной работы было построено регулярное выражение для распознавания различных форматов вещественных чисел. В соответствии с теоремой Клини по заданному регулярному выражению, задающему регулярный - язык, был построен недетерминированный конечный автомат-распознаватель. Затем полученный конечный автомат был детерминирован. На основе разработанного автомата была реализована программа, которая проверяет соответствие входной строки заданному формату и генерирует случайные корректные строки. + В ходе выполнения лабораторной работы была построена контекстно-свободная грамматика для подмножества немецкого языка, описывающая простое прошедшее время Претерит. На основе разработанной грамматики была реализована программа, которая проверяет принадлежность входной строки заданному языку и генерирует случайные корректные предложения. Для анализа предложений использовался алгоритм LL(1)-разбора, основанный на построении множеств FIRST и FOLLOW для всех нетерминалов грамматики и создании таблицы синтаксического анализа. - Из достоинств выполнения лабораторной работы можно выделить структурирование кода за счёт использования ООП. Вся логика работы с конечными автоматами вынесена в отдельный класс \texttt{FiniteAutomaton} с четко разделенными методами для проверки строк и генерации случайных строк. Создана удобная интерактивная консольная оболочка для взаимодействия с пользователем, позволяющая выполнять различные команды. + Из достоинств выполнения лабораторной работы можно выделить возможность задания грамматики в отдельном текстовом файле, что позволяет легко изменять и расширять её без модификации программного кода. Использование объектно-ориентированного подхода в реализации обеспечило хорошую структуру кода. Вся логика работы с грамматикой реализована в классе Grammar, а обработка пользовательского ввода вынесена в функцию main. - К недостаткам текущей реализации можно отнести следующие аспекты. Во-первых, переходы в автомате представлены в виде строк, содержащих допустимые символы, такой способ представления переходов не является самым оптимальным с точки зрения производительности. Во-вторых, в реализации генерации случайных строк вероятность остановки одинакова для всех финальных состояний, что может приводить к неравномерному распределению различных форматов чисел в генерируемых строках. + К недостаткам текущей реализации можно отнести ограниченность словарного запаса, что сужает разнообразие генерируемых предложений. Также алгоритм генерации не контролирует длину предложений, что может приводить к избыточно длинным или коротким конструкциям. В текущей версии система не учитывает некоторые грамматические особенности немецкого языка, например, склонение прилагательных и согласование артиклей с родом существительных. - Функционал программы несложно масштабировать. Класс \texttt{FiniteAutomaton} может быть использован для работы с различными конечными автоматами-распознавателями. Для изменения распознаваемого языка достаточно задать новые параметры для автомата, не меняя базовую логику программы. Однако, текущая реализация работает только с символьными переходами, поэтому задать строчные переходы в виде, например, регулярных выражений не представляется возможным. Однако подобный функционал также несложно реализовать, взяв за основу существующий код. + Функционал программы несложно масштабировать. Грамматику легко расширять, добавляя новые слова и правила в текстовый файл без необходимости изменения программного кода. Класс Grammar может служить хорошей основой для создания полноценного LL(k) анализатора. - На выполнение лабораторной работы ушло около 10 часов. Работа была выполнена в среде разработки Visual Studio Code. Программа написана на Python версии 3.10. + На выполнение лабораторной работы ушло около 12 часов. Работа была выполнена в среде разработки Visual Studio Code. Программа написана на Python версии 3.13. \newpage \section*{Список литературы} @@ -455,8 +994,6 @@ Востров, А.В. Курс лекций по дисциплине <<Математическая логика>>. URL \url{https://tema.spbstu.ru/compiler/} (дата обращения 01.04.2025 г.) \bibitem{lutz} Лутц, М. Изучаем Python. 5-е изд. / М. Лутц. — СПб.: Питер, 2019. — 1216 с. - \bibitem{friedl} - Фридл, Дж. Регулярные выражения = Mastering Regular Expressions / Дж. Фридл. — СПб.: Питер, 2001. — 352 с. — (Библиотека программиста). \end{thebibliography} \end{document} \ No newline at end of file