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