Скопировал прогу из lab3
This commit is contained in:
3
lab5/programm/.gitignore
vendored
Normal file
3
lab5/programm/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
grammar_*
|
||||
analysis_*
|
||||
generation_*
|
||||
401
lab5/programm/grammar.py
Normal file
401
lab5/programm/grammar.py
Normal file
@@ -0,0 +1,401 @@
|
||||
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 - {""}:
|
||||
self._add_to_lookup_table(non_terminal, terminal, rule)
|
||||
|
||||
# Если эпсилон в First(w), то для каждого b в Follow(A)
|
||||
if "" in first_of_rule:
|
||||
for terminal in self.follow_sets[non_terminal]:
|
||||
self._add_to_lookup_table(non_terminal, terminal, rule)
|
||||
|
||||
def _add_to_lookup_table(self, non_terminal, terminal, rule):
|
||||
if terminal in self.lookup_table[non_terminal]:
|
||||
raise ValueError(
|
||||
"\nГрамматика не является LL(1)-грамматикой.\n"
|
||||
f'Распознаваемый нетерминал: "{non_terminal}"\n'
|
||||
f'Поступающий на вход терминал: "{terminal}"\n'
|
||||
f"Неоднозначность между правилами:\n"
|
||||
f"{non_terminal} -> {' '.join(rule)}\n"
|
||||
f"{non_terminal} -> {' '.join(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
|
||||
4
lab5/programm/grammar.txt
Normal file
4
lab5/programm/grammar.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
S -> B A b
|
||||
A -> a A B C | b B
|
||||
B -> b
|
||||
C -> c A
|
||||
174
lab5/programm/main.py
Normal file
174
lab5/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()
|
||||
2
lab5/programm/requirements.txt
Normal file
2
lab5/programm/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
prettytable==3.16.0
|
||||
wcwidth==0.2.13
|
||||
Reference in New Issue
Block a user