\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{Общая характеристика претеритума} Претеритум (Präteritum, Imperfekt) является одной из двух основных форм прошедшего времени в немецком языке, наряду с перфектом (Perfekt). В отличие от перфекта, претеритум представляет собой простую (синтетическую) форму, образуемую без вспомогательных глаголов, путём изменения основной формы глагола. Претеритум используется преимущественно в письменной речи, особенно в художественной литературе, исторических текстах и формальных документах. В разговорной речи он встречается редко, за исключением глаголов \textit{sein} (быть), \textit{haben} (иметь), модальных глаголов, а также в некоторых диалектах северной Германии. \subsection{Структура предложений в претеритуме} Структура предложений в претеритуме соответствует общим правилам построения немецких предложений. Основные типы структур: \begin{enumerate} \item \textbf{Прямой порядок слов} (главное предложение): \begin{center} \textit{Подлежащее} + \textit{глагол в претеритуме} + \textit{дополнения и обстоятельства} \end{center} Например: \textit{Der Mann las ein Buch.} (Мужчина читал книгу.) \item \textbf{Обратный порядок слов} (инверсия): \begin{center} \textit{Обстоятельство} + \textit{глагол в претеритуме} + \textit{подлежащее} + \textit{другие члены предложения} \end{center} Например: \textit{Gestern las der Mann ein Buch.} (Вчера мужчина читал книгу.) \item \textbf{Придаточное предложение}: \begin{center} \textit{..., союз} + \textit{подлежащее} + \textit{второстепенные члены} + \textit{глагол в претеритуме} \end{center} Например: \textit{Ich wusste, dass der Mann ein Buch las.} (Я знал, что мужчина читал книгу.) \end{enumerate} \subsection{Образование форм претеритума} \subsubsection{Слабые (правильные) глаголы} Образуются по формуле: основа глагола + суффикс \textit{-te} + личное окончание: \begin{center} \begin{tabular}{|l|l|} \hline \textbf{Лицо} & \textbf{Окончание (machen - делать)} \\ \hline ich & mach\textbf{te} \\ du & mach\textbf{test} \\ er/sie/es & mach\textbf{te} \\ wir & mach\textbf{ten} \\ ihr & mach\textbf{tet} \\ sie/Sie & mach\textbf{ten} \\ \hline \end{tabular} \end{center} \subsubsection{Сильные (неправильные) глаголы} Образуются путём изменения корневой гласной без добавления суффикса \textit{-te}: \begin{center} \begin{tabular}{|l|l|} \hline \textbf{Лицо} & \textbf{Окончание (lesen - читать)} \\ \hline ich & \textbf{las} \\ du & \textbf{las}st \\ er/sie/es & \textbf{las} \\ wir & \textbf{las}en \\ ihr & \textbf{las}t \\ sie/Sie & \textbf{las}en \\ \hline \end{tabular} \end{center} \subsubsection{Смешанные глаголы} Сочетают особенности сильных и слабых глаголов: меняют корневую гласную и получают суффикс \textit{-te}. Например: \textit{bringen} (приносить) – \textit{brachte}, \textit{kennen} (знать) – \textit{kannte}. \subsection{Отрицание в претеритуме} Отрицание в предложениях с претеритом образуется с помощью отрицательной частицы \textit{nicht}, которая обычно ставится после глагола и перед дополнениями: \textit{Der Mann las nicht ein Buch.} (Мужчина не читал книгу.) При отрицании всего предложения \textit{nicht} ставится в конце: \textit{Der Mann las ein Buch nicht.} (Мужчина не читал книгу [а что-то другое].) Размещение \textit{nicht} в конце предложения также часто используется в разговорной речи и может указывать на отрицание всего действия или ситуации в целом: \textit{Er kam gestern nicht.} (Он вчера не приходил.) \textit{Das Kind spielte draußen nicht.} (Ребенок не играл на улице.) \subsection{Особенности использования претеритума} В немецком языке выбор между претеритумом и перфектом часто определяется: \begin{itemize} \item \textbf{Стилем повествования:} претеритум более характерен для литературного стиля \item \textbf{Типом текста:} в биографиях, исторических текстах, сказках преимущественно используется претеритум \item \textbf{Региональными особенностями:} в Северной Германии претеритум используется чаще \item \textbf{Типом глагола:} для глаголов \textit{sein}, \textit{haben} и модальных глаголов претеритум используется даже в разговорной речи \end{itemize} В рамках данной работы мы рассматриваем формализованную грамматику, учитывающую основные структурные особенности предложений в претеритуме, без полного описания всех лексических и морфологических особенностей немецкого языка. \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{<>} \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 для всех нетерминалов грамматики и создании таблицы синтаксического анализа. Из достоинств выполнения лабораторной работы можно выделить возможность задания грамматики в отдельном текстовом файле, что позволяет легко изменять и расширять её без модификации программного кода. Также программа автоматически проверяет, что введенная грамматика является LL(1)-грамматикой. В противном случае, программа выводит сообщение об ошибке, в указывается на конкретные правила грамматики, между выбором которых возникает неоднозначность. К недостаткам текущей реализации можно отнести ограниченность словарного запаса, что сужает разнообразие генерируемых предложений. Также алгоритм генерации не контролирует длину предложений, что может приводить к избыточно длинным или коротким конструкциям. В текущей версии система не учитывает некоторые грамматические особенности немецкого языка, например, склонение прилагательных и согласование артиклей с родом существительных. Функционал программы несложно масштабировать. Грамматику легко расширять, добавляя новые слова и правила в текстовый файл без необходимости изменения программного кода. Класс 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}