Compare commits
6 Commits
c186941dce
...
a60d2a9db7
| Author | SHA1 | Date | |
|---|---|---|---|
| a60d2a9db7 | |||
| 96c9e6db53 | |||
| 681b56b31b | |||
| d7a0e400cd | |||
| 3c09cc0e9b | |||
| 529da8d98e |
8
lab3/.gitignore
vendored
Normal file
8
lab3/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
**/*
|
||||||
|
!.gitignore
|
||||||
|
!report.tex
|
||||||
|
!img
|
||||||
|
!img/**
|
||||||
|
!programm
|
||||||
|
!programm/*.py
|
||||||
|
!programm/*.txt
|
||||||
BIN
lab3/img/result1.png
Normal file
BIN
lab3/img/result1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
lab3/img/wrong.png
Normal file
BIN
lab3/img/wrong.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
3
lab3/programm/.gitignore
vendored
Normal file
3
lab3/programm/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
grammar_*
|
||||||
|
analysis_*
|
||||||
|
generation_*
|
||||||
409
lab3/programm/grammar.py
Normal file
409
lab3/programm/grammar.py
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
import random
|
||||||
|
import re
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from prettytable import PrettyTable
|
||||||
|
|
||||||
|
|
||||||
|
class Grammar:
|
||||||
|
EPSILON: str = "epsilon"
|
||||||
|
|
||||||
|
def __init__(self, text: str):
|
||||||
|
self.productions: OrderedDict[str, list[list[str]]] = OrderedDict()
|
||||||
|
self.start_symbol: str = ""
|
||||||
|
self._parse_productions(text)
|
||||||
|
|
||||||
|
self.terminals: set[str] = set()
|
||||||
|
self._find_terminals()
|
||||||
|
|
||||||
|
self.first_sets: dict[str, set[str]] = {}
|
||||||
|
self._calculate_first_sets()
|
||||||
|
|
||||||
|
self.follow_sets: dict[str, set[str]] = {}
|
||||||
|
self._calculate_follow_sets()
|
||||||
|
|
||||||
|
self.lookup_table: dict[str, dict[str, list[str]]] = {}
|
||||||
|
self._fill_lookup_table()
|
||||||
|
|
||||||
|
# Сопостовляем уникальный номер с каждым правилом
|
||||||
|
self.rule_numbers = {}
|
||||||
|
rule_idx = 1
|
||||||
|
for nt, rules in self.productions.items():
|
||||||
|
for rule in rules:
|
||||||
|
self.rule_numbers[(nt, tuple(rule))] = rule_idx
|
||||||
|
rule_idx += 1
|
||||||
|
|
||||||
|
def _parse_productions(self, text: str):
|
||||||
|
for line in text.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
non_terminal, rule = line.split("->")
|
||||||
|
if self.start_symbol == "":
|
||||||
|
self.start_symbol = non_terminal.strip()
|
||||||
|
|
||||||
|
non_terminal = non_terminal.strip()
|
||||||
|
rules = [
|
||||||
|
[
|
||||||
|
symbol.strip('"')
|
||||||
|
for symbol in re.findall(r"\".*?\"|\S+", rule.strip())
|
||||||
|
if symbol.strip('"') != self.EPSILON
|
||||||
|
]
|
||||||
|
for rule in rule.split("|")
|
||||||
|
]
|
||||||
|
|
||||||
|
if non_terminal not in self.productions:
|
||||||
|
self.productions[non_terminal] = []
|
||||||
|
|
||||||
|
self.productions[non_terminal].extend(rules)
|
||||||
|
|
||||||
|
def _find_terminals(self):
|
||||||
|
for rules in self.productions.values():
|
||||||
|
for rule in rules:
|
||||||
|
for symbol in rule:
|
||||||
|
if symbol not in self.productions:
|
||||||
|
self.terminals.add(symbol)
|
||||||
|
|
||||||
|
def _calculate_first_sets(self):
|
||||||
|
# Инициализация FIRST для всех символов
|
||||||
|
for non_terminal in self.productions:
|
||||||
|
self.first_sets[non_terminal] = set()
|
||||||
|
|
||||||
|
# Для терминалов FIRST содержит только сам терминал
|
||||||
|
for terminal in self.terminals:
|
||||||
|
self.first_sets[terminal] = {terminal}
|
||||||
|
|
||||||
|
# Вычисление FIRST для нетерминалов
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
changed = False
|
||||||
|
for non_terminal, rules in self.productions.items():
|
||||||
|
for rule in rules:
|
||||||
|
if not rule: # Пустое правило (эпсилон)
|
||||||
|
if "" not in self.first_sets[non_terminal]:
|
||||||
|
self.first_sets[non_terminal].add("")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
all_can_derive_epsilon = True
|
||||||
|
for i, symbol in enumerate(rule):
|
||||||
|
# Добавляем все символы из FIRST(symbol) кроме эпсилон
|
||||||
|
first_without_epsilon = self.first_sets[symbol] - {""}
|
||||||
|
old_size = len(self.first_sets[non_terminal])
|
||||||
|
self.first_sets[non_terminal].update(first_without_epsilon)
|
||||||
|
if len(self.first_sets[non_terminal]) > old_size:
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Если symbol не может порождать эпсилон, прерываем
|
||||||
|
if "" not in self.first_sets[symbol]:
|
||||||
|
all_can_derive_epsilon = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# Если все символы в правиле могут порождать эпсилон,
|
||||||
|
# то и нетерминал может порождать эпсилон
|
||||||
|
if (
|
||||||
|
all_can_derive_epsilon
|
||||||
|
and "" not in self.first_sets[non_terminal]
|
||||||
|
):
|
||||||
|
self.first_sets[non_terminal].add("")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
def _calculate_follow_sets(self):
|
||||||
|
# инициализировать Fo(S) = { $ }, а все остальные Fo(Ai) пустыми множествами
|
||||||
|
for non_terminal in self.productions:
|
||||||
|
self.follow_sets[non_terminal] = set()
|
||||||
|
|
||||||
|
# Добавляем символ конца строки $ для начального символа
|
||||||
|
self.follow_sets[self.start_symbol].add("$")
|
||||||
|
|
||||||
|
# Повторяем, пока в наборах Follow происходят изменения
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
# Для каждого нетерминала Aj в грамматике
|
||||||
|
for non_terminal_j, rules in self.productions.items():
|
||||||
|
|
||||||
|
# Для каждого правила Aj → w
|
||||||
|
for rule in rules:
|
||||||
|
|
||||||
|
# Для каждого символа в правиле
|
||||||
|
for i, symbol in enumerate(rule):
|
||||||
|
|
||||||
|
# Если символ - нетерминал Ai
|
||||||
|
if symbol in self.productions:
|
||||||
|
# w' - остаток правила после Ai
|
||||||
|
remainder = rule[i + 1 :] if i + 1 < len(rule) else []
|
||||||
|
|
||||||
|
# Если есть терминалы после Ai (w' не пусто)
|
||||||
|
if remainder:
|
||||||
|
# Вычисляем First(w')
|
||||||
|
first_of_remainder = self._first_of_sequence(remainder)
|
||||||
|
|
||||||
|
# Если терминал a находится в First(w'), то добавляем a к Follow(Ai)
|
||||||
|
for terminal in first_of_remainder - {""}:
|
||||||
|
if terminal not in self.follow_sets[symbol]:
|
||||||
|
self.follow_sets[symbol].add(terminal)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Если ε находится в First(w'), то добавляем Follow(Aj) к Follow(Ai)
|
||||||
|
if "" in first_of_remainder:
|
||||||
|
old_size = len(self.follow_sets[symbol])
|
||||||
|
self.follow_sets[symbol].update(
|
||||||
|
self.follow_sets[non_terminal_j]
|
||||||
|
)
|
||||||
|
if len(self.follow_sets[symbol]) > old_size:
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Если w' пусто (Ai в конце правила), то добавляем Follow(Aj) к Follow(Ai)
|
||||||
|
else:
|
||||||
|
old_size = len(self.follow_sets[symbol])
|
||||||
|
self.follow_sets[symbol].update(
|
||||||
|
self.follow_sets[non_terminal_j]
|
||||||
|
)
|
||||||
|
if len(self.follow_sets[symbol]) > old_size:
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
def _first_of_sequence(self, sequence):
|
||||||
|
"""Вычисляет множество FIRST для последовательности символов"""
|
||||||
|
if not sequence:
|
||||||
|
return {""}
|
||||||
|
|
||||||
|
result = set()
|
||||||
|
all_can_derive_epsilon = True
|
||||||
|
|
||||||
|
for symbol in sequence:
|
||||||
|
# Добавляем First(symbol) без эпсилон к результату
|
||||||
|
result.update(self.first_sets[symbol] - {""})
|
||||||
|
|
||||||
|
# Если symbol не может порождать эпсилон, останавливаемся
|
||||||
|
if "" not in self.first_sets[symbol]:
|
||||||
|
all_can_derive_epsilon = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# Если все символы могут порождать эпсилон, добавляем эпсилон к результату
|
||||||
|
if all_can_derive_epsilon:
|
||||||
|
result.add("")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _fill_lookup_table(self):
|
||||||
|
# Для каждого нетерминала A
|
||||||
|
for non_terminal, rules in self.productions.items():
|
||||||
|
# Формируем таблицу синтаксического анализа
|
||||||
|
self.lookup_table[non_terminal] = {}
|
||||||
|
|
||||||
|
# Для каждого правила A → w
|
||||||
|
for rule in rules:
|
||||||
|
# Вычисляем First(w)
|
||||||
|
first_of_rule = self._first_of_sequence(rule)
|
||||||
|
|
||||||
|
# Для каждого терминала a в First(w)
|
||||||
|
for terminal in first_of_rule - {""}:
|
||||||
|
# Если T[A,a] уже содержит правило, грамматика не LL(1)
|
||||||
|
if terminal in self.lookup_table[non_terminal]:
|
||||||
|
raise ValueError(
|
||||||
|
"Грамматика не является LL(1)-грамматикой.\n"
|
||||||
|
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||||
|
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||||
|
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем правило в таблицу
|
||||||
|
self.lookup_table[non_terminal][terminal] = rule
|
||||||
|
|
||||||
|
# Если эпсилон в First(w), то для каждого b в Follow(A)
|
||||||
|
if "" in first_of_rule:
|
||||||
|
for terminal in self.follow_sets[non_terminal]:
|
||||||
|
# Если T[A,b] уже содержит правило, грамматика не LL(1)
|
||||||
|
if terminal in self.lookup_table[non_terminal]:
|
||||||
|
raise ValueError(
|
||||||
|
"Грамматика не является LL(1)-грамматикой.\n"
|
||||||
|
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||||
|
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||||
|
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем правило в таблицу
|
||||||
|
self.lookup_table[non_terminal][terminal] = rule
|
||||||
|
|
||||||
|
def format_rules(self) -> str:
|
||||||
|
result = []
|
||||||
|
sorted_rules = sorted(self.rule_numbers.items(), key=lambda x: x[1])
|
||||||
|
|
||||||
|
for rule, number in sorted_rules:
|
||||||
|
non_terminal, symbols = rule
|
||||||
|
rule_text = f"{number}: {non_terminal} -> {' '.join(symbols)}"
|
||||||
|
result.append(rule_text)
|
||||||
|
|
||||||
|
return "\n".join(result)
|
||||||
|
|
||||||
|
def format_lookup_table(self) -> str:
|
||||||
|
table = PrettyTable()
|
||||||
|
terminals = list(self.terminals)
|
||||||
|
table.field_names = [""] + terminals + ["$"]
|
||||||
|
|
||||||
|
for non_terminal in self.productions:
|
||||||
|
row = [non_terminal]
|
||||||
|
for terminal in terminals + ["$"]:
|
||||||
|
if terminal in self.lookup_table[non_terminal]:
|
||||||
|
rule = self.lookup_table[non_terminal][terminal]
|
||||||
|
rule_num = self.rule_numbers.get((non_terminal, tuple(rule)), "")
|
||||||
|
row.append(f"{rule_num}: {' '.join(rule)}")
|
||||||
|
else:
|
||||||
|
row.append(" - ")
|
||||||
|
table.add_row(row)
|
||||||
|
return str(table)
|
||||||
|
|
||||||
|
def format_first_sets(self) -> str:
|
||||||
|
"""Форматирует множества FIRST в читаемый вид."""
|
||||||
|
result = []
|
||||||
|
result.append("Множества FIRST:")
|
||||||
|
result.append("=" * 40)
|
||||||
|
|
||||||
|
# Сортируем для гарантии порядка вывода
|
||||||
|
for symbol in sorted(self.first_sets.keys()):
|
||||||
|
# Заменяем пустую строку на эпсилон для лучшей читаемости
|
||||||
|
first_set = {
|
||||||
|
self.EPSILON if item == "" else item for item in self.first_sets[symbol]
|
||||||
|
}
|
||||||
|
result.append(f"FIRST({symbol}) = {{{', '.join(sorted(first_set))}}}")
|
||||||
|
|
||||||
|
return "\n".join(result)
|
||||||
|
|
||||||
|
def format_follow_sets(self) -> str:
|
||||||
|
"""Форматирует множества FOLLOW в читаемый вид."""
|
||||||
|
result = []
|
||||||
|
result.append("Множества FOLLOW:")
|
||||||
|
result.append("=" * 40)
|
||||||
|
|
||||||
|
# Обрабатываем только нетерминалы
|
||||||
|
for non_terminal in sorted(self.productions.keys()):
|
||||||
|
follow_set = self.follow_sets.get(non_terminal, set())
|
||||||
|
result.append(
|
||||||
|
f"FOLLOW({non_terminal}) = {{{', '.join(sorted(follow_set))}}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(result)
|
||||||
|
|
||||||
|
def analyze(self, input_tokens: list[str]) -> list[int]:
|
||||||
|
input_tokens = input_tokens.copy()
|
||||||
|
input_tokens += ["$"]
|
||||||
|
input_pos = 0
|
||||||
|
|
||||||
|
# Инициализируем стек с терминальным символом и начальным символом
|
||||||
|
stack = ["$", self.start_symbol]
|
||||||
|
|
||||||
|
rules_applied = []
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
top = stack[-1]
|
||||||
|
|
||||||
|
current_symbol = (
|
||||||
|
input_tokens[input_pos] if input_pos < len(input_tokens) else "$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Случай 1: Верхний символ стека - нетерминал
|
||||||
|
if top in self.productions:
|
||||||
|
# Ищем правило в таблице синтаксического анализа
|
||||||
|
if current_symbol in self.lookup_table[top]:
|
||||||
|
# Получаем правило
|
||||||
|
production = self.lookup_table[top][current_symbol]
|
||||||
|
|
||||||
|
# Удаляем нетерминал из стека
|
||||||
|
stack.pop()
|
||||||
|
|
||||||
|
# Добавляем правило в rules_applied
|
||||||
|
rule_number = self.rule_numbers[(top, tuple(production))]
|
||||||
|
rules_applied.append(rule_number)
|
||||||
|
|
||||||
|
# Добавляем правило в стек в обратном порядке
|
||||||
|
for symbol in reversed(production):
|
||||||
|
stack.append(symbol)
|
||||||
|
else:
|
||||||
|
expected_symbols = list(self.lookup_table[top].keys())
|
||||||
|
raise ValueError(
|
||||||
|
f"Syntax error: expected one of {expected_symbols}, got '{current_symbol}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Случай 2: Верхний символ стека - терминал
|
||||||
|
elif top != "$":
|
||||||
|
if top == current_symbol:
|
||||||
|
# Удаляем терминал из стека
|
||||||
|
stack.pop()
|
||||||
|
# Переходим к следующему символу ввода
|
||||||
|
input_pos += 1
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Syntax error: expected '{top}', got '{current_symbol}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Случай 3: Верхний символ стека - $
|
||||||
|
else: # top == "$"
|
||||||
|
if current_symbol == "$":
|
||||||
|
# Успешный синтаксический анализ
|
||||||
|
stack.pop() # Удаляем $ из стека
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Syntax error: unexpected symbols at end of input: '{current_symbol}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return rules_applied
|
||||||
|
|
||||||
|
def generate(self, symbol: str | None = None) -> tuple[list[str], list[int]]:
|
||||||
|
"""Генерирует предложение по заданной грамматике. Возвращает список терминалов
|
||||||
|
и список номеров применённых правил."""
|
||||||
|
|
||||||
|
if symbol is None:
|
||||||
|
return self.generate(self.start_symbol)
|
||||||
|
|
||||||
|
# Если символ - терминал, возвращаем его
|
||||||
|
if symbol not in self.productions:
|
||||||
|
return [symbol], []
|
||||||
|
|
||||||
|
# Выбираем случайное правило для нетерминала
|
||||||
|
rules = self.productions[symbol]
|
||||||
|
chosen_rule = random.choice(rules)
|
||||||
|
|
||||||
|
# Получаем номер выбранного правила
|
||||||
|
rule_number = self.rule_numbers[(symbol, tuple(chosen_rule))]
|
||||||
|
|
||||||
|
# Инициализируем результаты
|
||||||
|
terminals = []
|
||||||
|
rule_numbers = [rule_number]
|
||||||
|
|
||||||
|
# Разворачиваем каждый символ в правой части правила
|
||||||
|
for s in chosen_rule:
|
||||||
|
sub_terminals, sub_rules = self.generate(s)
|
||||||
|
terminals.extend(sub_terminals)
|
||||||
|
rule_numbers.extend(sub_rules)
|
||||||
|
|
||||||
|
return terminals, rule_numbers
|
||||||
|
|
||||||
|
def generate_derivation_steps(self, rule_numbers: list[int]) -> list[str]:
|
||||||
|
"""Преобразует список номеров правил в последовательность шагов вывода.
|
||||||
|
Возвращает список строк, представляющих каждый шаг вывода."""
|
||||||
|
|
||||||
|
# Получаем соответствие между номерами правил и самими правилами
|
||||||
|
rule_details = {num: rule for rule, num in self.rule_numbers.items()}
|
||||||
|
|
||||||
|
# Начинаем с начального символа
|
||||||
|
current = self.start_symbol
|
||||||
|
steps = [current]
|
||||||
|
|
||||||
|
# Применяем каждое правило по порядку
|
||||||
|
for rule_num in rule_numbers:
|
||||||
|
if rule_num in rule_details:
|
||||||
|
non_terminal, replacement = rule_details[rule_num]
|
||||||
|
|
||||||
|
# Находим первое вхождение нетерминала и заменяем его
|
||||||
|
words = current.split()
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
if word == non_terminal:
|
||||||
|
words[i : i + 1] = replacement
|
||||||
|
break
|
||||||
|
|
||||||
|
current = " ".join(words)
|
||||||
|
steps.append(current)
|
||||||
|
|
||||||
|
return steps
|
||||||
26
lab3/programm/grammar.txt
Normal file
26
lab3/programm/grammar.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
Предложение -> Повествовательное "."
|
||||||
|
Повествовательное -> ПрямойПорядок | Инверсия
|
||||||
|
ПрямойПорядок -> Подлежащее ДополнениеКПодлежащему Глагол ВторостепенныеЧлены Отрицание
|
||||||
|
Инверсия -> Обстоятельство Глагол Подлежащее ВторостепенныеЧлены Отрицание
|
||||||
|
Подлежащее -> ИменнаяГруппа ПридаточноеПредложение | Местоимение
|
||||||
|
ПридаточноеПредложение -> "," Союз Подлежащее Глагол Отрицание "," | epsilon
|
||||||
|
ВторостепенныеЧлены -> ВторостепенныйЧлен ВторостепенныеЧлены | epsilon
|
||||||
|
ВторостепенныйЧлен -> Обстоятельство | Дополнение
|
||||||
|
Обстоятельство -> ОбстоятельствоВремени | ОбстоятельствоМеста | ОбстоятельствоОбразаДействия
|
||||||
|
ИменнаяГруппа -> АртикльЛибоМестоимение Прилагательные Существительное
|
||||||
|
АртикльЛибоМестоимение -> Артикль | ПритяжательноеМестоимение | epsilon
|
||||||
|
Прилагательные -> Прилагательное Прилагательные | epsilon
|
||||||
|
ДополнениеКПодлежащему -> Предлог Существительное | epsilon
|
||||||
|
Дополнение -> АртикльЛибоМестоимение Прилагательные Существительное
|
||||||
|
ОбстоятельствоМеста -> Предлог АртикльЛибоМестоимение Существительное
|
||||||
|
Союз -> "welcher" | "welche" | "welches"
|
||||||
|
Отрицание -> "nicht" | epsilon
|
||||||
|
Местоимение -> "ich" | "du" | "er" | "sie" | "es" | "wir" | "ihr" | "Sie"
|
||||||
|
ПритяжательноеМестоимение -> "mein" | "dein" | "sein" | "unser" | "euer" | "ihre"
|
||||||
|
Артикль -> "der" | "die" | "das" | "ein" | "eine" | "einen" | "einem" | "einer"
|
||||||
|
Прилагательное -> "alt" | "jung" | "groß" | "klein" | "schön" | "freundlich" | "süß" | "ruhig"
|
||||||
|
Существительное -> "Mann" | "Frau" | "Kind" | "Buch" | "Brief" | "Freund" | "Abendessen" | "Suppe"
|
||||||
|
Предлог -> "in" | "auf" | "unter" | "aus" | "mit" | "für" | "zu" | "am"
|
||||||
|
ОбстоятельствоВремени -> "gestern" | "heute" | "morgen" | "damals" | "jetzt" | "früh" | "spät" | "immer"
|
||||||
|
ОбстоятельствоОбразаДействия -> "schnell" | "langsam" | "gut" | "schlecht" | "laut" | "leise" | "gern" | "fleißig"
|
||||||
|
Глагол -> "las" | "schrieb" | "kochte" | "aß" | "ging" | "kam" | "sagte" | "machte" | "liebte"
|
||||||
174
lab3/programm/main.py
Normal file
174
lab3/programm/main.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
from grammar import Grammar
|
||||||
|
|
||||||
|
|
||||||
|
def load_grammar(filename: str = "grammar.txt") -> Grammar | None:
|
||||||
|
try:
|
||||||
|
with open(filename, "r", encoding="utf-8") as file:
|
||||||
|
text = file.read()
|
||||||
|
grammar = Grammar(text)
|
||||||
|
|
||||||
|
# Сохраняем информацию о грамматике в файлы
|
||||||
|
with open("grammar_rules.txt", "w", encoding="utf-8") as output_file:
|
||||||
|
output_file.write(grammar.format_rules())
|
||||||
|
print("Правила грамматики с номерами сохранены в grammar_rules.txt")
|
||||||
|
|
||||||
|
with open("grammar_lookup_table.txt", "w", encoding="utf-8") as output_file:
|
||||||
|
output_file.write(grammar.format_lookup_table())
|
||||||
|
print(
|
||||||
|
"Таблица синтаксического анализа сохранена в grammar_lookup_table.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open("grammar_first.txt", "w", encoding="utf-8") as output_file:
|
||||||
|
output_file.write(grammar.format_first_sets())
|
||||||
|
print("Множества FIRST сохранены в grammar_first.txt")
|
||||||
|
|
||||||
|
with open("grammar_follow.txt", "w", encoding="utf-8") as output_file:
|
||||||
|
output_file.write(grammar.format_follow_sets())
|
||||||
|
print("Множества FOLLOW сохранены в grammar_follow.txt")
|
||||||
|
|
||||||
|
print(f"Грамматика успешно загружена из файла {filename}")
|
||||||
|
return grammar
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Ошибка: Файл {filename} не найден")
|
||||||
|
return None
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Ошибка при загрузке грамматики: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Неизвестная ошибка: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize_string(input_string: str) -> list[str]:
|
||||||
|
input_string = input_string.replace(",", " , ").replace(".", " . ")
|
||||||
|
return input_string.split()
|
||||||
|
|
||||||
|
|
||||||
|
def check_string(grammar: Grammar | None, input_string: str) -> None:
|
||||||
|
if not grammar:
|
||||||
|
print("Ошибка: Грамматика не загружена")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Проверка строки: '{input_string}'")
|
||||||
|
try:
|
||||||
|
input_tokens = tokenize_string(input_string)
|
||||||
|
|
||||||
|
if not input_tokens:
|
||||||
|
parse_result = grammar.analyze(input_tokens)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
input_tokens[0] = input_tokens[0][0].lower() + input_tokens[0][1:]
|
||||||
|
parse_result = grammar.analyze(input_tokens)
|
||||||
|
except ValueError as e:
|
||||||
|
input_tokens[0] = input_tokens[0][0].upper() + input_tokens[0][1:]
|
||||||
|
parse_result = grammar.analyze(input_tokens)
|
||||||
|
|
||||||
|
print(f"Результат: Строка соответствует грамматике")
|
||||||
|
print(f"Применённые правила: {parse_result}")
|
||||||
|
|
||||||
|
# Сохраняем результат анализа в файл
|
||||||
|
with open("analysis_result.txt", "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"Input: {input_string}\n")
|
||||||
|
f.write("Applied rules: ")
|
||||||
|
f.write(str(parse_result))
|
||||||
|
f.write("\n\n")
|
||||||
|
f.write("Derivation steps:\n")
|
||||||
|
|
||||||
|
derivation_steps = grammar.generate_derivation_steps(parse_result)
|
||||||
|
for step in derivation_steps:
|
||||||
|
f.write(f"{step}\n")
|
||||||
|
|
||||||
|
print("Подробный результат анализа сохранен в analysis_result.txt")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Результат: Строка не соответствует грамматике")
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Произошла ошибка при анализе: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def post_process_string(string: str) -> str:
|
||||||
|
if string:
|
||||||
|
string = string[0].upper() + string[1:]
|
||||||
|
string = string.replace(" ,", ",")
|
||||||
|
string = string.replace(" .", ".")
|
||||||
|
string = string.replace(",.", ".")
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
def generate_string(grammar: Grammar | None) -> None:
|
||||||
|
if not grammar:
|
||||||
|
print("Ошибка: Грамматика не загружена")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
terminals, rules = grammar.generate()
|
||||||
|
generated_string = " ".join(terminals)
|
||||||
|
generated_string = post_process_string(generated_string)
|
||||||
|
print(f"Сгенерированная строка: {generated_string}")
|
||||||
|
print(f"Применённые правила: {rules}")
|
||||||
|
|
||||||
|
# Сохраняем результат генерации в файл
|
||||||
|
with open("generation_result.txt", "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"Generated string: {generated_string}\n")
|
||||||
|
f.write("Applied rules: ")
|
||||||
|
f.write(str(rules))
|
||||||
|
f.write("\n\n")
|
||||||
|
f.write("Derivation steps:\n")
|
||||||
|
|
||||||
|
derivation_steps = grammar.generate_derivation_steps(rules)
|
||||||
|
for step in derivation_steps:
|
||||||
|
f.write(f"{step}\n")
|
||||||
|
|
||||||
|
print("Подробный результат генерации сохранен в generation_result.txt")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Произошла ошибка при генерации: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Программа для работы с LL(1)-грамматиками")
|
||||||
|
print("=" * 60)
|
||||||
|
print("Варианты команд:")
|
||||||
|
print(" - load <файл> - загрузить грамматику из файла (по умолчанию grammar.txt)")
|
||||||
|
print(" - check <строка> - проверить, соответствует ли строка грамматике")
|
||||||
|
print(" - generate - сгенерировать случайную строку по грамматике")
|
||||||
|
print(" - exit - выход из программы")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Загружаем грамматику по умолчанию при старте
|
||||||
|
grammar = load_grammar()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
command = input("\nВведите команду: ").strip()
|
||||||
|
|
||||||
|
if not command:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = command.split(maxsplit=1)
|
||||||
|
cmd = parts[0].lower()
|
||||||
|
|
||||||
|
if cmd == "exit":
|
||||||
|
print("Выход из программы.")
|
||||||
|
break
|
||||||
|
|
||||||
|
elif cmd == "load":
|
||||||
|
filename = "grammar.txt"
|
||||||
|
if len(parts) > 1:
|
||||||
|
filename = parts[1].strip()
|
||||||
|
grammar = load_grammar(filename)
|
||||||
|
|
||||||
|
elif cmd == "check":
|
||||||
|
input_string = ""
|
||||||
|
if len(parts) > 1:
|
||||||
|
input_string = parts[1].strip()
|
||||||
|
check_string(grammar, input_string)
|
||||||
|
|
||||||
|
elif cmd == "generate":
|
||||||
|
generate_string(grammar)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Неизвестная команда: {cmd}")
|
||||||
|
print("Доступные команды: load, check, generate, exit")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
999
lab3/report.tex
Normal file
999
lab3/report.tex
Normal file
@@ -0,0 +1,999 @@
|
|||||||
|
\documentclass[a4paper, final]{article}
|
||||||
|
%\usepackage{literat} % Нормальные шрифты
|
||||||
|
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
|
||||||
|
\usepackage{tabularx}
|
||||||
|
\usepackage[T2A]{fontenc}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage[russian]{babel}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
|
||||||
|
\usepackage{ragged2e} %для растягивания по ширине
|
||||||
|
\usepackage{setspace} %для межстрочно го интервала
|
||||||
|
\usepackage{moreverb} %для работы с листингами
|
||||||
|
\usepackage{indentfirst} % для абзацного отступа
|
||||||
|
\usepackage{moreverb} %для печати в листинге исходного кода программ
|
||||||
|
\usepackage{pdfpages} %для вставки других pdf файлов
|
||||||
|
\usepackage{tikz}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{afterpage}
|
||||||
|
\usepackage{longtable}
|
||||||
|
\usepackage{float}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
% \usepackage[paper=A4,DIV=12]{typearea}
|
||||||
|
\usepackage{pdflscape}
|
||||||
|
% \usepackage{lscape}
|
||||||
|
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{multirow}
|
||||||
|
|
||||||
|
\renewcommand\verbatimtabsize{4\relax}
|
||||||
|
\renewcommand\listingoffset{0.2em} %отступ от номеров строк в листинге
|
||||||
|
\renewcommand{\arraystretch}{1.4} % изменяю высоту строки в таблице
|
||||||
|
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} %для настройки заголовка таблицы
|
||||||
|
\usepackage{listings} %листинги
|
||||||
|
\usepackage{xcolor} % цвета
|
||||||
|
\usepackage{hyperref}% для гиперссылок
|
||||||
|
\usepackage{enumitem} %для перечислений
|
||||||
|
|
||||||
|
\newcommand{\specialcell}[2][l]{\begin{tabular}[#1]{@{}l@{}}#2\end{tabular}}
|
||||||
|
|
||||||
|
|
||||||
|
\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях
|
||||||
|
|
||||||
|
\hypersetup{colorlinks,
|
||||||
|
allcolors=[RGB]{010 090 200}} %красивые гиперссылки (не красные)
|
||||||
|
|
||||||
|
% подгружаемые языки — подробнее в документации listings (это всё для листингов)
|
||||||
|
\lstloadlanguages{ SQL}
|
||||||
|
% включаем кириллицу и добавляем кое−какие опции
|
||||||
|
\lstset{tabsize=2,
|
||||||
|
breaklines,
|
||||||
|
basicstyle=\footnotesize,
|
||||||
|
columns=fullflexible,
|
||||||
|
flexiblecolumns,
|
||||||
|
numbers=left,
|
||||||
|
numberstyle={\footnotesize},
|
||||||
|
keywordstyle=\color{blue},
|
||||||
|
inputencoding=cp1251,
|
||||||
|
extendedchars=true
|
||||||
|
}
|
||||||
|
\lstdefinelanguage{MyC}{
|
||||||
|
language=SQL,
|
||||||
|
% ndkeywordstyle=\color{darkgray}\bfseries,
|
||||||
|
% identifierstyle=\color{black},
|
||||||
|
% morecomment=[n]{/**}{*/},
|
||||||
|
% commentstyle=\color{blue}\ttfamily,
|
||||||
|
% stringstyle=\color{red}\ttfamily,
|
||||||
|
% morestring=[b]",
|
||||||
|
% showstringspaces=false,
|
||||||
|
% morecomment=[l][\color{gray}]{//},
|
||||||
|
keepspaces=true,
|
||||||
|
escapechar=\%,
|
||||||
|
texcl=true
|
||||||
|
}
|
||||||
|
|
||||||
|
\textheight=24cm % высота текста
|
||||||
|
\textwidth=16cm % ширина текста
|
||||||
|
\oddsidemargin=0pt % отступ от левого края
|
||||||
|
\topmargin=-1.5cm % отступ от верхнего края
|
||||||
|
\parindent=24pt % абзацный отступ
|
||||||
|
\parskip=5pt % интервал между абзацами
|
||||||
|
\tolerance=2000 % терпимость к "жидким" строкам
|
||||||
|
\flushbottom % выравнивание высоты страниц
|
||||||
|
|
||||||
|
|
||||||
|
% Настройка листингов
|
||||||
|
\lstset{
|
||||||
|
language=python,
|
||||||
|
extendedchars=\true,
|
||||||
|
inputencoding=utf8,
|
||||||
|
keepspaces=true,
|
||||||
|
% captionpos=b, % подписи листингов снизу
|
||||||
|
}
|
||||||
|
|
||||||
|
\begin{document} % начало документа
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
% НАЧАЛО ТИТУЛЬНОГО ЛИСТА
|
||||||
|
\begin{center}
|
||||||
|
\hfill \break
|
||||||
|
\hfill \break
|
||||||
|
\normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
|
||||||
|
федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
|
||||||
|
\normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt]
|
||||||
|
\normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt]
|
||||||
|
\normalsize{Направление: 02.03.01 <<Математика и компьютерные науки>>}\\
|
||||||
|
|
||||||
|
\hfill \break
|
||||||
|
\hfill \break
|
||||||
|
\hfill \break
|
||||||
|
\hfill \break
|
||||||
|
\large{Лабораторная работа №3}\\
|
||||||
|
\large{<<Грамматика простого прошедшего времени}\\
|
||||||
|
\large{немецкого языка>>}\\
|
||||||
|
\large{по дисциплине}\\
|
||||||
|
\large{<<Математическая логика и теория автоматов>>}\\
|
||||||
|
\large{Вариант 15}\\
|
||||||
|
|
||||||
|
% \hfill \break
|
||||||
|
\hfill \break
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\small{
|
||||||
|
\begin{tabular}{lrrl}
|
||||||
|
\!\!\!Студент, & \hspace{2cm} & & \\
|
||||||
|
\!\!\!группы 5130201/20102 & \hspace{2cm} & \underline{\hspace{3cm}} &Тищенко А. А. \\\\
|
||||||
|
\!\!\!Преподаватель & \hspace{2cm} & \underline{\hspace{3cm}} & Востров А. В. \\\\
|
||||||
|
&&\hspace{4cm}
|
||||||
|
\end{tabular}
|
||||||
|
\begin{flushright}
|
||||||
|
<<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2025г.
|
||||||
|
\end{flushright}
|
||||||
|
}
|
||||||
|
|
||||||
|
\hfill \break
|
||||||
|
% \hfill \break
|
||||||
|
\begin{center} \small{Санкт-Петербург, 2025} \end{center}
|
||||||
|
\thispagestyle{empty} % выключаем отображение номера для этой страницы
|
||||||
|
|
||||||
|
% КОНЕЦ ТИТУЛЬНОГО ЛИСТА
|
||||||
|
\newpage
|
||||||
|
|
||||||
|
\tableofcontents
|
||||||
|
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
|
||||||
|
\section*{Введение}
|
||||||
|
\addcontentsline{toc}{section}{Введение}
|
||||||
|
Лабораторная №3 заключается в построении контексно-свободной грамматики подмножества естественного языка и написании программы для генерации предложений языка и проверки предложений на принадлежность языку. В данном варианте рассматривается грамматика простого прошедшего времени немецкого языка.
|
||||||
|
|
||||||
|
Простое прошедшее время в немецком языке называется \textit{Претерит} или \textit{Претеритум} (нем. \textit{Präteritum} или нем. \textit{Imperfekt}). Это время не требует образования сложных конструкций, что позволяет относить его к простым формам.
|
||||||
|
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
\section {Математическое описание}
|
||||||
|
\subsection{Особенности грамматики}
|
||||||
|
Грамматика, рассматриваемая в рамках данной лабораторной работы, имеет следующие особенности:
|
||||||
|
\begin{itemize}
|
||||||
|
\item Рассматриваются только повествовательные и повествовательные отрицательные предложения.
|
||||||
|
\item Учтены два возможных порядка членов предложения: прямой и обратный. В прямом порядке сначала идет подлежащее, потом сказуемое, а затем второстепенные члены предложения. В обратном порядке на первом месте стоит обстоятельство, на втором сказуемое, на третьем подлежащее, а затем второстепенные члены предложения. В \textit{Претеритуме} сказуемое всегда стоит на втором месте.
|
||||||
|
\item Придаточные предложения могут относится только к подлежащему и начинаться с союзов \textit{welcher} (который), \textit{welche} (которая), \textit{welches} (которое). При этом вложенность придаточных предложений не ограничена.
|
||||||
|
\item Порядок второстепеннных членов не фиксирован. Несмотря на то, что в немецком языке существует предпочтительный порядок слов, в данной грамматике он не учитывается, потому что порядок не явлется строгим и реальные предложения языка далеко не всегда ему соответствуют.
|
||||||
|
\item Рассматриваются только простые сказуемые, не содержащие вспомогательных глаголов.
|
||||||
|
\item Рассматриваются три вида обстоятельств: обстоятельства времени, места и образа действия. В реальных предложениях языка могут встречаться и другие виды, но перечисленные являются наиболее часто встречающимися.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection{Грамматика в виде списка продукций}
|
||||||
|
\label{subsec:grammar}
|
||||||
|
|
||||||
|
Формально можно описать грамматику в виде списка продукций:
|
||||||
|
|
||||||
|
\begin{verbatim}
|
||||||
|
Предложение -> Повествовательное "."
|
||||||
|
Повествовательное -> ПрямойПорядок | Инверсия
|
||||||
|
ПрямойПорядок -> Подлежащее ДополнениеКПодлежащему Глагол
|
||||||
|
ВторостепенныеЧлены Отрицание
|
||||||
|
Инверсия -> Обстоятельство Глагол Подлежащее
|
||||||
|
ВторостепенныеЧлены Отрицание
|
||||||
|
Подлежащее -> ИменнаяГруппа ПридаточноеПредложение | Местоимение
|
||||||
|
ПридаточноеПредложение -> "," Союз Подлежащее Глагол Отрицание "," |
|
||||||
|
epsilon
|
||||||
|
ВторостепенныеЧлены -> ВторостепенныйЧлен ВторостепенныеЧлены |
|
||||||
|
epsilon
|
||||||
|
ВторостепенныйЧлен -> Обстоятельство | Дополнение
|
||||||
|
Обстоятельство -> ОбстоятельствоВремени | ОбстоятельствоМеста |
|
||||||
|
ОбстоятельствоОбразаДействия
|
||||||
|
ИменнаяГруппа -> АртикльЛибоМестоимение Прилагательные
|
||||||
|
Существительное
|
||||||
|
АртикльЛибоМестоимение -> Артикль | ПритяжательноеМестоимение |
|
||||||
|
epsilon
|
||||||
|
Прилагательные -> Прилагательное Прилагательные | epsilon
|
||||||
|
ДополнениеКПодлежащему -> Предлог Существительное | epsilon
|
||||||
|
Дополнение -> АртикльЛибоМестоимение Прилагательные Существительное
|
||||||
|
ОбстоятельствоМеста -> Предлог АртикльЛибоМестоимение Существительное
|
||||||
|
Союз -> "welcher" | "welche" | "welches"
|
||||||
|
Отрицание -> "nicht" | epsilon
|
||||||
|
Местоимение -> "ich" | "du" | "er" | "sie" | "es" | "wir" |
|
||||||
|
"ihr" | "Sie"
|
||||||
|
ПритяжательноеМестоимение -> "mein" | "dein" | "sein" |
|
||||||
|
"unser" | "euer" | "ihre"
|
||||||
|
Артикль -> "der" | "die" | "das" | "ein" | "eine" | "einen" |
|
||||||
|
"einem" | "einer"
|
||||||
|
Прилагательное -> "alt" | "jung" | "groß" | "klein" | "schön" |
|
||||||
|
"freundlich" | "süß" | "ruhig"
|
||||||
|
Существительное -> "Mann" | "Frau" | "Kind" | "Buch" | "Brief" |
|
||||||
|
"Freund" | "Abendessen" | "Suppe"
|
||||||
|
Предлог -> "in" | "auf" | "unter" | "aus" | "mit" | "für" |
|
||||||
|
"zu" | "am"
|
||||||
|
ОбстоятельствоВремени -> "gestern" | "heute" | "morgen" | "damals" |
|
||||||
|
"jetzt" | "früh" | "spät" | "immer"
|
||||||
|
ОбстоятельствоОбразаДействия -> "schnell" | "langsam" | "gut" |
|
||||||
|
"schlecht" | "laut" | "leise" |
|
||||||
|
"gern" | "fleißig"
|
||||||
|
Глагол -> "las" | "schrieb" | "kochte" | "aß" | "ging" |
|
||||||
|
"kam" | "sagte" | "machte"
|
||||||
|
\end{verbatim}
|
||||||
|
|
||||||
|
Терминальными являются все символы в кавычках, все остальные символы являются нетерминальными.
|
||||||
|
|
||||||
|
Язык, пораждаемый данной грамматикой, как и сама грамматика, является контекстно-свободным, так как:
|
||||||
|
\begin{itemize}
|
||||||
|
\item В левых частях всех продукций присутствует только один нетерминальный символ, а значит язык не является контекстно-зависимым.
|
||||||
|
\item Язык не является автоматным из-за наличия придаточных предложений неограниченной вложенности. Придаточные предложения являются аналогом скобочной структуры, которая не может быть описана конечным автоматом.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Также эта грамматика является LL(1)-грамматикой. Множества FIRST и FOLLOW для всех нетерминальных символов не пересекаются. В дальнейшем это особенность используется для распознавания предложений языка.
|
||||||
|
|
||||||
|
|
||||||
|
\subsection{Грамматика в БНФ}
|
||||||
|
|
||||||
|
\vspace{-0.2cm}
|
||||||
|
Грамматика в БНФ имеет следующий вид:
|
||||||
|
|
||||||
|
\vspace{-0.5cm}
|
||||||
|
\begin{verbatim}
|
||||||
|
<Предложение> ::= <Повествовательное> "."
|
||||||
|
<Повествовательное> ::= <ПрямойПорядок> | <Инверсия>
|
||||||
|
<ПрямойПорядок> ::= <Подлежащее> [ДополнениеКПодлежащему] <Глагол>
|
||||||
|
{ВторостепенныйЧлен} [Отрицание]
|
||||||
|
<Инверсия> ::= <Обстоятельство> <Глагол> <Подлежащее>
|
||||||
|
{ВторостепенныйЧлен} [Отрицание]
|
||||||
|
<Подлежащее> ::= <ИменнаяГруппа> [ПридаточноеПредложение] | Местоимение
|
||||||
|
<ПридаточноеПредложение> ::= "," <Союз> <Подлежащее> <Глагол> [Отрицание] ","
|
||||||
|
<ВторостепенныйЧлен> ::= <Обстоятельство> | <Дополнение>
|
||||||
|
<Обстоятельство> ::= <ОбстоятельствоВремени> | <ОбстоятельствоМеста> |
|
||||||
|
<ОбстоятельствоОбразаДействия>
|
||||||
|
<ИменнаяГруппа> ::= [Артикль | ПритяжательноеМестоимение]
|
||||||
|
{Прилагательное} <Существительное>
|
||||||
|
<ДополнениеКПодлежащему> ::= <Предлог> <Существительное>
|
||||||
|
<Дополнение> ::= [Артикль | ПритяжательноеМестоимение]
|
||||||
|
{Прилагательное} <Существительное>
|
||||||
|
<ОбстоятельствоМеста> ::= <Предлог>
|
||||||
|
[Артикль | ПритяжательноеМестоимение] <Существительное>
|
||||||
|
<Союз> ::= "welcher" | "welche" | "welches"
|
||||||
|
<Отрицание> ::= "nicht"
|
||||||
|
<Местоимение> ::= "ich" | "du" | "er" | "sie" | "es" | "wir" |
|
||||||
|
"ihr" | "Sie"
|
||||||
|
<ПритяжательноеМестоимение> ::= "mein" | "dein" | "sein" |
|
||||||
|
"unser" | "euer" | "ihre"
|
||||||
|
<Артикль> ::= "der" | "die" | "das" | "ein" | "eine" | "einen" |
|
||||||
|
"einem" | "einer"
|
||||||
|
<Прилагательное> ::= "alt" | "jung" | "groß" | "klein" | "schön" |
|
||||||
|
"freundlich" | "süß" | "ruhig"
|
||||||
|
<Существительное> ::= "Mann" | "Frau" | "Kind" | "Buch" | "Brief" |
|
||||||
|
"Freund" | "Abendessen" | "Suppe"
|
||||||
|
<Предлог> ::= "in" | "auf" | "unter" | "aus" | "mit" | "für" |
|
||||||
|
"zu" | "am"
|
||||||
|
<ОбстоятельствоВремени> ::= "gestern" | "heute" | "morgen" |
|
||||||
|
"damals" | "jetzt" | "früh" | "spät" | "immer"
|
||||||
|
<ОбстоятельствоОбразаДействия> ::= "schnell" | "langsam" | "gut" |
|
||||||
|
"schlecht" | "laut" | "leise" |
|
||||||
|
"gern" | "fleißig"
|
||||||
|
<Глагол> ::= "las" | "schrieb" | "kochte" | "aß" | "ging" |
|
||||||
|
"kam" | "sagte" | "machte" | "liebte"
|
||||||
|
\end{verbatim}
|
||||||
|
|
||||||
|
\subsection{Пример вывода предложения}
|
||||||
|
На Рис.~1 представлено дерево вывода предложения \textit{<<Mein alt Freund, welcher ich liebte, schrieb gestern einen schön Brief.>>}
|
||||||
|
|
||||||
|
\addtocounter{figure}{1}
|
||||||
|
\includepdf[pages={1}, fitpaper, pagecommand={
|
||||||
|
\thispagestyle{empty}
|
||||||
|
\begin{tikzpicture}[remember picture, overlay]
|
||||||
|
\node at (current page.south) [anchor=north, yshift=45pt] {\large{Рис 1. Пример дерева вывода предложения.}};
|
||||||
|
\end{tikzpicture}
|
||||||
|
}]{pdf/tree.pdf}
|
||||||
|
|
||||||
|
\subsection{Алгоритм построения LL(1) таблицы синтаксического анализа}
|
||||||
|
Чтобы заполнить таблицу синтаксического анализа, необходимо установить, какое правило грамматики синтаксический анализатор должен выбрать, если нетерминальное $A$ находится на вершине его стека и символ $a$ — во входном потоке. Легко заметить, что такое правило должно иметь форму $A \rightarrow w$ и что у языка, соответствующего $w$, должна быть по крайней мере одна строка, начинающаяся с $a$.
|
||||||
|
|
||||||
|
С этой целью мы определяем \textbf{множество \texttt{FIRST()} для $w$}, обозначенное как $\texttt{FIRST}(w)$, как множество терминалов, которые могут быть найдены в начале любой строки в $w$, и $\varepsilon$, если пустая строка также принадлежит $w$.
|
||||||
|
|
||||||
|
Учитывая грамматику с правилами $A_1 \rightarrow w_1, \dots, A_n \rightarrow w_n$, можно вычислить $\texttt{FIRST}(w_i)$ и $\texttt{FIRST}(A_i)$ для каждого правила следующим образом:
|
||||||
|
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Инициализировать каждое множество $\texttt{FIRST}(A_i)$ пустым множеством.
|
||||||
|
\item Добавить $\texttt{FIRST}(w_i)$ к $\texttt{FIRST}(A_i)$ для каждого правила $A_i \rightarrow w_i$, где $\texttt{FIRST}(w_i)$ определяется следующим образом:
|
||||||
|
\begin{itemize}
|
||||||
|
\item $\texttt{FIRST}(a w') = \{ a \}$ для каждого терминала $a$
|
||||||
|
\item $\texttt{FIRST}(A w') = \texttt{FIRST}(A)$ для каждого нетерминального $A$ с $\varepsilon \notin \texttt{FIRST}(A)$
|
||||||
|
\item $\texttt{FIRST}(A w') = \left( \texttt{FIRST}(A) \setminus \{ \varepsilon \} \right) \cup \texttt{FIRST}(w')$ для каждого нетерминального $A$ с $\varepsilon \in \texttt{FIRST}(A)$, включая случай $A_i \rightarrow A$, то есть $w' = \varepsilon$, в этом случае $\texttt{FIRST}(A w') = \texttt{FIRST}(A)$
|
||||||
|
\item $\texttt{FIRST}(\varepsilon) = \{ \varepsilon \}$
|
||||||
|
\end{itemize}
|
||||||
|
\item Повторять шаг 2, пока в множествах $\texttt{FIRST}$ происходят изменения.
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
Однако множеств \texttt{FIRST()} недостаточно, чтобы построить таблицу синтаксического анализа. Так происходит, потому что правая сторона $w$ правила могла бы в конечном счёте быть приведена к пустой строке. Таким образом синтаксический анализатор должен также использовать правило $A \rightarrow w$, если $\varepsilon \in \texttt{FIRST}(w)$ и во входном потоке символ, который может следовать за $A$. Поэтому также необходимо построить \textbf{множество \texttt{FOLLOW()} для $A$}, которое определяется как множество терминалов $a$, таких что существует строка символов $\alpha A a \beta$, которая может быть получена из начального символа.
|
||||||
|
|
||||||
|
Вычисление множеств \texttt{FOLLOW()} для нетерминалов в грамматике выполняется следующим образом:
|
||||||
|
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Инициализировать $\texttt{FOLLOW}(S) = \{ \$ \}$, а все остальные множества $\texttt{FOLLOW}(A_i)$ пустыми.
|
||||||
|
\item Если есть правило формы $A_j \rightarrow w A_i w'$, тогда:
|
||||||
|
\begin{itemize}
|
||||||
|
\item Если терминал $a \in \texttt{FIRST}(w')$, то добавить $a$ в $\texttt{FOLLOW}(A_i)$
|
||||||
|
\item Если $\varepsilon \in \texttt{FIRST}(w')$, то добавить $\texttt{FOLLOW}(A_j)$ в $\texttt{FOLLOW}(A_i)$
|
||||||
|
\item Если $w'$ имеет длину 0, то добавить $\texttt{FOLLOW}(A_j)$ в $\texttt{FOLLOW}(A_i)$
|
||||||
|
\end{itemize}
|
||||||
|
\item Повторять шаг 2, пока в множествах \texttt{FOLLOW()} происходят изменения.
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
Теперь можно точно определить, какие правила будут содержаться в таблице синтаксического анализа. Если $T[A, a]$ обозначает ячейку в таблице для нетерминального $A$ и терминала $a$, то:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item $T[A,a]$ содержит правило $A \rightarrow w$ тогда и только тогда, когда:
|
||||||
|
\begin{itemize}
|
||||||
|
\item $a \in \texttt{FIRST}(w)$ при проходе правила $A \rightarrow w$, или
|
||||||
|
\item $\varepsilon \in \texttt{FIRST}(w)$ и $a \in \texttt{FOLLOW}(A)$
|
||||||
|
\end{itemize}
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Если таблица будет содержать не более одного правила в каждой ячейке, то синтаксический анализатор сможет однозначно определить, какое правило необходимо применить на каждом шаге разбора. В этом случае грамматику является \textbf{LL(1)} грамматикой.
|
||||||
|
|
||||||
|
\subsection{Алгоритм разбора предложений}
|
||||||
|
|
||||||
|
Перед запуском алгоритма разбора в стек помещаются два символа:
|
||||||
|
\begin{itemize}
|
||||||
|
\item Специальный символ~\$ — признак конца входной цепочки.
|
||||||
|
\item Начальный символ грамматики (на вершину стека).
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Cинтаксический анализатор выполняет три различных вида действий в зависимости от того, находится ли на вершине стека нетерминал, терминал или специальный символ~\$.
|
||||||
|
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{Если на вершине стека — нетерминал}, то в таблице синтаксического анализа ищется правило грамматики, находящееся на пересечении строки и столбца, соответствующих этому нетерминалу на вершине стека и текущему входному символу. Если же в указанной ячейке таблицы правило отсутствует — синтаксический анализатор останавливается и сообщает об ошибке.
|
||||||
|
|
||||||
|
\item \textbf{Если на вершине стека — терминал}, то синтаксический анализатор сравнивает его с текущим входным символом. Если они равны, то входной символ считывается, а соответствующий символ из вершины стека — удаляется.
|
||||||
|
|
||||||
|
\item \textbf{Если на вершине стека — специальный символ~\$}, и текущий символ на ленте также равен~\$, то синтаксический анализатор сообщает об успешном распознавании цепочки. В противном случае — сообщает об ошибке. В обоих случаях происходит останов анализатора.
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
Эти шаги повторяются до тех пор, пока не произойдёт останов. После останова мы получаем либо сообщение об ошибке, либо сообщение об успешном распознавании цепочки.
|
||||||
|
|
||||||
|
\subsection{Алгоритм генерации предложения}
|
||||||
|
|
||||||
|
Генерация предложения происходит следующим образом:
|
||||||
|
\begin{enumerate}
|
||||||
|
\item В список помещается начальный символ грамматики.
|
||||||
|
\item Осуществляется проход по списку. Первый встреченный нетерминал заменяется на его случайно выбранную продукцию, после чего проход начинается заново.
|
||||||
|
\item Алгоритм завершается, когда в списке не осталось нетерминалов.
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
Этот алгоритм не позволяет контролировать длину предложения. Также алгоритм может не сходиться в общем случае. Но вероятность того, что алгоритм попадет в петлю убывает геометрически с ростом длины цепочки, так что на практике этот метод работает.
|
||||||
|
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
\section{Особенности реализации}
|
||||||
|
|
||||||
|
\subsection{Задание правил грамматики}
|
||||||
|
|
||||||
|
Грамматика задаётся в виде текстового файла, в котором каждая строка соответствует одной продукции. Синтаксис задания продукций соответствует общепринятому, за исключением того, что вместо $\varepsilon$ используется слово <<\texttt{epsilon}>>. Терминалы записываются в кавычках, нетерминалы — без кавычек. Левая и правая части продукции разделяются двумя символами -- <<\texttt{->}>>. Таким образом содержимое файла, соответствует тексту описания грамматики в разделе \ref{subsec:grammar}.
|
||||||
|
|
||||||
|
|
||||||
|
\subsection{Класс \texttt{Grammar}}
|
||||||
|
|
||||||
|
Класс \texttt{Grammar} содержит в себе всю информацию о грамматике, а также методы для работы с ней. Код конструктора класса представлен в листинге \ref{lst:grammar_constructor}. Конструктор класса принимает на вход строку, содержащую описание грамматики. Вызывает методы для парсинга продукций, нахождения терминалов, вычисления множеств FIRST и FOLLOW, заполнения таблицы синтаксического анализа.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код конструктора класса \texttt{Grammar}.}, label={lst:grammar_constructor}]
|
||||||
|
class Grammar:
|
||||||
|
EPSILON: str = "epsilon"
|
||||||
|
|
||||||
|
def __init__(self, text: str):
|
||||||
|
self.productions: OrderedDict[str, list[list[str]]] = OrderedDict()
|
||||||
|
self.start_symbol: str = ""
|
||||||
|
self._parse_productions(text)
|
||||||
|
|
||||||
|
self.terminals: set[str] = set()
|
||||||
|
self._find_terminals()
|
||||||
|
|
||||||
|
self.first_sets: dict[str, set[str]] = {}
|
||||||
|
self._calculate_first_sets()
|
||||||
|
|
||||||
|
self.follow_sets: dict[str, set[str]] = {}
|
||||||
|
self._calculate_follow_sets()
|
||||||
|
|
||||||
|
self.lookup_table: dict[str, dict[str, list[str]]] = {}
|
||||||
|
self._fill_lookup_table()
|
||||||
|
|
||||||
|
# Сопостовляем уникальный номер с каждым правилом
|
||||||
|
self.rule_numbers = {}
|
||||||
|
rule_idx = 1
|
||||||
|
for nt, rules in self.productions.items():
|
||||||
|
for rule in rules:
|
||||||
|
self.rule_numbers[(nt, tuple(rule))] = rule_idx
|
||||||
|
rule_idx += 1
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{\_parse\_productions}}
|
||||||
|
|
||||||
|
Метод \texttt{\_parse\_productions} выполняет разбор текстового описания грамматики и создаёт внутреннее представление продукций. Код метода представлен в листинге~\ref{lst:parse_productions}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar и строку text типа str, содержащую текстовое представление грамматики.
|
||||||
|
|
||||||
|
Метод не возвращает значений, но заполняет словарь productions и устанавливает переменную start\_symbol в объекте класса.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{\_parse\_productions}.}, label={lst:parse_productions}]
|
||||||
|
def _parse_productions(self, text: str):
|
||||||
|
for line in text.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
non_terminal, rule = line.split("->")
|
||||||
|
if self.start_symbol == "":
|
||||||
|
self.start_symbol = non_terminal.strip()
|
||||||
|
|
||||||
|
non_terminal = non_terminal.strip()
|
||||||
|
rules = [
|
||||||
|
[
|
||||||
|
symbol.strip('"')
|
||||||
|
for symbol in re.findall(r"\".*?\"|\S+", rule.strip())
|
||||||
|
if symbol.strip('"') != self.EPSILON
|
||||||
|
]
|
||||||
|
for rule in rule.split("|")
|
||||||
|
]
|
||||||
|
|
||||||
|
if non_terminal not in self.productions:
|
||||||
|
self.productions[non_terminal] = []
|
||||||
|
|
||||||
|
self.productions[non_terminal].extend(rules)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{\_calculate\_first\_sets}}
|
||||||
|
|
||||||
|
Метод \texttt{\_calculate\_first\_sets} вычисляет множества FIRST для всех символов грамматики. Код метода представлен в листинге~\ref{lst:calculate_first_sets}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar.
|
||||||
|
|
||||||
|
Метод не возвращает значений, но заполняет словарь first\_sets в объекте класса.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{\_calculate\_first\_sets}.}, label={lst:calculate_first_sets}]
|
||||||
|
def _calculate_first_sets(self):
|
||||||
|
# Инициализация FIRST для всех символов
|
||||||
|
for non_terminal in self.productions:
|
||||||
|
self.first_sets[non_terminal] = set()
|
||||||
|
|
||||||
|
# Для терминалов FIRST содержит только сам терминал
|
||||||
|
for terminal in self.terminals:
|
||||||
|
self.first_sets[terminal] = {terminal}
|
||||||
|
|
||||||
|
# Вычисление FIRST для нетерминалов
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
changed = False
|
||||||
|
for non_terminal, rules in self.productions.items():
|
||||||
|
for rule in rules:
|
||||||
|
if not rule: # Пустое правило (эпсилон)
|
||||||
|
if "" not in self.first_sets[non_terminal]:
|
||||||
|
self.first_sets[non_terminal].add("")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
all_can_derive_epsilon = True
|
||||||
|
for i, symbol in enumerate(rule):
|
||||||
|
# Добавляем все символы из FIRST(symbol) кроме эпсилон
|
||||||
|
first_without_epsilon = self.first_sets[symbol] - {""}
|
||||||
|
old_size = len(self.first_sets[non_terminal])
|
||||||
|
self.first_sets[non_terminal].update(first_without_epsilon)
|
||||||
|
if len(self.first_sets[non_terminal]) > old_size:
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Если symbol не может порождать эпсилон, прерываем
|
||||||
|
if "" not in self.first_sets[symbol]:
|
||||||
|
all_can_derive_epsilon = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# Если все символы в правиле могут порождать эпсилон,
|
||||||
|
# то и нетерминал может порождать эпсилон
|
||||||
|
if (
|
||||||
|
all_can_derive_epsilon
|
||||||
|
and "" not in self.first_sets[non_terminal]
|
||||||
|
):
|
||||||
|
self.first_sets[non_terminal].add("")
|
||||||
|
changed = True
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{\_calculate\_follow\_sets}}
|
||||||
|
|
||||||
|
Метод \texttt{\_calculate\_follow\_sets} вычисляет множества FOLLOW для нетерминальных символов грамматики. Код метода представлен в листинге~\ref{lst:calculate_follow_sets}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar.
|
||||||
|
|
||||||
|
Метод не возвращает значений, но заполняет словарь follow\_sets в объекте класса.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{\_calculate\_follow\_sets}.}, label={lst:calculate_follow_sets}]
|
||||||
|
def _calculate_follow_sets(self):
|
||||||
|
# инициализировать Fo(S) = { $ }, а все остальные Fo(Ai) пустыми множествами
|
||||||
|
for non_terminal in self.productions:
|
||||||
|
self.follow_sets[non_terminal] = set()
|
||||||
|
|
||||||
|
# Добавляем символ конца строки $ для начального символа
|
||||||
|
self.follow_sets[self.start_symbol].add("$")
|
||||||
|
|
||||||
|
# Повторяем, пока в наборах Follow происходят изменения
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
# Для каждого нетерминала Aj в грамматике
|
||||||
|
for non_terminal_j, rules in self.productions.items():
|
||||||
|
|
||||||
|
# Для каждого правила Aj → w
|
||||||
|
for rule in rules:
|
||||||
|
|
||||||
|
# Для каждого символа в правиле
|
||||||
|
for i, symbol in enumerate(rule):
|
||||||
|
|
||||||
|
# Если символ - нетерминал Ai
|
||||||
|
if symbol in self.productions:
|
||||||
|
# w' - остаток правила после Ai
|
||||||
|
remainder = rule[i + 1 :] if i + 1 < len(rule) else []
|
||||||
|
|
||||||
|
# Если есть терминалы после Ai (w' не пусто)
|
||||||
|
if remainder:
|
||||||
|
# Вычисляем First(w')
|
||||||
|
first_of_remainder = self._first_of_sequence(remainder)
|
||||||
|
|
||||||
|
# Если терминал a находится в First(w'), то добавляем a к Follow(Ai)
|
||||||
|
for terminal in first_of_remainder - {""}:
|
||||||
|
if terminal not in self.follow_sets[symbol]:
|
||||||
|
self.follow_sets[symbol].add(terminal)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Если epsilon находится в First(w'), то добавляем Follow(Aj) к Follow(Ai)
|
||||||
|
if "" in first_of_remainder:
|
||||||
|
old_size = len(self.follow_sets[symbol])
|
||||||
|
self.follow_sets[symbol].update(
|
||||||
|
self.follow_sets[non_terminal_j]
|
||||||
|
)
|
||||||
|
if len(self.follow_sets[symbol]) > old_size:
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Если w' пусто (Ai в конце правила), то добавляем Follow(Aj) к Follow(Ai)
|
||||||
|
else:
|
||||||
|
old_size = len(self.follow_sets[symbol])
|
||||||
|
self.follow_sets[symbol].update(
|
||||||
|
self.follow_sets[non_terminal_j]
|
||||||
|
)
|
||||||
|
if len(self.follow_sets[symbol]) > old_size:
|
||||||
|
changed = True
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{\_first\_of\_sequence}}
|
||||||
|
|
||||||
|
Метод \texttt{\_first\_of\_sequence} вычисляет множество FIRST для последовательности символов. Код метода представлен в листинге~\ref{lst:first_of_sequence}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar и список символов sequence.
|
||||||
|
|
||||||
|
Метод возвращает множество (set) строк, представляющих FIRST для заданной последовательности.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{\_first\_of\_sequence}.}, label={lst:first_of_sequence}]
|
||||||
|
def _first_of_sequence(self, sequence):
|
||||||
|
"""Вычисляет множество FIRST для последовательности символов"""
|
||||||
|
if not sequence:
|
||||||
|
return {""}
|
||||||
|
|
||||||
|
result = set()
|
||||||
|
all_can_derive_epsilon = True
|
||||||
|
|
||||||
|
for symbol in sequence:
|
||||||
|
# Добавляем First(symbol) без эпсилон к результату
|
||||||
|
result.update(self.first_sets[symbol] - {""})
|
||||||
|
|
||||||
|
# Если symbol не может порождать эпсилон, останавливаемся
|
||||||
|
if "" not in self.first_sets[symbol]:
|
||||||
|
all_can_derive_epsilon = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# Если все символы могут порождать эпсилон, добавляем эпсилон к результату
|
||||||
|
if all_can_derive_epsilon:
|
||||||
|
result.add("")
|
||||||
|
|
||||||
|
return result
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{\_fill\_lookup\_table}}
|
||||||
|
|
||||||
|
Метод \texttt{\_fill\_lookup\_table} заполняет таблицу синтаксического анализа для LL(1)-грамматики. Код метода представлен в листинге~\ref{lst:fill_lookup_table}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar.
|
||||||
|
|
||||||
|
Метод не возвращает значений, но заполняет словарь lookup\_table в объекте класса или вызывает исключение, если грамматика не является LL(1).
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{\_fill\_lookup\_table}.}, label={lst:fill_lookup_table}]
|
||||||
|
def _fill_lookup_table(self):
|
||||||
|
# Для каждого нетерминала A
|
||||||
|
for non_terminal, rules in self.productions.items():
|
||||||
|
# Формируем таблицу синтаксического анализа
|
||||||
|
self.lookup_table[non_terminal] = {}
|
||||||
|
|
||||||
|
# Для каждого правила A → w
|
||||||
|
for rule in rules:
|
||||||
|
# Вычисляем First(w)
|
||||||
|
first_of_rule = self._first_of_sequence(rule)
|
||||||
|
|
||||||
|
# Для каждого терминала a в First(w)
|
||||||
|
for terminal in first_of_rule - {""}:
|
||||||
|
# Если T[A,a] уже содержит правило, грамматика не LL(1)
|
||||||
|
if terminal in self.lookup_table[non_terminal]:
|
||||||
|
raise ValueError(
|
||||||
|
"Грамматика не является LL(1)-грамматикой.\n"
|
||||||
|
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||||
|
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||||
|
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем правило в таблицу
|
||||||
|
self.lookup_table[non_terminal][terminal] = rule
|
||||||
|
|
||||||
|
# Если эпсилон в First(w), то для каждого b в Follow(A)
|
||||||
|
if "" in first_of_rule:
|
||||||
|
for terminal in self.follow_sets[non_terminal]:
|
||||||
|
# Если T[A,b] уже содержит правило, грамматика не LL(1)
|
||||||
|
if terminal in self.lookup_table[non_terminal]:
|
||||||
|
raise ValueError(
|
||||||
|
"Грамматика не является LL(1)-грамматикой.\n"
|
||||||
|
f"Конфликт в ячейке [{non_terminal}, {terminal}]\n"
|
||||||
|
f"Новое правило: {non_terminal} -> {' '.join(rule)};\n"
|
||||||
|
f"Существующее правило: {non_terminal} -> {self.lookup_table[non_terminal][terminal]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем правило в таблицу
|
||||||
|
self.lookup_table[non_terminal][terminal] = rule
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{format\_rules}}
|
||||||
|
|
||||||
|
Метод \texttt{format\_rules} форматирует все правила грамматики для удобного представления. Код метода представлен в листинге~\ref{lst:format_rules}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar.
|
||||||
|
|
||||||
|
Метод возвращает строку (str), содержащую все правила грамматики с их номерами.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{format\_rules}.}, label={lst:format_rules}]
|
||||||
|
def format_rules(self) -> str:
|
||||||
|
result = []
|
||||||
|
sorted_rules = sorted(self.rule_numbers.items(), key=lambda x: x[1])
|
||||||
|
|
||||||
|
for rule, number in sorted_rules:
|
||||||
|
non_terminal, symbols = rule
|
||||||
|
rule_text = f"{number}: {non_terminal} -> {' '.join(symbols)}"
|
||||||
|
result.append(rule_text)
|
||||||
|
|
||||||
|
return "\n".join(result)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{format\_lookup\_table}}
|
||||||
|
|
||||||
|
Метод \texttt{format\_lookup\_table} форматирует таблицу синтаксического анализа для удобного представления. Код метода представлен в листинге~\ref{lst:format_lookup_table}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar.
|
||||||
|
|
||||||
|
Метод возвращает строку (str), содержащую таблицу синтаксического анализа в виде таблицы с использованием библиотеки PrettyTable.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{format\_lookup\_table}.}, label={lst:format_lookup_table}]
|
||||||
|
def format_lookup_table(self) -> str:
|
||||||
|
table = PrettyTable()
|
||||||
|
terminals = list(self.terminals)
|
||||||
|
table.field_names = [""] + terminals + ["$"]
|
||||||
|
|
||||||
|
for non_terminal in self.productions:
|
||||||
|
row = [non_terminal]
|
||||||
|
for terminal in terminals + ["$"]:
|
||||||
|
if terminal in self.lookup_table[non_terminal]:
|
||||||
|
rule = self.lookup_table[non_terminal][terminal]
|
||||||
|
rule_num = self.rule_numbers.get((non_terminal, tuple(rule)), "")
|
||||||
|
row.append(f"{rule_num}: {' '.join(rule)}")
|
||||||
|
else:
|
||||||
|
row.append(" - ")
|
||||||
|
table.add_row(row)
|
||||||
|
return str(table)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{format\_first\_sets}}
|
||||||
|
|
||||||
|
Метод \texttt{format\_first\_sets} форматирует множества FIRST для удобного представления. Код метода представлен в листинге~\ref{lst:format_first_sets}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar.
|
||||||
|
|
||||||
|
Метод возвращает строку (str), содержащую множества FIRST для всех символов грамматики.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{format\_first\_sets}.}, label={lst:format_first_sets}]
|
||||||
|
def format_first_sets(self) -> str:
|
||||||
|
"""Форматирует множества FIRST в читаемый вид."""
|
||||||
|
result = []
|
||||||
|
result.append("Множества FIRST:")
|
||||||
|
result.append("=" * 40)
|
||||||
|
|
||||||
|
# Сортируем для гарантии порядка вывода
|
||||||
|
for symbol in sorted(self.first_sets.keys()):
|
||||||
|
# Заменяем пустую строку на эпсилон для лучшей читаемости
|
||||||
|
first_set = {
|
||||||
|
self.EPSILON if item == "" else item for item in self.first_sets[symbol]
|
||||||
|
}
|
||||||
|
result.append(f"FIRST({symbol}) = {{{', '.join(sorted(first_set))}}}")
|
||||||
|
|
||||||
|
return "\n".join(result)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{format\_follow\_sets}}
|
||||||
|
|
||||||
|
Метод \texttt{format\_follow\_sets} форматирует множества FOLLOW для удобного представления. Код метода представлен в листинге~\ref{lst:format_follow_sets}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar.
|
||||||
|
|
||||||
|
Метод возвращает строку (str), содержащую множества FOLLOW для всех нетерминалов грамматики.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{format\_follow\_sets}.}, label={lst:format_follow_sets}]
|
||||||
|
def format_follow_sets(self) -> str:
|
||||||
|
"""Форматирует множества FOLLOW в читаемый вид."""
|
||||||
|
result = []
|
||||||
|
result.append("Множества FOLLOW:")
|
||||||
|
result.append("=" * 40)
|
||||||
|
|
||||||
|
# Обрабатываем только нетерминалы
|
||||||
|
for non_terminal in sorted(self.productions.keys()):
|
||||||
|
follow_set = self.follow_sets.get(non_terminal, set())
|
||||||
|
result.append(
|
||||||
|
f"FOLLOW({non_terminal}) = {{{', '.join(sorted(follow_set))}}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(result)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{analyze}}
|
||||||
|
|
||||||
|
Метод \texttt{analyze} выполняет синтаксический анализ входной последовательности токенов с использованием LL(1)-алгоритма. Код метода представлен в листинге~\ref{lst:analyze}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar и список input\_tokens типа list[str], представляющий последовательность входных токенов.
|
||||||
|
|
||||||
|
Метод возвращает список (list) целых чисел, содержащий номера применённых правил в процессе анализа.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{analyze}.}, label={lst:analyze}]
|
||||||
|
def analyze(self, input_tokens: list[str]) -> list[int]:
|
||||||
|
input_tokens = input_tokens.copy()
|
||||||
|
input_tokens += ["$"]
|
||||||
|
input_pos = 0
|
||||||
|
|
||||||
|
# Инициализируем стек с терминальным символом и начальным символом
|
||||||
|
stack = ["$", self.start_symbol]
|
||||||
|
|
||||||
|
rules_applied = []
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
top = stack[-1]
|
||||||
|
|
||||||
|
current_symbol = (
|
||||||
|
input_tokens[input_pos] if input_pos < len(input_tokens) else "$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Случай 1: Верхний символ стека - нетерминал
|
||||||
|
if top in self.productions:
|
||||||
|
# Ищем правило в таблице синтаксического анализа
|
||||||
|
if current_symbol in self.lookup_table[top]:
|
||||||
|
# Получаем правило
|
||||||
|
production = self.lookup_table[top][current_symbol]
|
||||||
|
|
||||||
|
# Удаляем нетерминал из стека
|
||||||
|
stack.pop()
|
||||||
|
|
||||||
|
# Добавляем правило в rules_applied
|
||||||
|
rule_number = self.rule_numbers[(top, tuple(production))]
|
||||||
|
rules_applied.append(rule_number)
|
||||||
|
|
||||||
|
# Добавляем правило в стек в обратном порядке
|
||||||
|
for symbol in reversed(production):
|
||||||
|
stack.append(symbol)
|
||||||
|
else:
|
||||||
|
expected_symbols = list(self.lookup_table[top].keys())
|
||||||
|
raise ValueError(
|
||||||
|
f"Syntax error: expected one of {expected_symbols}, got '{current_symbol}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Случай 2: Верхний символ стека - терминал
|
||||||
|
elif top != "$":
|
||||||
|
if top == current_symbol:
|
||||||
|
# Удаляем терминал из стека
|
||||||
|
stack.pop()
|
||||||
|
# Переходим к следующему символу ввода
|
||||||
|
input_pos += 1
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Syntax error: expected '{top}', got '{current_symbol}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Случай 3: Верхний символ стека - $
|
||||||
|
else: # top == "$"
|
||||||
|
if current_symbol == "$":
|
||||||
|
# Успешный синтаксический анализ
|
||||||
|
stack.pop() # Удаляем $ из стека
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Syntax error: unexpected symbols at end of input: '{current_symbol}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return rules_applied
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{generate}}
|
||||||
|
|
||||||
|
Метод \texttt{generate} генерирует случайное предложение по заданной грамматике. Код метода представлен в листинге~\ref{lst:generate}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar и необязательный параметр symbol типа str или None, указывающий начальный символ для генерации.
|
||||||
|
|
||||||
|
Метод возвращает кортеж (tuple), содержащий список терминалов и список номеров применённых правил.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{generate}.}, label={lst:generate}]
|
||||||
|
def generate(self, symbol: str | None = None) -> tuple[list[str], list[int]]:
|
||||||
|
"""Генерирует предложение по заданной грамматике. Возвращает список терминалов
|
||||||
|
и список номеров применённых правил."""
|
||||||
|
|
||||||
|
if symbol is None:
|
||||||
|
return self.generate(self.start_symbol)
|
||||||
|
|
||||||
|
# Если символ - терминал, возвращаем его
|
||||||
|
if symbol not in self.productions:
|
||||||
|
return [symbol], []
|
||||||
|
|
||||||
|
# Выбираем случайное правило для нетерминала
|
||||||
|
rules = self.productions[symbol]
|
||||||
|
chosen_rule = random.choice(rules)
|
||||||
|
|
||||||
|
# Получаем номер выбранного правила
|
||||||
|
rule_number = self.rule_numbers[(symbol, tuple(chosen_rule))]
|
||||||
|
|
||||||
|
# Инициализируем результаты
|
||||||
|
terminals = []
|
||||||
|
rule_numbers = [rule_number]
|
||||||
|
|
||||||
|
# Разворачиваем каждый символ в правой части правила
|
||||||
|
for s in chosen_rule:
|
||||||
|
sub_terminals, sub_rules = self.generate(s)
|
||||||
|
terminals.extend(sub_terminals)
|
||||||
|
rule_numbers.extend(sub_rules)
|
||||||
|
|
||||||
|
return terminals, rule_numbers
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{Метод \texttt{generate\_derivation\_steps}}
|
||||||
|
|
||||||
|
Метод \texttt{generate\_derivation\_steps} преобразует список номеров правил в последовательность шагов вывода. Код метода представлен в листинге~\ref{lst:generate_derivation_steps}.
|
||||||
|
|
||||||
|
Метод принимает ссылку на объект класса Grammar и список rule\_numbers типа list[int], содержащий номера правил для применения.
|
||||||
|
|
||||||
|
Метод возвращает список (list) строк, представляющий каждый шаг вывода предложения.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код метода \texttt{generate\_derivation\_steps}.}, label={lst:generate_derivation_steps}]
|
||||||
|
def generate_derivation_steps(self, rule_numbers: list[int]) -> list[str]:
|
||||||
|
"""Преобразует список номеров правил в последовательность шагов вывода.
|
||||||
|
Возвращает список строк, представляющих каждый шаг вывода."""
|
||||||
|
|
||||||
|
# Получаем соответствие между номерами правил и самими правилами
|
||||||
|
rule_details = {num: rule for rule, num in self.rule_numbers.items()}
|
||||||
|
|
||||||
|
# Начинаем с начального символа
|
||||||
|
current = self.start_symbol
|
||||||
|
steps = [current]
|
||||||
|
|
||||||
|
# Применяем каждое правило по порядку
|
||||||
|
for rule_num in rule_numbers:
|
||||||
|
if rule_num in rule_details:
|
||||||
|
non_terminal, replacement = rule_details[rule_num]
|
||||||
|
|
||||||
|
# Находим первое вхождение нетерминала и заменяем его
|
||||||
|
words = current.split()
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
if word == non_terminal:
|
||||||
|
words[i : i + 1] = replacement
|
||||||
|
break
|
||||||
|
|
||||||
|
current = " ".join(words)
|
||||||
|
steps.append(current)
|
||||||
|
|
||||||
|
return steps
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsection{Функция \texttt{main}}
|
||||||
|
|
||||||
|
Функция \texttt{main} реализует интерактивную консольную оболочку для взаимодействия с пользователем. Код функции представлен в листинге~\ref{lst:main}.
|
||||||
|
|
||||||
|
\begin{lstlisting}[caption={Код функции \texttt{main}.}, label={lst:main}]
|
||||||
|
def main():
|
||||||
|
print("Программа для работы с LL(1)-грамматиками")
|
||||||
|
print("=" * 60)
|
||||||
|
print("Варианты команд:")
|
||||||
|
print(" - load <файл> - загрузить грамматику из файла (по умолчанию grammar.txt)")
|
||||||
|
print(" - check <строка> - проверить, соответствует ли строка грамматике")
|
||||||
|
print(" - generate - сгенерировать случайную строку по грамматике")
|
||||||
|
print(" - exit - выход из программы")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Загружаем грамматику по умолчанию при старте
|
||||||
|
grammar = load_grammar()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
command = input("\nВведите команду: ").strip()
|
||||||
|
|
||||||
|
if not command:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = command.split(maxsplit=1)
|
||||||
|
cmd = parts[0].lower()
|
||||||
|
|
||||||
|
if cmd == "exit":
|
||||||
|
print("Выход из программы.")
|
||||||
|
break
|
||||||
|
|
||||||
|
elif cmd == "load":
|
||||||
|
filename = "grammar.txt"
|
||||||
|
if len(parts) > 1:
|
||||||
|
filename = parts[1].strip()
|
||||||
|
grammar = load_grammar(filename)
|
||||||
|
|
||||||
|
elif cmd == "check":
|
||||||
|
input_string = ""
|
||||||
|
if len(parts) > 1:
|
||||||
|
input_string = parts[1].strip()
|
||||||
|
check_string(grammar, input_string)
|
||||||
|
|
||||||
|
elif cmd == "generate":
|
||||||
|
generate_string(grammar)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Неизвестная команда: {cmd}")
|
||||||
|
print("Доступные команды: load, check, generate, exit")
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
\section{Результаты работы программы}
|
||||||
|
Результаты работы программы представлены на Рис.~\ref{fig:result1}.
|
||||||
|
|
||||||
|
\begin{figure}[h!]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=1\linewidth]{img/result1.png}
|
||||||
|
\caption{Результаты работы программы.}
|
||||||
|
\label{fig:result1}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
% \newpage
|
||||||
|
|
||||||
|
\begin{figure}[h!]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.5\linewidth]{img/wrong.png}
|
||||||
|
\caption{Реакция программы на некорректный пользовательский ввод.}
|
||||||
|
\label{fig:wrong}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
На Рис.~\ref{fig:wrong} представлена реакция программы на некорректный пользовательский ввод.
|
||||||
|
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
\section*{Заключение}
|
||||||
|
\addcontentsline{toc}{section}{Заключение}
|
||||||
|
В ходе выполнения лабораторной работы была построена контекстно-свободная грамматика для подмножества немецкого языка, описывающая простое прошедшее время Претерит. На основе разработанной грамматики была реализована программа, которая проверяет принадлежность входной строки заданному языку и генерирует случайные корректные предложения. Для анализа предложений использовался алгоритм LL(1)-разбора, основанный на построении множеств FIRST и FOLLOW для всех нетерминалов грамматики и создании таблицы синтаксического анализа.
|
||||||
|
|
||||||
|
Из достоинств выполнения лабораторной работы можно выделить возможность задания грамматики в отдельном текстовом файле, что позволяет легко изменять и расширять её без модификации программного кода. Использование объектно-ориентированного подхода в реализации обеспечило хорошую структуру кода. Вся логика работы с грамматикой реализована в классе Grammar, а обработка пользовательского ввода вынесена в функцию main.
|
||||||
|
|
||||||
|
К недостаткам текущей реализации можно отнести ограниченность словарного запаса, что сужает разнообразие генерируемых предложений. Также алгоритм генерации не контролирует длину предложений, что может приводить к избыточно длинным или коротким конструкциям. В текущей версии система не учитывает некоторые грамматические особенности немецкого языка, например, склонение прилагательных и согласование артиклей с родом существительных.
|
||||||
|
|
||||||
|
Функционал программы несложно масштабировать. Грамматику легко расширять, добавляя новые слова и правила в текстовый файл без необходимости изменения программного кода. Класс Grammar может служить хорошей основой для создания полноценного LL(k) анализатора.
|
||||||
|
|
||||||
|
На выполнение лабораторной работы ушло около 12 часов. Работа была выполнена в среде разработки Visual Studio Code. Программа написана на Python версии 3.13.
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
\section*{Список литературы}
|
||||||
|
\addcontentsline{toc}{section}{Список литературы}
|
||||||
|
|
||||||
|
\vspace{-1.5cm}
|
||||||
|
\begin{thebibliography}{0}
|
||||||
|
\bibitem{vostrov}
|
||||||
|
Востров, А.В. Курс лекций по дисциплине <<Математическая логика>>. URL \url{https://tema.spbstu.ru/compiler/} (дата обращения 01.04.2025 г.)
|
||||||
|
\bibitem{lutz}
|
||||||
|
Лутц, М. Изучаем Python. 5-е изд. / М. Лутц. — СПб.: Питер, 2019. — 1216 с.
|
||||||
|
\end{thebibliography}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
Reference in New Issue
Block a user