319 lines
14 KiB
Python
319 lines
14 KiB
Python
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 analyze(self, string: str) -> list[int]:
|
||
input_tokens = string.split() + ["$"]
|
||
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
|