\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}