diff --git a/.github/workflows/a.yaml b/.github/workflows/a.yaml new file mode 100644 index 0000000..d67ab45 --- /dev/null +++ b/.github/workflows/a.yaml @@ -0,0 +1,29 @@ +name: Parser edsl + +on: + push: + branches: [ main, geogreck-cw-wip ] + pull_request: + branches: [ main, geogreck-cw-wip ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Install dependencies + run: | + pip install pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run tests + run: | + pytest diff --git a/.gitignore b/.gitignore index b6e4761..3cf7303 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,7 @@ dmypy.json # Pyre type checker .pyre/ + +#LaTEX misc +_minted-report +junk/ \ No newline at end of file diff --git a/parser_edsl.py b/parser_edsl.py index c92d344..394103c 100644 --- a/parser_edsl.py +++ b/parser_edsl.py @@ -14,9 +14,40 @@ Fragment Parser Error +Left +Right +NonAssoc +TokenAttributeError '''.split() +@dataclasses.dataclass(frozen=True) +class Precedence: + level: int + associativity: str + + def __repr__(self): + return f"Precedence({self.associativity!r}, {self.level!r})" + + +@dataclasses.dataclass(frozen=True) +class Left(Precedence): + level: int + associativity: str = 'left' + + +@dataclasses.dataclass(frozen=True) +class Right(Precedence): + level: int + associativity: str = 'right' + + +@dataclasses.dataclass(frozen=True) +class NonAssoc(Precedence): + level: int + associativity: str = 'nonassoc' + + class Symbol: pass @@ -25,6 +56,13 @@ class BaseTerminal(Symbol): pass +def pos_from_offset(text, offset): + line = text.count('\n', 0, offset) + 1 + last_newline = text.rfind('\n', 0, offset) + col = offset - last_newline if last_newline != -1 else offset + 1 + return Position(offset, line, col) + + class Terminal(BaseTerminal): def __init__(self, name, regex, func, *, priority=5, re_flags=re.MULTILINE): self.name = name @@ -43,7 +81,10 @@ def match(self, string, pos): m = self.re.match(string, pos) if m != None: begin, end = m.span() - attrib = self.func(string[begin:end]) + try: + attrib = self.func(string[begin:end]) + except TokenAttributeError as exc: + raise LexerError(pos_from_offset(string, begin), string, message=exc.message) from exc return end - begin, attrib else: return 0, None @@ -98,7 +139,7 @@ def match(string, pos): @dataclasses.dataclass(frozen = True) -class ExAction(): +class ExAction: callee : object @staticmethod @@ -114,6 +155,7 @@ def __init__(self, name): self.name = name self.productions = [] self.lambdas = [] + self.precedence_info = [] def __repr__(self): return 'NonTerminal(' + repr(self.name) + ')' @@ -148,18 +190,33 @@ def __wrap_literals(symbol): def __ior__(self, other): is_callable = lambda obj: hasattr(obj, '__call__') is_fold = lambda obj: is_callable(obj) or isinstance(obj, ExAction) + is_precedence = lambda obj: isinstance(obj, Precedence) if other == (): self |= lambda: None - elif isinstance(other, tuple) and isinstance(other[-1], ExAction): + elif isinstance(other, tuple) and len(other) >= 2 and is_precedence(other[-2]): + # if precedence rule + *symbols, prec, fold = other + symbols = [self.__wrap_literals(sym) for sym in symbols] + symbols[1].priority = prec.level + if callable(fold): + fold = ExAction.wrap_simple_action(fold) + + self.productions.append(symbols) + self.lambdas.append(fold) + self.precedence_info.append(prec) + elif isinstance(other, tuple) and (isinstance(other[-1], ExAction) + or (callable(other[-1]) and not isinstance(other[-1], Precedence))): *symbols, fold = other symbols = [self.__wrap_literals(sym) for sym in symbols] + if callable(fold): + fold = ExAction.wrap_simple_action(fold) + self.productions.append(symbols) self.lambdas.append(fold) - elif isinstance(other, tuple) and is_callable(other[-1]): - self |= other[:-1] + (ExAction.wrap_simple_action(other[-1]),) + self.precedence_info.append(None) elif isinstance(other, tuple): - self |= other + (self.__default_fold,) + self |= other + (self.__default_fold,) elif isinstance(other, Symbol) or is_fold(other): self |= (other,) elif isinstance(other, str): @@ -178,7 +235,9 @@ def __default_fold(*args): else: raise RuntimeError('__default_fold', args) - def enum_rules(self): + def enum_rules(self, include_precedence = True): + if include_precedence: + return zip(self.productions, self.lambdas, self.precedence_info) return zip(self.productions, self.lambdas) @@ -280,7 +339,7 @@ def __setup_from_grammar(self, gr): for state_id in range(self.n_states): for item, next_symbol in self.__ccol[state_id]: prod_index, dot = item - pname, pbody, plambda = gr.productions[prod_index] + _, pbody, _, _ = gr.productions[prod_index] if dot < len(pbody): terminal = pbody[dot] @@ -312,7 +371,7 @@ def __stringify_goto_entry(nt, sid): def __stringify_lr_zero_item(self, item): prod_index, dot = item - pname, pbody, plambda = self.grammar.productions[prod_index] + pname, pbody, _, _ = self.grammar.productions[prod_index] dotted_pbody = pbody[:dot] + ['.'] + pbody[dot:] dotted_pbody_str = ' '.join(str(x) for x in dotted_pbody) return RULE_INDEXING_PATTERN % (prod_index, pname.name + ': ' + dotted_pbody_str) @@ -379,7 +438,7 @@ def get_canonical_collection(gr): # For each item in closure_set whose . (dot) points to a symbol equal to 'sym' # i.e. a production expecting to see 'sym' next for ((prod_index, dot), next_symbol) in closure_set: - pname, pbody, plambda = gr.productions[prod_index] + _, pbody, _, _ = gr.productions[prod_index] if dot == len(pbody) or pbody[dot] != sym: continue @@ -420,7 +479,7 @@ def closure(gr, item_set): while len(current) > 0: new_elements = [] for ((prod_index, dot), lookahead) in current: - pname, pbody, plambda = gr.productions[prod_index] + _, pbody, _, _ = gr.productions[prod_index] if dot == len(pbody) or pbody[dot] not in gr.nonterms: continue nt = pbody[dot] @@ -443,23 +502,135 @@ def message(self): pass +class TokenAttributeError(Error): + def __init__(self, text): + self.bad = text + + def __repr__(self): + return f'TokenAttributeError({self.pos!r}, {self.bad!r})' + + @property + def message(self): + return f'{self.bad}' + + @dataclasses.dataclass class ParseError(Error): pos : Position unexpected : Symbol expected : list + _text: str = "" @property def message(self): + if self._text != "": + return self._text expected = ', '.join(map(str, self.expected)) - return f'Неожиданный символ {self.unexpected}, ' \ - + f'ожидалось {expected}' + return (f'Неожиданный символ {self.unexpected}, ' + f'ожидалось {expected}') + + +class PredictiveTableConflictError(Error): + def __init__(self, nonterm, terminal, existing_rule, new_rule): + self.nonterm = nonterm + self.terminal = terminal + self.existing_rule = existing_rule + self.new_rule = new_rule + + @property + def message(self): + return (f'LL(1) conflict: для нетерминала {self.nonterm} и терминала {self.terminal}\n' + f'уже есть правило {self.existing_rule}, попытка добавить {self.new_rule}') + + +@dataclasses.dataclass +class ParseTreeNode: + symbol: Symbol + fold: ExAction = None + token: Token = None + children: list = dataclasses.field(default_factory=list) + attribute: object = None + + +class PredictiveParsingTable: + def __init__(self, grammar): + self.grammar = grammar + self.table = {} + + self.follow_sets = self._build_follow_sets() + self._build_table() + + def _build_follow_sets(self): + follow = {nt: set() for nt in self.grammar.nonterms} + start_nt = self.grammar.nonterms[0] + + follow[start_nt].add(EOF_SYMBOL) + + changed = True + while changed: + changed = False + for (nt, prod, _, _) in self.grammar.productions: + for i, sym in enumerate(prod): + if sym not in self.grammar.nonterms: + continue + beta = prod[i+1:] + first_of_beta = self.grammar.first_set(beta) + before_len = len(follow[sym]) + + without_epsilon = set(first_of_beta) - {None} + follow[sym].update(without_epsilon) + + if None in first_of_beta: + follow[sym].update(follow[nt]) + + after_len = len(follow[sym]) + if after_len > before_len: + changed = True + return follow + + def _build_table(self): + for nt in self.grammar.nonterms: + self.table[nt] = {} + + for i, (nt, prod, fold, _) in enumerate(self.grammar.productions): + fs = self.grammar.first_set(prod) + non_epsilon = fs - {None} + for t in non_epsilon: + self._add_rule(nt, t, prod, fold) + + if None in fs: + for t in self.follow_sets[nt]: + self._add_rule(nt, t, prod, fold) + + def _add_rule(self, nt, terminal, prod, fold): + if terminal not in self.table[nt]: + self.table[nt][terminal] = (prod, fold) + else: + existing = self.table[nt][terminal] + raise PredictiveTableConflictError( + nonterm=nt, + terminal=terminal, + existing_rule=existing, + new_rule=(prod, fold) + ) + + def stringify(self): + lines = [] + for nt in self.grammar.nonterms: + row = self.table[nt] + lines.append(f'{nt}:') + for term, (alpha, fold) in row.items(): + alpha_str = ' '.join(str(s) for s in alpha) if alpha else 'ε' + lines.append(f' {term} -> {alpha_str}') + return '\n'.join(lines) class Parser(object): def __init__(self, start_nonterminal): + fake_axiom = NonTerminal(START_SYMBOL) fake_axiom |= start_nonterminal + self.start = start_nonterminal self.nonterms = [] self.terminals = set() @@ -488,10 +659,10 @@ def register(symbol): nt = self.nonterms[nt_idx] self.nonterm_offset[nt] = len(self.productions) - for prod, func in nt.enum_rules(): + for prod, func, prec in nt.enum_rules(): for symbol in prod: register(symbol) - self.productions.append((nt, prod, func)) + self.productions.append((nt, prod, func, prec)) scanned_count = last_unscanned @@ -503,6 +674,9 @@ def register(symbol): self.__build_first_sets() self.table = ParsingTable(self) + self.ll1_table = None + self.ll1_is_ok = True + def first_set(self, x): result = set() skippable_symbols = 0 @@ -527,7 +701,7 @@ def __build_first_sets(self): while repeat: repeat = False - for nt, prod, func in self.productions: + for nt, prod, _, _ in self.productions: curfs = self.__first_sets[nt] curfs_len = len(curfs) curfs.update(self.first_set(prod)) @@ -553,16 +727,60 @@ def add_skipped_domain(self, regex): def parse(self, text): lexer = Lexer(self.terminals, text, self.skipped_domains) stack = [(0, Fragment(Position(), Position()), None)] - cur = lexer.next_token() + try: + cur = lexer.next_token() + except LexerError as lex_err: + raise ParseError(pos=lex_err.pos, unexpected=lex_err, expected=[], _text=lex_err.message) from lex_err + while True: cur_state, cur_coord, top_attr = stack[-1] - action = next(iter(self.table.action[cur_state][cur.type]), None) + actions = self.table.action[cur_state][cur.type] + if len(actions) < 2: + action = next(iter(actions), None) + elif len(actions) == 2: + # Shift/reduce conflict: resolve via precedence + shift_action = None + reduce_action = None + for act in actions: + if isinstance(act, Shift): + shift_action = act + elif isinstance(act, Reduce): + reduce_action = act + if shift_action is not None and reduce_action is not None: + _, _, fold, prod_prec = self.productions[reduce_action.rule] + if prod_prec is None: + prod = self.productions[reduce_action.rule][1] + for sym in reversed(prod): + if isinstance(sym, BaseTerminal): + prod_prec = Precedence(sym.priority, 'left') + break + token_prec = cur.type.priority + if prod_prec is None: + action = shift_action + elif token_prec > prod_prec.level: + action = shift_action + elif token_prec < prod_prec.level: + action = reduce_action + else: + if prod_prec.associativity == 'left': + action = reduce_action + elif prod_prec.associativity == 'right': + action = shift_action + else: + raise ParseError(pos=cur.pos.start, unexpected=cur, expected=[cur.type], _text="Неассоциативная операция") + else: + action = shift_action or reduce_action + match action: case Shift(state): stack.append((state, cur.pos, cur.attr)) - cur = lexer.next_token() + try: + cur = lexer.next_token() + except LexerError as lex_err: + raise ParseError(pos=lex_err.pos, unexpected=lex_err, expected=[], _text=lex_err.message) from lex_err + case Reduce(rule): - nt, prod, fold = self.productions[rule] + nt, prod, fold, _ = self.productions[rule] n = len(prod) attrs = [attr for state, coord, attr in stack[len(stack)-n:] if attr != None] @@ -585,6 +803,124 @@ def parse(self, text): raise ParseError(pos=cur.pos.start, unexpected=cur, expected=expected) + def parse_earley(self, text): + tokens = list(self.tokenize(text)) + tokens = [token for token in tokens if token.type != EOF_SYMBOL] + earley_parser = EarleyParser(self) + res = earley_parser.parse(tokens) + + return res + + def parse_ll1(self, text): + if not self.is_ll1(): + raise ValueError("Grammar is not LL(1); cannot parse in LL(1) mode.") + + lexer = Lexer(self.terminals, text, self.skipped_domains) + + start_nt = self.nonterms[0] + root = ParseTreeNode(symbol=start_nt, fold=None) + + stack = [ParseTreeNode(symbol=EOF_SYMBOL), root] + + cur_token = lexer.next_token() + + while True: + top_node = stack[-1] + top_sym = top_node.symbol + + if isinstance(top_sym, BaseTerminal): + if top_sym == cur_token.type: + top_node.token = cur_token + stack.pop() + if top_sym == EOF_SYMBOL: + break + cur_token = lexer.next_token() + else: + expected = [top_sym] + raise ParseError(pos=cur_token.pos.start, + unexpected=cur_token, + expected=expected) + else: + table_row = self.ll1_table.table[top_sym] + entry = table_row.get(cur_token.type, None) + if entry is None: + expected = list(table_row.keys()) + raise ParseError(pos=cur_token.pos.start, + unexpected=cur_token, + expected=expected) + + stack.pop() + prod_symbols, fold = entry + children_nodes = [] + for sym in prod_symbols: + child_node = ParseTreeNode(symbol=sym) + children_nodes.append(child_node) + top_node.children = children_nodes + top_node.fold = fold + + for child in reversed(children_nodes): + stack.append(child) + + # print(root) + self._evaluate_parse_tree(root) + return root.attribute + + def build_ll1_table(self): + if self.ll1_table is not None: + return + try: + self.ll1_table = PredictiveParsingTable(self) + except PredictiveTableConflictError: + self.ll1_is_ok = False + raise + + def is_ll1(self): + if self.ll1_table is None and self.ll1_is_ok: + try: + self.build_ll1_table() + except PredictiveTableConflictError: + return False + return self.ll1_is_ok + + def stringify_ll1_table(self): + if not self.is_ll1(): + return "Grammar is NOT LL(1) - conflicts found." + return self.ll1_table.stringify() + + def _evaluate_parse_tree(self, node: ParseTreeNode): + if isinstance(node.symbol, BaseTerminal): + node.attribute = node.token.attr if node.token else None + return node.attribute + + for child in node.children: + self._evaluate_parse_tree(child) + attrs = [child.attribute for child in node.children if child.attribute is not None] + + coords = [child.token.pos for child in node.children if child.token is not None] + if len(coords) > 0: + res_coord = Fragment(coords[0].start, coords[-1].following) + else: + if node.children: + coords_for_start = [c for c in node.children if c.token is not None] + if coords_for_start: + start = coords_for_start[0].start + else: + start = Position() + else: + start = Position() + res_coord = Fragment(start, start) + + if node.fold is not None: + node.attribute = node.fold.callee(attrs, coords, res_coord) + else: + if len(attrs) == 1: + node.attribute = attrs[0] + elif len(attrs) == 0: + node.attribute = None + else: + raise RuntimeError('No fold function for production with multiple children!') + return node.attribute + def tokenize(self, text): lexer = Lexer(self.terminals, text, self.skipped_domains) @@ -605,7 +941,7 @@ def goto(gr, item_set, inp): result_set = set() for (item, lookahead) in item_set: prod_id, dot = item - pname, pbody, plambda = gr.productions[prod_id] + _, pbody, _, _ = gr.productions[prod_id] if dot == len(pbody) or pbody[dot] != inp: continue @@ -698,7 +1034,7 @@ def __closure(gr, item_set): while len(set_queue) > 0: new_elements = [] for itemProdId, dot in set_queue: - pname, pbody, plambda = gr.productions[itemProdId] + _, pbody, _, _ = gr.productions[itemProdId] if dot == len(pbody) or pbody[dot] not in gr.nonterms: continue nt = pbody[dot] @@ -716,7 +1052,7 @@ def __closure(gr, item_set): def __goto(gr, item_set, inp): result_set = set() for prod_index, dot in item_set: - pname, pbody, plambda = gr.productions[prod_index] + _, pbody, _, _ = gr.productions[prod_index] if dot < len(pbody) and pbody[dot] == inp: result_set.add((prod_index, dot + 1)) result_set = LR0_Automaton.__closure(gr, result_set) @@ -734,16 +1070,17 @@ def kstates(self): class LexerError(Error): ERROR_SLICE = 10 - def __init__(self, pos, text): + def __init__(self, pos, text, message=""): self.pos = pos self.bad = text[pos.offset:pos.offset + self.ERROR_SLICE] + self._message = f'Не удалось разобрать {self.bad!r}' if message == "" else message def __repr__(self): return f'LexerError({self.pos!r},{self.bad!r})' @property def message(self): - return f'Не удалось разобрать {self.bad!r}' + return self._message class Lexer: @@ -777,3 +1114,235 @@ def next_token(self): return token return Token(EOF_SYMBOL, Fragment(self.pos, self.pos), None) + +@dataclasses.dataclass(frozen=True) +class EarleyState: + rule: tuple + dot: int + start: int + end: int + attrs: any = dataclasses.field(default_factory=lambda: None, hash=False) + coords: tuple = () + + def __post_init__(self): + if type(self.coords) is not tuple: + object.__setattr__(self, 'coords', tuple(self.coords)) + + def __repr__(self): + lhs, rhs, _ = self.rule + dotted_rhs = ' '.join(str(x) for x in rhs[:self.dot]) + ' • ' + ' '.join(str(x) for x in rhs[self.dot:]) + return f"{lhs} → {dotted_rhs} [{self.start}, {self.end}] {self.is_complete()} attr({self.attrs})" + + def is_complete(self): + _, rhs, _ = self.rule + return self.dot == len(rhs) + + def next_symbol(self): + _, rhs, _ = self.rule + if self.dot < len(rhs): + return rhs[self.dot] + return None + + +class EarleyParser: + def __init__(self, grammar: Parser): + self.grammar = grammar + self.chart = collections.defaultdict(set) + + def predict(self, state, pos, coords, states): + if not isinstance(coords, tuple): + coords = (coords,) + next_sym = state.next_symbol() + if isinstance(next_sym, NonTerminal): + for prod, fold, _ in next_sym.enum_rules(): + if not prod: + attrs = state.attrs + if not attrs: + attrs = [] + res_coord = Fragment(coords[0].start, coords[-1].following) + attrs = attrs+[fold.callee([], coords, res_coord)] + new_state = EarleyState(state.rule, + state.dot + 1, + state.start, + pos, + attrs, + coords) + + if new_state.is_complete(): + _, _, fold1 = new_state.rule + coords = new_state.coords + res_coord = Fragment(coords[0].start, coords[-1].following) + res_attr = fold1.callee(attrs, coords, res_coord) + new_state = dataclasses.replace(new_state, attrs=[res_attr]) + + if new_state not in self.chart[pos] and new_state not in states: + states.append(new_state) + self.chart[pos].add(new_state) + + new_state = EarleyState((next_sym, tuple(prod), fold), + 0, + pos, + pos, + attrs=[], + coords=coords) + if new_state.is_complete(): + _, _, fold = new_state.rule + coords = new_state.coords + res_coord = Fragment(coords[0].start, coords[-1].following) + res_attr = fold.callee([], coords, res_coord) + new_state = dataclasses.replace(new_state, attrs=[res_attr]) + + if new_state not in self.chart[pos] and new_state not in states: + states.append(new_state) + else: + new_state = EarleyState((next_sym, tuple(prod), fold), + 0, + pos, + pos, + attrs=[], + coords=coords) + if new_state not in self.chart[pos] and new_state not in states: + states.append(new_state) + self.predict(new_state, pos, coords, states) + + def scan(self, state, token, pos): + next_sym = state.next_symbol() + if (isinstance(next_sym, LiteralTerminal) or isinstance(next_sym, Terminal)) and next_sym == token.type: + new_attrs = [] + + if token.attr is None: + new_attrs = state.attrs + elif not isinstance(state, list) or len(token.attr) > 1: + new_attrs = state.attrs + [token.attr] + else: + new_attrs = state.attrs + token.attr + + new_coords = state.coords + (token.pos,) + new_state = EarleyState(state.rule, + state.dot + 1, + state.start, + pos + 1, + new_attrs, + new_coords) + if new_state.is_complete(): + _, _, fold = new_state.rule + coords = new_state.coords + res_coord = Fragment(coords[0].start, coords[-1].following) + res_attr = fold.callee(new_attrs, coords, res_coord) + + new_state = EarleyState(state.rule, state.dot + 1, state.start, pos + 1, [res_attr], new_coords) + + self.chart[pos + 1].add(new_state) + + def complete(self, state: EarleyState, pos, states: list[EarleyState]): + _, rhs, _ = state.rule + if not rhs: + return + for prev_state in self.chart[state.start]: + next_sym = prev_state.next_symbol() + if next_sym == state.rule[0]: + state_attrs = state.attrs + new_attrs = [] + + if state.is_complete() and state.start == state.end: + state_attrs = [state_attrs] + + if prev_state.attrs is None: + new_attrs = state_attrs + elif not isinstance(state_attrs, list) or len(state_attrs) > 1 or state.is_complete(): + new_attrs = prev_state.attrs + state_attrs + else: + new_attrs = prev_state.attrs + state_attrs + + new_coords = prev_state.coords + state.coords + new_state = EarleyState( + prev_state.rule, + prev_state.dot + 1, + prev_state.start, + pos, + new_attrs, + new_coords + ) + if new_state not in self.chart[pos]: + states.append(new_state) + if not new_state.is_complete(): + self.predict(new_state, pos, new_coords, states) + if new_state.is_complete(): + _, _, fold = new_state.rule + # if new_state.attrs is not None: + # if isinstance(new_state.attrs, list): + # attrs = [attr for attr in list(new_state.attrs) if attr] + # else: + # attrs = [new_state.attrs] + attrs = new_state.attrs + coords = new_state.coords + res_coord = Fragment(coords[0].start, coords[-1].following) + + res_attr = [] + + res_attr = fold.callee(attrs, coords, res_coord) + states.remove(new_state) + new_state = dataclasses.replace(new_state, attrs=[res_attr], coords=[res_coord]) + states.append(new_state) + + + def parse(self, tokens): + start_rule = (self.grammar.nonterms[0], tuple(self.grammar.productions[0][1]), self.grammar.productions[0][2]) + self.chart[0].add(EarleyState(start_rule, 0, 0, 0)) + + for pos in range(len(tokens)+1): + states = list(self.chart[pos]) + + if len(states) == 0: + expected_set = set() + last_chart = self.chart[pos-1] if pos > 0 else self.chart[0] + for state in last_chart: + next_sym = state.next_symbol() + if next_sym is not None: + if isinstance(next_sym, Terminal): + expected_set.add(next_sym) + elif isinstance(next_sym, NonTerminal): + expected_set.update(self.grammar.first_set([next_sym]) - {None}) + expected_list = list(expected_set) + raise ParseError(tokens[pos-1].pos.start, + unexpected=tokens[pos-1], + expected=expected_list) + + i = 0 + while i < len(states): + state = states[i] + if not state.is_complete(): + next_sym = state.next_symbol() + # print(next_sym) + if isinstance(next_sym, NonTerminal): + # print('a', next_sym) + coords = Fragment(Position(), Position()) + if len(tokens) > 0: + coords = tokens[min(pos, len(tokens)-1)].pos + self.predict(state, pos, coords, states) + elif pos < len(tokens): + self.scan(state, tokens[pos], pos) + else: + self.complete(state, pos, states) + i+=1 + self.chart[pos].update(states) + + final_states = [state + for state in self.chart[len(tokens)] + if (state.rule[0] == self.grammar.start + and state.is_complete() + and state.start == 0)] + + if len(final_states) > 1: + raise ParseError(pos=Position(), + expected="", + unexpected="", + _text=f"Неопределенная грамматика: найдено {len(final_states)} путей разбора") + if final_states: + return final_states[0].attrs[0] + + def print_chart(self): + for pos, states in sorted(self.chart.items()): + print(f"Chart[{pos}]:") + for state in states: + print(f" {state}") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d347ff2 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,5 @@ +import os, sys + +current_dir = os.path.dirname(os.path.abspath(__file__)) +source_dir = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.append(source_dir) diff --git a/tests/bisonification_test.py b/tests/bisonification_test.py new file mode 100644 index 0000000..35affd5 --- /dev/null +++ b/tests/bisonification_test.py @@ -0,0 +1,71 @@ +import pytest +import parser_edsl as pe + +@pytest.mark.parametrize( + "text, attr", [ + ("2+2*2+2", 8), + ("2*2+2*2", 8), + ("10+100*2", 210) + ] +) +def test_parse_priority_1(text, attr): + expr = pe.NonTerminal('expr') + expr |= (expr, '+', expr, pe.Left(1), lambda a, b: a + b) + expr |= (expr, '*', expr, pe.Left(20), lambda a, b: a * b) + expr |= (pe.Terminal('NUM', r'\d+', int), lambda x: x) + + parser = pe.Parser(expr) + + assert parser.parse(text) == attr + +@pytest.mark.parametrize( + "text, attr", [ + ("2+2*2+2", 16), + ("2*2+2*2", 16), + ("10+100*2", 220) + ] +) +def test_parse_priority_2(text, attr): + expr = pe.NonTerminal('expr') + expr |= (expr, '+', expr, pe.Left(20), lambda a, b: a + b) + expr |= (expr, '*', expr, pe.Left(1), lambda a, b: a * b) + expr |= (pe.Terminal('NUM', r'\d+', int), lambda x: x) + + parser = pe.Parser(expr) + + assert parser.parse(text) == attr + + +@pytest.mark.parametrize( + "text, attr", [ + ("100-10-10", 80), + ("2^3^2", 64), + ("100-10^2", 8100) + ] +) +def test_parse_left_assoc(text, attr): + expr = pe.NonTerminal('expr') + expr |= (expr, '-', expr, pe.Left(20), lambda a, b: a - b) + expr |= (expr, '^', expr, pe.Left(1), lambda a, b: a ** b) + expr |= (pe.Terminal('NUM', r'\d+', int), lambda x: x) + + parser = pe.Parser(expr) + + assert parser.parse(text) == attr + +@pytest.mark.parametrize( + "text, attr", [ + ("100-10-10", 100), + ("2^3^2", 512), + ("100-10^2", 8100) + ] +) +def test_parse_right_assoc(text, attr): + expr = pe.NonTerminal('expr') + expr |= (expr, '-', expr, pe.Right(20), lambda a, b: a - b) + expr |= (expr, '^', expr, pe.Right(1), lambda a, b: a ** b) + expr |= (pe.Terminal('NUM', r'\d+', int), lambda x: x) + + parser = pe.Parser(expr) + + assert parser.parse(text) == attr diff --git a/tests/earley_pascal_test.py b/tests/earley_pascal_test.py new file mode 100644 index 0000000..a6d4ab7 --- /dev/null +++ b/tests/earley_pascal_test.py @@ -0,0 +1,180 @@ +import abc +import enum +import pytest +import parser_edsl as pe +import re +import typing + +from dataclasses import dataclass + +good_text = """var + (* переменные *) + x: integer; +begin + { операторы } + x := 100; +end +""" + +bad_text = """ +var + (* переменные *) + x: integer; + y: integer; +begin + { операторы } + x := 100; +end +""" + +class Type(enum.Enum): + Integer = 'INTEGER' + Real = 'REAL' + Boolean = 'BOOLEAN' + + +@dataclass +class VarDef: + name : str + type : Type + + +class Statement(abc.ABC): + pass + + +@dataclass +class Program: + var_defs : list[VarDef] + statements : list[Statement] + + +class Expr(abc.ABC): + pass + + +@dataclass +class AssignStatement(Statement): + variable : str + expr : Expr + + +@dataclass +class VariableExpr(Expr): + varname : str + + +@dataclass +class ConstExpr(Expr): + value : typing.Any + type : Type + + +@dataclass +class BinOpExpr(Expr): + left : Expr + op : str + right : Expr + + +@dataclass +class UnOpExpr(Expr): + op : str + expr : Expr + + +def validate(str): + if str == "a": + raise pe.TokenAttributeError("pos", "dsadsa") + return str.upper() + +INTEGER = pe.Terminal('INTEGER', '[0-9]+', int, priority=7) +REAL = pe.Terminal('REAL', '[0-9]+(\\.[0-9]*)?(e[-+]?[0-9]+)?', float) +VARNAME = pe.Terminal('VARNAME', '[A-Za-z][A-Za-z0-9]*', validate) + +def make_keyword(image): + return pe.Terminal(image, image, lambda name: None, + re_flags=re.IGNORECASE, priority=10) + +KW_VAR, KW_BEGIN, KW_END, KW_INTEGER, KW_REAL, KW_BOOLEAN = \ + map(make_keyword, 'var begin end integer real boolean'.split()) + +KW_IF, KW_THEN, KW_ELSE, KW_WHILE, KW_DO, KW_FOR, KW_TO = \ + map(make_keyword, 'if then else while do for to'.split()) + +KW_OR, KW_DIV, KW_MOD, KW_AND, KW_NOT, KW_TRUE, KW_FALSE = \ + map(make_keyword, 'or div mod and not true false'.split()) + + +NProgram, NVarDefs, NVarDef, NType, NStatements = \ + map(pe.NonTerminal, 'Program VarDefs VarDef Type Statements'.split()) + +NStatement, NExpr, NCmpOp, NArithmExpr, NAddOp = \ + map(pe.NonTerminal, 'Statement Expr CmpOp ArithmOp AddOp'.split()) + +NTerm, NMulOp, NFactor, NPower, NConst = \ + map(pe.NonTerminal, 'Term MulOp Factor Power Const'.split()) + + +NProgram |= KW_VAR, NVarDefs, KW_BEGIN, NStatements, KW_END, Program + +NVarDefs |= lambda: [] +NVarDefs |= NVarDef, lambda vd: [vd] + +NVarDef |= VARNAME, ':', NType, ';', VarDef + +NType |= KW_INTEGER, lambda: Type.Integer +NType |= KW_REAL, lambda: Type.Real +NType |= KW_BOOLEAN, lambda: Type.Boolean + +NStatements |= NStatement, ';', lambda st: [st] + +NStatement |= VARNAME, ':=', NExpr, AssignStatement + +NExpr |= NArithmExpr + +def make_op_lambda(op): + return lambda: op + +for op in ('>', '<', '>=', '<=', '=', '<>'): + NCmpOp |= op, make_op_lambda(op) + +NArithmExpr |= NTerm + +NAddOp |= '+', lambda: '+' +NAddOp |= '-', lambda: '-' +NAddOp |= KW_OR, lambda: 'or' + +NTerm |= NFactor + +NMulOp |= '*', lambda: '*' +NMulOp |= '/', lambda: '/' +NMulOp |= KW_DIV, lambda: 'div' +NMulOp |= KW_MOD, lambda: 'mod' +NMulOp |= KW_AND, lambda: 'and' + +NFactor |= NPower + +NPower |= NConst + +NConst |= INTEGER, lambda v: ConstExpr(v, Type.Integer) +NConst |= REAL, lambda v: ConstExpr(v, Type.Real) +NConst |= KW_TRUE, lambda: ConstExpr(True, Type.Boolean) +NConst |= KW_FALSE, lambda: ConstExpr(False, Type.Boolean) + +def test_good_parse(): + p = pe.Parser(NProgram) + p.add_skipped_domain('\\s') + p.add_skipped_domain('(\\(\\*|\\{).*?(\\*\\)|\\})') + assert p.parse_earley(good_text) == Program(var_defs=[VarDef(name='X', type=Type.Integer)], statements=[AssignStatement(variable='X', expr=ConstExpr(value=100, type=Type.Integer))]) + +def test_bad_parse(): + p = pe.Parser(NProgram) + p.add_skipped_domain('\\s') + p.add_skipped_domain('(\\(\\*|\\{).*?(\\*\\)|\\})') + assert p.is_ll1() + + with pytest.raises(pe.ParseError) as parse_error: + p.parse_earley(bad_text) + + assert parse_error.value.message == "Неожиданный символ VARNAME(Y), ожидалось begin" diff --git a/tests/earley_test.py b/tests/earley_test.py new file mode 100644 index 0000000..83c80a9 --- /dev/null +++ b/tests/earley_test.py @@ -0,0 +1,58 @@ +import pytest +import parser_edsl as pe + +def test_defiened_grammar(): + expr = pe.NonTerminal('expr') + texpr = pe.NonTerminal('texpr') + expr |= (expr, '+', texpr, lambda a, b: a + b) + expr |= (expr, '-', texpr, lambda a, b: a - b) + expr |= (texpr, lambda a: a) + texpr |= (pe.Terminal('NUM', r'\d+', int), lambda x: x) + + parser = pe.Parser(expr) + + assert parser.parse_earley("42+3-5") == 40 + +def test_undefiened_grammar(): + expr = pe.NonTerminal('expr') + expr |= (expr, '+', expr, lambda a, b: a + b) + expr |= (expr, '-', expr, lambda a, b: a - b) + expr |= (pe.Terminal('NUM', r'\d+', int), lambda x: x) + + parser = pe.Parser(expr) + + with pytest.raises(pe.ParseError): + result = parser.parse_earley("42+3-5") + + +def test_epsilon_rule_empty_grammar(): + expr = pe.NonTerminal('expr') + expr |= () + + parser = pe.Parser(expr) + + assert parser.parse_earley("") == None + + +def test_epsilon_rule(): + expr = pe.NonTerminal('expr') + expr |= ('a', expr, lambda _: None) + expr |= (lambda: None) + + parser = pe.Parser(expr) + + assert parser.parse_earley("aaa") == None + + +def test_epsilon_rule_attrs(): + NAr = pe.NonTerminal("NAr") + ARRAY = pe.Terminal("array", "array", lambda _: None, priority=10) + + NAr |= ARRAY, NAr, lambda x: x + 1 + NAr |= lambda: 0 + + p = pe.Parser(NAr) + p.add_skipped_domain("\\s") + + assert p.parse_earley("array array") == 2 + assert p.parse_earley(" ") == 0 diff --git a/tests/ll1_pascal_test.py b/tests/ll1_pascal_test.py new file mode 100644 index 0000000..f95096f --- /dev/null +++ b/tests/ll1_pascal_test.py @@ -0,0 +1,181 @@ +import abc +import enum +import pytest +import parser_edsl as pe +import re +import typing + +from dataclasses import dataclass + +good_text = """var + (* переменные *) + x: integer; +begin + { операторы } + x := 100; +end +""" + +bad_text = """ +var + (* переменные *) + x: integer; + y: integer; +begin + { операторы } + x := 100; +end +""" + +class Type(enum.Enum): + Integer = 'INTEGER' + Real = 'REAL' + Boolean = 'BOOLEAN' + + +@dataclass +class VarDef: + name : str + type : Type + + +class Statement(abc.ABC): + pass + + +@dataclass +class Program: + var_defs : list[VarDef] + statements : list[Statement] + + +class Expr(abc.ABC): + pass + + +@dataclass +class AssignStatement(Statement): + variable : str + expr : Expr + + +@dataclass +class VariableExpr(Expr): + varname : str + + +@dataclass +class ConstExpr(Expr): + value : typing.Any + type : Type + + +@dataclass +class BinOpExpr(Expr): + left : Expr + op : str + right : Expr + + +@dataclass +class UnOpExpr(Expr): + op : str + expr : Expr + + +def validate(str): + if str == "a": + raise pe.TokenAttributeError("pos", "dsadsa") + return str.upper() + +INTEGER = pe.Terminal('INTEGER', '[0-9]+', int, priority=7) +REAL = pe.Terminal('REAL', '[0-9]+(\\.[0-9]*)?(e[-+]?[0-9]+)?', float) +VARNAME = pe.Terminal('VARNAME', '[A-Za-z][A-Za-z0-9]*', validate) + +def make_keyword(image): + return pe.Terminal(image, image, lambda name: None, + re_flags=re.IGNORECASE, priority=10) + +KW_VAR, KW_BEGIN, KW_END, KW_INTEGER, KW_REAL, KW_BOOLEAN = \ + map(make_keyword, 'var begin end integer real boolean'.split()) + +KW_IF, KW_THEN, KW_ELSE, KW_WHILE, KW_DO, KW_FOR, KW_TO = \ + map(make_keyword, 'if then else while do for to'.split()) + +KW_OR, KW_DIV, KW_MOD, KW_AND, KW_NOT, KW_TRUE, KW_FALSE = \ + map(make_keyword, 'or div mod and not true false'.split()) + + +NProgram, NVarDefs, NVarDef, NType, NStatements = \ + map(pe.NonTerminal, 'Program VarDefs VarDef Type Statements'.split()) + +NStatement, NExpr, NCmpOp, NArithmExpr, NAddOp = \ + map(pe.NonTerminal, 'Statement Expr CmpOp ArithmOp AddOp'.split()) + +NTerm, NMulOp, NFactor, NPower, NConst = \ + map(pe.NonTerminal, 'Term MulOp Factor Power Const'.split()) + + +NProgram |= KW_VAR, NVarDefs, KW_BEGIN, NStatements, KW_END, Program + +NVarDefs |= lambda: [] +NVarDefs |= NVarDef, lambda vd: [vd] + +NVarDef |= VARNAME, ':', NType, ';', VarDef + +NType |= KW_INTEGER, lambda: Type.Integer +NType |= KW_REAL, lambda: Type.Real +NType |= KW_BOOLEAN, lambda: Type.Boolean + +NStatements |= NStatement, ';', lambda st: [st] + +NStatement |= VARNAME, ':=', NExpr, AssignStatement + +NExpr |= NArithmExpr + +def make_op_lambda(op): + return lambda: op + +for op in ('>', '<', '>=', '<=', '=', '<>'): + NCmpOp |= op, make_op_lambda(op) + +NArithmExpr |= NTerm + +NAddOp |= '+', lambda: '+' +NAddOp |= '-', lambda: '-' +NAddOp |= KW_OR, lambda: 'or' + +NTerm |= NFactor + +NMulOp |= '*', lambda: '*' +NMulOp |= '/', lambda: '/' +NMulOp |= KW_DIV, lambda: 'div' +NMulOp |= KW_MOD, lambda: 'mod' +NMulOp |= KW_AND, lambda: 'and' + +NFactor |= NPower + +NPower |= NConst + +NConst |= INTEGER, lambda v: ConstExpr(v, Type.Integer) +NConst |= REAL, lambda v: ConstExpr(v, Type.Real) +NConst |= KW_TRUE, lambda: ConstExpr(True, Type.Boolean) +NConst |= KW_FALSE, lambda: ConstExpr(False, Type.Boolean) + +def test_good_parse(): + p = pe.Parser(NProgram) + p.add_skipped_domain('\\s') + p.add_skipped_domain('(\\(\\*|\\{).*?(\\*\\)|\\})') + assert p.is_ll1() + assert p.parse_ll1(good_text) == Program(var_defs=[VarDef(name='X', type=Type.Integer)], statements=[AssignStatement(variable='X', expr=ConstExpr(value=100, type=Type.Integer))]) + +def test_bad_parse(): + p = pe.Parser(NProgram) + p.add_skipped_domain('\\s') + p.add_skipped_domain('(\\(\\*|\\{).*?(\\*\\)|\\})') + assert p.is_ll1() + + with pytest.raises(pe.ParseError) as parse_error: + p.parse_ll1(bad_text) + + assert parse_error.value.message == "Неожиданный символ VARNAME(Y), ожидалось begin" diff --git a/tests/parser_math_test.py b/tests/parser_math_test.py new file mode 100644 index 0000000..1bee96d --- /dev/null +++ b/tests/parser_math_test.py @@ -0,0 +1,90 @@ +import pytest +import parser_edsl as pe +import re +import math + +CONSTANTS = { + "pi": math.pi, + "e": math.e, + "Na": 6.02214076e23, # Постоянная Больцмана + "kB": 1.380649e-23, # Число Авогадро +} + +# Определения нетерминальных символов +Expr = pe.NonTerminal("Expr") +Term = pe.NonTerminal("Term") +Factor = pe.NonTerminal("Factor") + + +class ZeroDiv(pe.Error): + def __init__(self, pos): + self.pos = pos + + @property + def message(self): + return "Деление на нуль (или ноль)" + + +def checked_div(values, coords, res_coord): + x, y = values + cx, cdiv, cy = coords + + if y != 0: + return x / y + else: + raise ZeroDiv(cy) + + +# Определения терминальных символов + +# Строка из десятичных цифр подходит под оба регулярных выражения, +# поэтому для целых чисел повышаем приоритет (по умолчанию — 5) +integer = pe.Terminal("INTEGER", "[0-9]+", int, priority=7) +real = pe.Terminal("REAL", "[0-9]+(\\.[0-9]*)?([eE][-+]?[0-9]+)?", float) + +# Ключевое слово без учёта регистра (тоже повышаем приоритет) +kw_mod = pe.Terminal("MOD", "mod", lambda x: None, re_flags=re.IGNORECASE, priority=10) +const = pe.Terminal("CONST", "[A-Za-z]+", str) + +# Определение правил грамматики +Expr |= Expr, "+", Term, lambda x, y: x + y +Expr |= Expr, "-", Term, lambda x, y: x - y +Expr |= Term +Term |= Term, "*", Factor, lambda x, y: x * y +Term |= Term, "/", Factor, pe.ExAction(checked_div) # Деление с проверкой +Term |= Term, kw_mod, Factor, lambda x, y: x % y +Term |= Factor +Factor |= integer +Factor |= real +Factor |= const, lambda name: CONSTANTS[name] +Factor |= "(", Expr, ")" + + +def test_is_lalr(): + # Создаём парсер и проверяем грамматику на LALR(1) + p = pe.Parser(Expr) + assert p.is_lalr_one() + + +@pytest.mark.parametrize( + "text, attr", + [ + ("1", 1), + ("1 {комментарий} + 2", 3), + ("2 + 3.5*4/(76-6)", 2.2), + ("1e+2 + 1e-2", 100.01), + ("100 mod 7", 2), + ("100 Mod 7", 2), + ("100 MOD 7", 2), + ("pi + e", 5.859874), + ("kB * Na", 8.314463), + ], +) +def test_parse_ok(text, attr): + p = pe.Parser(Expr) + p.add_skipped_domain("\\s") + p.add_skipped_domain("\\{.*?\\}") + try: + assert round(p.parse(text), 6) == attr + except pe.Error as e: + pytest.fail(f"Ошибка разбора: {e}") diff --git a/tests/token_errors_test.py b/tests/token_errors_test.py new file mode 100644 index 0000000..bc6cb35 --- /dev/null +++ b/tests/token_errors_test.py @@ -0,0 +1,27 @@ +import pytest +import parser_edsl as pe + +def number_action(a): + if int(a) > 100: + raise pe.TokenAttributeError('Слишком большое значение: ' + a) + return int(a) + +@pytest.mark.parametrize( + "text, message, offset, line, col", [ + ("10+300-5", "Слишком большое значение: 300", 3, 1, 4), + ("100+500-5", "Слишком большое значение: 500", 4, 1, 5), + ("1000+2000-5", "Слишком большое значение: 1000", 0, 1, 1), + ] +) +def test_token_attribute_error(text, message, offset, line, col): + expr = pe.NonTerminal('expr') + expr |= (expr, pe.LiteralTerminal('+'), expr, lambda a, b: a + b) + expr |= (expr, pe.LiteralTerminal('-'), expr, lambda a, b: a - b) + expr |= (pe.Terminal('NUM', r'\d+', number_action), lambda x: x) + parser = pe.Parser(expr) + + with pytest.raises(pe.ParseError) as parse_error: + parser.parse(text) + + assert parse_error.value.message == message + assert parse_error.value.pos == pe.Position(offset, line, col) diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/Makefile" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/Makefile" new file mode 100644 index 0000000..885bbb3 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/Makefile" @@ -0,0 +1,18 @@ +.PHONY: build + +all: build + + + +build: + @latexmk -f -pdf -output-directory=build -shell-escape ./report.tex + +build-presentation: + @latexmk -f -pdf -output-directory=build -shell-escape ./presentation.tex + +clean: + @rm -rf build + +build-with-biblio: build + @cp ./biblio.bib ./build + @cd build && biber report diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/biblio.bib" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/biblio.bib" new file mode 100644 index 0000000..ee64a2d --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/biblio.bib" @@ -0,0 +1,94 @@ +@misc{Python, +title = {Документация языка Python}, +url = {https://docs.python.org/3/index.html}, +note = {(дата обращения: 26.12.2024)} +} + +@misc{abc, +title = {Документация библиотеки abc}, +url = {https://docs.python.org/3/library/abc.html}, +note = {(дата обращения: 27.12.2024)} +} + +@misc{collections, +title = {Документация библиотеки collections}, +url = {https://docs.python.org/3/library/collections.html}, +note = {(дата обращения: 26.12.2024)} +} + +@misc{dataclasses, +title = {Документация библиотеки dataclasses}, +url = {https://docs.python.org/3/library/dataclasses.html}, +note = {(дата обращения: 27.12.2024)} +} + +@misc{parsedsl, +title = {Исходный код библиотеки parser\_edsl}, +url = {https://github.com/bmstu-iu9/parser\_edsl\_python}, +note = {(дата обращения: 01.10.2024)} +} + +@book{conmocons, +title={Compiler Construction}, +author={Niklaus Wirth}, +year={2005}, +publisher={Addison-Wesley}, +numpages={131}, +note = {(дата обращения: 10.10.2024)} +} + +@book{earley, +title={An Efficient Context-Free Parsing Algorithm}, +author={Jay Earley}, +year={1968}, +publisher={Carnegie-Mellon University}, +numpages={145}, +note = {(дата обращения: 20.10.2024)} +} + +@incollection{ll1, + title={Predictive analysis and over-the-top parsing}, + author={Kimball, John P}, + booktitle={Syntax and Semantics volume 4}, + pages={155--179}, + year={1975}, + publisher={Brill}, +note = {(дата обращения: 21.10.2024)} +} + +@book{lr, +title={Компиляторы. Принципы, технологии, инструменты}, +author={Альфред Ахо, Рави Сети, Джеффри Ульман}, +year={2003}, +publisher={Вильямс}, +numpages={301-326}, +note = {(дата обращения: 22.10.2024)} +} + +@book{wilhelm2013compiler, + title={Compiler design: syntactic and semantic analysis}, + author={Wilhelm, Reinhard and Seidl, Helmut and Hack, Sebastian}, + year={2013}, + publisher={Springer Science \& Business Media}, + note = {(дата обращения: 24.10.2024)} +} + +@book{Docker, +title={Learn Docker in a Month of Lunches}, +author={Elton Stoneman}, +year={2020}, +publisher={Manning}, +numpages = {464} +} + +@mics{CI, +title = {GitHub Actions documentation}, +url = {https://docs.github.com/en/actions}, +note = {(дата обращения: 06.02.2025)} +} + +@mics{bison, +title={Документация Bison}, +url={https://www.gnu.org/software/bison/manual/}, +note = {(дата обращения: 02.01.2025)} +} diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/images/Emblem.png" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/images/Emblem.png" new file mode 100644 index 0000000..d5d065d Binary files /dev/null and "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/images/Emblem.png" differ diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/a.yaml" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/a.yaml" new file mode 100644 index 0000000..cf21bc3 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/a.yaml" @@ -0,0 +1,30 @@ +name: Parser edsl + +on: + push: + branches: [ main, geogreck-cw-wip ] + pull_request: + branches: [ main, geogreck-cw-wip ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Install dependencies + run: | + pip install pytest + if [ -f requirements.txt ]; + then pip install -r requirements.txt; fi + + - name: Run tests + run: | + pytest diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/bison.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/bison.py" new file mode 100644 index 0000000..52ecc83 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/bison.py" @@ -0,0 +1,42 @@ +@dataclasses.dataclass(frozen=True) +class Precedence: + level: int + associativity: str + def __repr__(self): + return f"Precedence({self.associativity!r}, {self.level!r})" +@dataclasses.dataclass(frozen=True) +class Left(Precedence): + level: int + associativity: str = 'left' +@dataclasses.dataclass(frozen=True) +class Right(Precedence): + level: int + associativity: str = 'right' +@dataclasses.dataclass(frozen=True) +class NonAssoc(Precedence): + level: int + associativity: str = 'nonassoc' + +if shift_action is not None and reduce_action is not None: + _, _, fold, prod_prec = self.productions[reduce_action.rule] + if prod_prec is None: + prod = self.productions[reduce_action.rule][1] + for sym in reversed(prod): + if isinstance(sym, BaseTerminal): + prod_prec = Precedence(sym.priority, 'left') + break + token_prec = cur.type.priority + if prod_prec is None: + action = shift_action + elif token_prec > prod_prec.level: + action = shift_action + elif token_prec < prod_prec.level: + action = reduce_action + else: + if prod_prec.associativity == 'left': + action = reduce_action + elif prod_prec.associativity == 'right': + action = shift_action + else: + raise ParseError(pos=cur.pos.start, unexpected=cur, + expected=[cur.type]) diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/curerrros.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/curerrros.py" new file mode 100644 index 0000000..b781e62 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/curerrros.py" @@ -0,0 +1,12 @@ +@dataclasses.dataclass +class ParseError(Error): + pos : Position + unexpected : Symbol + expected : list + +class LexerError(Error): + ERROR_SLICE = 10 + + def __init__(self, pos, text): + self.pos = pos + self.bad = text[pos.offset:pos.offset + self.ERROR_SLICE] diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_1.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_1.py" new file mode 100644 index 0000000..e6287fb --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_1.py" @@ -0,0 +1,29 @@ +@dataclasses.dataclass(frozen=True) +class EarleyState: + rule: tuple + dot: int + start: int + end: int + attrs: tuple = () + coords: tuple = () + + def __post_init__(self): + object.__setattr__(self, 'attrs', tuple(self.attrs)) + object.__setattr__(self, 'coords', tuple(self.coords)) + + def __repr__(self): + lhs, rhs, _ = self.rule + dotted_rhs = ' '.join(str(x) for x in rhs[:self.dot]) + \ + ' • ' + ' '.join(str(x) for x in rhs[self.dot:]) + return f"{lhs} → {dotted_rhs} [{self.start}, {self.end}] \ + {self.is_complete()} attr({self.attrs})" + + def is_complete(self): + _, rhs, _ = self.rule + return self.dot == len(rhs) + + def next_symbol(self): + _, rhs, _ = self.rule + if self.dot < len(rhs): + return rhs[self.dot] + return None \ No newline at end of file diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_2.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_2.py" new file mode 100644 index 0000000..58e3321 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_2.py" @@ -0,0 +1,49 @@ +class EarleyParser: + """Implements the Earley parsing algorithm.""" + def __init__(self, grammar: Parser): + self.grammar = grammar + self.chart = defaultdict(set) + + def predict(self, state, pos, states): + """Predictor step: Add new states for non-terminals.""" + next_sym = state.next_symbol() + if isinstance(next_sym, NonTerminal): + for prod, fold in next_sym.enum_rules(): + new_state = EarleyState((next_sym, tuple(prod), fold), + 0, pos, pos) + if new_state not in self.chart[pos] and new_state \ + not in states: + states.append(new_state) + self.predict(new_state, pos, states) + + def scan(self, state, token, pos): + """Scanner step: Match terminals with the input.""" + next_sym = state.next_symbol() + if (isinstance(next_sym, LiteralTerminal) or + isinstance(next_sym, Terminal)) and next_sym == token.type: + new_attrs = state.attrs + (token.attr,) + new_coords = state.coords + (token.pos,) + new_state = EarleyState(state.rule, state.dot + 1, + state.start, pos + 1, new_attrs, new_coords) + self.chart[pos + 1].add(new_state) + + def complete(self, state, pos, states: list[EarleyState]): + """Completer step: Propagate completed states.""" + for prev_state in self.chart[state.start]: + next_sym = prev_state.next_symbol() + if next_sym == state.rule[0]: + state_attrs = state.attrs + new_attrs = prev_state.attrs + state_attrs + new_coords = prev_state.coords + state.coords + new_state = EarleyState( + prev_state.rule, + prev_state.dot + 1, + prev_state.start, + pos, + new_attrs, + new_coords + ) + if new_state not in self.chart[pos]: + states.append(new_state) + if not new_state.is_complete(): + self.predict(new_state, pos, states) \ No newline at end of file diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_3.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_3.py" new file mode 100644 index 0000000..c509eb6 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/earley_3.py" @@ -0,0 +1,39 @@ +def parse(self, tokens): + start_rule = (self.grammar.nonterms[0], + tuple(self.grammar.productions[0][1]), + self.grammar.productions[0][2]) + self.chart[0].add(EarleyState(start_rule, 0, 0, 0)) + for pos in range(len(tokens)+1): + states = list(self.chart[pos]) + for state in states: + if not state.is_complete(): + next_sym = state.next_symbol() + if isinstance(next_sym, NonTerminal): + self.predict(state, pos, states) + elif pos < len(tokens): + self.scan(state, tokens[pos], pos) + else: + self.complete(state, pos, states) + if len(states) == len(list(self.chart[pos])): + print(tokens[pos]) + print(list(self.chart[pos-1])[0]) + raise ParseError(tokens[pos].pos.start, + unexpected=tokens[pos], expected=["+"]) + self.chart[pos].update(states) + final_states = [state for state in self.chart[len(tokens)] + if state.rule[0] == self.grammar.nonterms[1] and + state.is_complete() and state.start == 0] + + if len(final_states) > 1: + raise RuntimeError(f"Неопределенная грамматика: найдено + {len(final_states)} путей разбора") + if final_states: + return final_states[0].attrs[0] + return None + + def print_chart(self): + """Print the Earley chart for debugging.""" + for pos, states in sorted(self.chart.items()): + print(f"Chart[{pos}]:") + for state in states: + print(f" {state}") diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/error.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/error.py" new file mode 100644 index 0000000..1786b10 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/error.py" @@ -0,0 +1,35 @@ +def pos_from_offset(text, offset): + line = text.count('\n', 0, offset) + 1 + last_newline = text.rfind('\n', 0, offset) + col = offset - last_newline if last_newline != -1 else offset + 1 + return Position(offset, line, col) + +class TokenAttributeError(Error): + def __init__(self, text): + self.bad = text + def __repr__(self): + return f'TokenAttributeError({self.pos!r},{self.bad!r})' + @property + def message(self): + return f'{self.bad}' + +class Terminal(BaseTerminal): + ... + def match(self, string, pos): + ... + try: + attrib = self.func(string[begin:end]) + except TokenAttributeError as exc: + raise LexerError(pos_from_offset(string, begin), string, + message=exc.message) from exc + +... + +class ParseError(Error): + def parse(self, text): + ... + try: + cur = lexer.next_token() + except LexerError as lex_err: + raise ParseError(pos=lex_err.pos, unexpected=lex_err, + expected=[], _text=lex_err.message) from lex_err diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/1.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/1.py" new file mode 100644 index 0000000..428d9bf --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/1.py" @@ -0,0 +1,2 @@ +from parser_edsl import Terminal, NonTerminal, Parser, EOF_SYMBOL, \ + Left, Right, NonAssoc diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/2.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/2.py" new file mode 100644 index 0000000..e326633 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/2.py" @@ -0,0 +1 @@ +number = Terminal("NUMBER", r"\d+", int) diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/3.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/3.py" new file mode 100644 index 0000000..2c42b30 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/3.py" @@ -0,0 +1,4 @@ +expr = NonTerminal("expr") + +expr |= (expr, '+', expr, Left(1), lambda x, y, z: x + z) +expr |= number diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/4.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/4.py" new file mode 100644 index 0000000..c3d2ea8 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/4.py" @@ -0,0 +1,6 @@ +parser = Parser(expr) +result = parser.parse("1+2+3") + +result = parser.parse_earley("1+2+3") + +result = parser.parse_ll1("1+2+3") diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/5.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/guide/5.py" new file mode 100644 index 0000000..e69de29 diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_1.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_1.py" new file mode 100644 index 0000000..c70b226 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_1.py" @@ -0,0 +1,20 @@ +class PredictiveTableConflictError(Error): + def __init__(self, nonterm, terminal, existing_rule, new_rule): + self.nonterm = nonterm + self.terminal = terminal + self.existing_rule = existing_rule + self.new_rule = new_rule + + @property + def message(self): + return (f'LL(1) conflict: {self.nonterm} {self.terminal}\n' + f'{self.existing_rule}, {self.new_rule}') + + +@dataclasses.dataclass +class ParseTreeNode: + symbol: Symbol + fold: ExAction = None + token: Token = None + children: list = dataclasses.field(default_factory=list) + attribute: object = None diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_2.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_2.py" new file mode 100644 index 0000000..71176c5 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_2.py" @@ -0,0 +1,49 @@ +class PredictiveParsingTable: + def __init__(self, grammar): + self.grammar = grammar + self.table = {} + + self.follow_sets = self._build_follow_sets() + self._build_table() + + def _build_follow_sets(self): + follow = {nt: set() for nt in self.grammar.nonterms} + start_nt = self.grammar.nonterms[0] + + follow[start_nt].add(EOF_SYMBOL) + + changed = True + while changed: + changed = False + for (nt, prod, _, _) in self.grammar.productions: + for i, sym in enumerate(prod): + if sym not in self.grammar.nonterms: + continue + beta = prod[i+1:] + first_of_beta = self.grammar.first_set(beta) + before_len = len(follow[sym]) + + without_epsilon = set(first_of_beta) - {None} + follow[sym].update(without_epsilon) + + if None in first_of_beta: + follow[sym].update(follow[nt]) + + after_len = len(follow[sym]) + if after_len > before_len: + changed = True + return follow + + def _build_table(self): + for nt in self.grammar.nonterms: + self.table[nt] = {} + + for i, (nt, prod, fold, _) in enumerate(self.grammar.productions): + fs = self.grammar.first_set(prod) + non_epsilon = fs - {None} + for t in non_epsilon: + self._add_rule(nt, t, prod, fold) + + if None in fs: + for t in self.follow_sets[nt]: + self._add_rule(nt, t, prod, fold) diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_3.py" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_3.py" new file mode 100644 index 0000000..fbfe36c --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/ll1_3.py" @@ -0,0 +1,22 @@ +def _add_rule(self, nt, terminal, prod, fold): + if terminal not in self.table[nt]: + self.table[nt][terminal] = (prod, fold) + else: + existing = self.table[nt][terminal] + raise PredictiveTableConflictError( + nonterm=nt, + terminal=terminal, + existing_rule=existing, + new_rule=(prod, fold) + ) + + def stringify(self): + lines = [] + for nt in self.grammar.nonterms: + row = self.table[nt] + lines.append(f'{nt}:') + for term, (alpha, fold) in row.items(): + alpha_str = ' '.join(str(s) for s in alpha) if alpha \ + else 'v' + lines.append(f' {term} -> {alpha_str}') + return '\n'.join(lines) diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/scriptA1.sql" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/scriptA1.sql" new file mode 100644 index 0000000..016863f --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/listings/scriptA1.sql" @@ -0,0 +1 @@ +privet \ No newline at end of file diff --git "a/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/report.tex" "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/report.tex" new file mode 100644 index 0000000..4c66049 --- /dev/null +++ "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\223\321\200\320\265\321\207\320\272\320\276 2024/report.tex" @@ -0,0 +1,1466 @@ +% !TeX TXS-program:bibliography = txs:///biber +\documentclass[14pt, russian]{scrartcl} +\let\counterwithout\relax +\let\counterwithin\relax +%\usepackage{lmodern} +\usepackage{float} +\usepackage{xcolor} +\usepackage{extsizes} +\usepackage{subfig} +\usepackage[export]{adjustbox} +\usepackage{tocvsec2} % возможность менять учитываемую глубину разделов в оглавлении +\usepackage[subfigure]{tocloft} +\usepackage[newfloat]{minted} +\removefromtoclist[float]{lol} +\captionsetup[listing]{position=top} + +\AtBeginEnvironment{figure}{\vspace{0.5cm}} +\AtBeginEnvironment{table}{\vspace{0.5cm}} +\AtBeginEnvironment{listing}{\vspace{0.5cm}} +\AtBeginEnvironment{algorithm}{\vspace{0.5cm}} +\AtBeginEnvironment{minted}{\vspace{-0.5cm}} + +\usepackage{fancyvrb} +\usepackage{ulem,bm,mathrsfs,ifsym} %зачеркивания, особо жирный стиль и RSFS начертание +\usepackage{sectsty} % переопределение стилей подразделов +%%%%%%%%%%%%%%%%%%%%%%% + +%%% Поля и разметка страницы %%% +\usepackage{pdflscape} % Для включения альбомных страниц +\usepackage{geometry} % Для последующего задания полей +\geometry{a4paper,tmargin=2cm,bmargin=2cm,lmargin=3cm,rmargin=1cm} % тоже самое, но лучше + +%%% Математические пакеты %%% +\usepackage{amsthm,amsfonts,amsmath,amssymb,amscd} % Математические дополнения от AMS +\usepackage{mathtools} % Добавляет окружение multlined +\usepackage[perpage]{footmisc} +%\usepackage{times} + +%%%% Установки для размера шрифта 14 pt %%%% +%% Формирование переменных и констант для сравнения (один раз для всех подключаемых файлов)%% +%% должно располагаться до вызова пакета fontspec или polyglossia, потому что они сбивают его работу +%\newlength{\curtextsize} +%\newlength{\bigtextsize} +%\setlength{\bigtextsize}{13pt} +\KOMAoptions{fontsize=14pt} + +\makeatletter +\def\showfontsize{\f@size{} point} +\makeatother + +%\makeatletter +%\show\f@size % неплохо для отслеживания, но вызывает стопорение процесса, если документ компилируется без команды -interaction=nonstopmode +%\setlength{\curtextsize}{\f@size pt} +%\makeatother + +%шрифт times +\usepackage{tempora} +%\usepackage{pscyr} +%\setmainfont[Ligatures={TeX,Historic}]{Times New Roman} + + %%% Решение проблемы копирования текста в буфер кракозябрами +% \input glyphtounicode.tex +% \input glyphtounicode-cmr.tex %from pdfx package +% \pdfgentounicode=1 + \usepackage{cmap} % Улучшенный поиск русских слов в полученном pdf-файле + \usepackage[T1]{fontenc} % Поддержка русских букв + \usepackage[utf8]{inputenc} % Кодировка utf8 + \usepackage[english, main=russian]{babel} % Языки: русский, английский +% \IfFileExists{pscyr.sty}{\usepackage{pscyr}}{} % Красивые русские шрифты +%\renewcommand{\rmdefault}{ftm} +%%% Оформление абзацев %%% +\usepackage{indentfirst} % Красная строка +%\usepackage{eskdpz} + +%%% Таблицы %%% +\usepackage{longtable} % Длинные таблицы +\usepackage{multirow,makecell,array} % Улучшенное форматирование таблиц +\usepackage{booktabs} % Возможность оформления таблиц в классическом книжном стиле (при правильном использовании не противоречит ГОСТ) + +%%% Общее форматирование +\usepackage{soulutf8} % Поддержка переносоустойчивых подчёркиваний и зачёркиваний +\usepackage{icomma} % Запятая в десятичных дробях + + + +%%% Изображения %%% +\usepackage{graphicx} % Подключаем пакет работы с графикой +\usepackage{wrapfig} + +%%% Списки %%% +\usepackage{enumitem} + +%%% Подписи %%% +\usepackage{caption} % Для управления подписями (рисунков и таблиц) % Может управлять номерами рисунков и таблиц с caption %Иногда может управлять заголовками в списках рисунков и таблиц +%% Использование: +%\begin{table}[h!]\ContinuedFloat - чтобы не переключать счетчик +%\captionsetup{labelformat=continued}% должен стоять до самого caption +%\caption{} +% либо ручками \caption*{Продолжение таблицы~\ref{...}.} :) + +%%% Интервалы %%% +\addto\captionsrussian{% + \renewcommand{\listingname}{Листинг}% +} +%%% Счётчики %%% +\usepackage[figure,table,section]{totalcount} % Счётчик рисунков и таблиц +\DeclareTotalCounter{lstlisting} +\usepackage{totcount} % Пакет создания счётчиков на основе последнего номера подсчитываемого элемента (может требовать дважды компилировать документ) +\usepackage{totpages} % Счётчик страниц, совместимый с hyperref (ссылается на номер последней страницы). Желательно ставить последним пакетом в преамбуле + +%%% Продвинутое управление групповыми ссылками (пока только формулами) %%% +%% Кодировки и шрифты %%% + +% \newfontfamily{\cyrillicfont}{Times New Roman} +% \newfontfamily{\cyrillicfonttt}{CMU Typewriter Text} + %\setmainfont{Times New Roman} + %\newfontfamily\cyrillicfont{Times New Roman} + %\setsansfont{Times New Roman} %% задаёт шрифт без засечек +% \setmonofont{Liberation Mono} %% задаёт моноширинный шрифт +% \IfFileExists{pscyr.sty}{\renewcommand{\rmdefault}{ftm}}{} +%%% Интервалы %%% +%linespread-реализация ближе к реализации полуторного интервала в ворде. +%setspace реализация заточена под шрифты 10, 11, 12pt, под остальные кегли хуже, но всё же ближе к типографской классике. +\linespread{1.3} % Полуторный интервал (ГОСТ Р 7.0.11-2011, 5.3.6) +%\renewcommand{\@biblabel}[1]{#1} + +%%% Гиперссылки %%% +\usepackage{hyperref} + +%%% Выравнивание и переносы %%% +\sloppy % Избавляемся от переполнений +\clubpenalty=10000 % Запрещаем разрыв страницы после первой строки абзаца +\widowpenalty=10000 % Запрещаем разрыв страницы после последней строки абзаца + +\makeatletter % малые заглавные, small caps shape +\let\@@scshape=\scshape +\renewcommand{\scshape}{% + \ifnum\strcmp{\f@series}{bx}=\z@ + \usefont{T1}{cmr}{bx}{sc}% + \else + \ifnum\strcmp{\f@shape}{it}=\z@ + \fontshape{scsl}\selectfont + \else + \@@scshape + \fi + \fi} +\makeatother + +%%% Подписи %%% +%\captionsetup{% +%singlelinecheck=off, % Многострочные подписи, например у таблиц +%skip=2pt, % Вертикальная отбивка между подписью и содержимым рисунка или таблицы определяется ключом +%justification=centering, % Центрирование подписей, заданных командой \caption +%} +%%% Подключение пакетов %%% +\usepackage{ifthen} % добавляет ifthenelse +%%% Инициализирование переменных, не трогать! %%% +\newcounter{intvl} +\newcounter{otstup} +\newcounter{contnumeq} +\newcounter{contnumfig} +\newcounter{contnumtab} +\newcounter{pgnum} +\newcounter{bibliosel} +\newcounter{chapstyle} +\newcounter{headingdelim} +\newcounter{headingalign} +\newcounter{headingsize} +\newcounter{tabcap} +\newcounter{tablaba} +\newcounter{tabtita} +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%% Область упрощённого управления оформлением %%% + +%% Интервал между заголовками и между заголовком и текстом +% Заголовки отделяют от текста сверху и снизу тремя интервалами (ГОСТ Р 7.0.11-2011, 5.3.5) +\setcounter{intvl}{3} % Коэффициент кратности к размеру шрифта + +%% Отступы у заголовков в тексте +\setcounter{otstup}{0} % 0 --- без отступа; 1 --- абзацный отступ + +%% Нумерация формул, таблиц и рисунков +\setcounter{contnumeq}{1} % Нумерация формул: 0 --- пораздельно (во введении подряд, без номера раздела); 1 --- сквозная нумерация по всей диссертации +\setcounter{contnumfig}{1} % Нумерация рисунков: 0 --- пораздельно (во введении подряд, без номера раздела); 1 --- сквозная нумерация по всей диссертации +\setcounter{contnumtab}{1} % Нумерация таблиц: 0 --- пораздельно (во введении подряд, без номера раздела); 1 --- сквозная нумерация по всей диссертации + +%% Оглавление +\setcounter{pgnum}{0} % 0 --- номера страниц никак не обозначены; 1 --- Стр. над номерами страниц (дважды компилировать после изменения) + +%% Библиография +\setcounter{bibliosel}{1} % 0 --- встроенная реализация с загрузкой файла через движок bibtex8; 1 --- реализация пакетом biblatex через движок biber + +%% Текст и форматирование заголовков +\setcounter{chapstyle}{1} % 0 --- разделы только под номером; 1 --- разделы с названием "Глава" перед номером +\setcounter{headingdelim}{1} % 0 --- номер отделен пропуском в 1em или \quad; 1 --- номера разделов и приложений отделены точкой с пробелом, подразделы пропуском без точки; 2 --- номера разделов, подразделов и приложений отделены точкой с пробелом. + +%% Выравнивание заголовков в тексте +\setcounter{headingalign}{0} % 0 --- по центру; 1 --- по левому краю + +%% Размеры заголовков в тексте +\setcounter{headingsize}{0} % 0 --- по ГОСТ, все всегда 14 пт; 1 --- пропорционально изменяющийся размер в зависимости от базового шрифта + +%% Подпись таблиц +\setcounter{tabcap}{0} % 0 --- по ГОСТ, номер таблицы и название разделены тире, выровнены по левому краю, при необходимости на нескольких строках; 1 --- подпись таблицы не по ГОСТ, на двух и более строках, дальнейшие настройки: +%Выравнивание первой строки, с подписью и номером +\setcounter{tablaba}{2} % 0 --- по левому краю; 1 --- по центру; 2 --- по правому краю +%Выравнивание строк с самим названием таблицы +\setcounter{tabtita}{1} % 0 --- по левому краю; 1 --- по центру; 2 --- по правому краю + +%%% Рисунки %%% +\DeclareCaptionLabelSeparator*{emdash}{~--- } % (ГОСТ 2.105, 4.3.1) +\captionsetup[figure]{labelsep=emdash,font=onehalfspacing,position=bottom} + +%%% Таблицы %%% +\ifthenelse{\equal{\thetabcap}{0}}{% + \newcommand{\tabcapalign}{\raggedright} % по левому краю страницы или аналога parbox +} + +\ifthenelse{\equal{\thetablaba}{0} \AND \equal{\thetabcap}{1}}{% + \newcommand{\tabcapalign}{\raggedright} % по левому краю страницы или аналога parbox +} + +\ifthenelse{\equal{\thetablaba}{1} \AND \equal{\thetabcap}{1}}{% + \newcommand{\tabcapalign}{\centering} % по центру страницы или аналога parbox +} + +\ifthenelse{\equal{\thetablaba}{2} \AND \equal{\thetabcap}{1}}{% + \newcommand{\tabcapalign}{\raggedleft} % по правому краю страницы или аналога parbox +} + +\ifthenelse{\equal{\thetabtita}{0} \AND \equal{\thetabcap}{1}}{% + \newcommand{\tabtitalign}{\raggedright} % по левому краю страницы или аналога parbox +} + +\ifthenelse{\equal{\thetabtita}{1} \AND \equal{\thetabcap}{1}}{% + \newcommand{\tabtitalign}{\centering} % по центру страницы или аналога parbox +} + +\ifthenelse{\equal{\thetabtita}{2} \AND \equal{\thetabcap}{1}}{% + \newcommand{\tabtitalign}{\raggedleft} % по правому краю страницы или аналога parbox +} + +\DeclareCaptionFormat{tablenocaption}{\tabcapalign #1\strut} % Наименование таблицы отсутствует +\ifthenelse{\equal{\thetabcap}{0}}{% + \DeclareCaptionFormat{tablecaption}{\tabcapalign #1#2#3} + \captionsetup[table]{labelsep=emdash} % тире как разделитель идентификатора с номером от наименования +}{% + \DeclareCaptionFormat{tablecaption}{\tabcapalign #1#2\par% % Идентификатор таблицы на отдельной строке + \tabtitalign{#3}} % Наименование таблицы строкой ниже + \captionsetup[table]{labelsep=space} % пробельный разделитель идентификатора с номером от наименования +} +\captionsetup[table]{format=tablecaption,singlelinecheck=off,font=onehalfspacing,position=top,skip=-5pt} % многострочные наименования и прочее +\DeclareCaptionLabelFormat{continued}{Продолжение таблицы~#2} +\setlength{\belowcaptionskip}{.2cm} +\setlength{\intextsep}{0ex} + +%%% Подписи подрисунков %%% +\renewcommand{\thesubfigure}{\asbuk{subfigure}} % Буквенные номера подрисунков +\captionsetup[subfigure]{font={normalsize}, % Шрифт подписи названий подрисунков (не отличается от основного) + labelformat=brace, % Формат обозначения подрисунка + justification=centering, % Выключка подписей (форматирование), один из вариантов +} +%\DeclareCaptionFont{font12pt}{\fontsize{12pt}{13pt}\selectfont} % объявляем шрифт 12pt для использования в подписях, тут же надо интерлиньяж объявлять, если не наследуется +%\captionsetup[subfigure]{font={font12pt}} % Шрифт подписи названий подрисунков (всегда 12pt) + +%%% Настройки гиперссылок %%% + +\definecolor{linkcolor}{rgb}{0.0,0,0} +\definecolor{citecolor}{rgb}{0,0.0,0} +\definecolor{urlcolor}{rgb}{0,0,0} + +\hypersetup{ + linktocpage=true, % ссылки с номера страницы в оглавлении, списке таблиц и списке рисунков +% linktoc=all, % both the section and page part are links +% pdfpagelabels=false, % set PDF page labels (true|false) + plainpages=true, % Forces page anchors to be named by the Arabic form of the page number, rather than the formatted form + colorlinks, % ссылки отображаются раскрашенным текстом, а не раскрашенным прямоугольником, вокруг текста + linkcolor={linkcolor}, % цвет ссылок типа ref, eqref и подобных + citecolor={citecolor}, % цвет ссылок-цитат + urlcolor={urlcolor}, % цвет гиперссылок + pdflang={ru}, +} +\urlstyle{same} +%%% Шаблон %%% +%\DeclareRobustCommand{\todo}{\textcolor{red}} % решаем проблему превращения названия цвета в результате \MakeUppercase, http://tex.stackexchange.com/a/187930/79756 , \DeclareRobustCommand protects \todo from expanding inside \MakeUppercase +\setlength{\parindent}{2.5em} % Абзацный отступ. Должен быть одинаковым по всему тексту и равен пяти знакам (ГОСТ Р 7.0.11-2011, 5.3.7). + +%%% Списки %%% +% Используем дефис для ненумерованных списков (ГОСТ 2.105-95, 4.1.7) +%\renewcommand{\labelitemi}{\normalfont\bfseries~{---}} +\renewcommand{\labelitemi}{\bfseries~{---}} +\setlist{nosep,% % Единый стиль для всех списков (пакет enumitem), без дополнительных интервалов. + labelindent=\parindent,leftmargin=*% % Каждый пункт, подпункт и перечисление записывают с абзацного отступа (ГОСТ 2.105-95, 4.1.8) +} +%%%%%%%%%%%%%%%%%%%%%% +%\usepackage{xltxtra} % load xunicode + +\usepackage{ragged2e} +\usepackage[explicit]{titlesec} +\usepackage{placeins} +\usepackage{xparse} +\usepackage{csquotes} + +\usepackage{listingsutf8} +\usepackage{url} %пакеты расширений +\usepackage{algorithm, algorithmicx} +\usepackage[noend]{algpseudocode} +\usepackage{blkarray} +\usepackage{chngcntr} +\usepackage{tabularx} +\usepackage[backend=biber, + bibstyle=gost-numeric, + citestyle=nature]{biblatex} +\newcommand*\template[1]{\text{<}#1\text{>}} +\addbibresource{biblio.bib} + +\titleformat{name=\section,numberless}[block]{\normalfont\Large\centering}{}{0em}{#1} +\titleformat{\section}[block]{\normalfont\Large\bfseries\raggedright}{}{0em}{\thesection\hspace{0.25em}#1} +\titleformat{\subsection}[block]{\normalfont\Large\bfseries\raggedright}{}{0em}{\thesubsection\hspace{0.25em}#1} +\titleformat{\subsubsection}[block]{\normalfont\large\bfseries\raggedright}{}{0em}{\thesubsubsection\hspace{0.25em}#1} + +\let\Algorithm\algorithm +\renewcommand\algorithm[1][]{\Algorithm[#1]\setstretch{1.5}} +%\renewcommand{\listingscaption}{Листинг} + +\usepackage{pifont} +\usepackage{calc} +\usepackage{suffix} +\usepackage{csquotes} +\DeclareQuoteStyle{russian} + {\guillemotleft}{\guillemotright}[0.025em] + {\quotedblbase}{\textquotedblleft} +\ExecuteQuoteOptions{style=russian} +\newcommand{\enq}[1]{\enquote{#1}} +\newcommand{\eng}[1]{\begin{english}#1\end{english}} +% Подчиненные счетчики в окружениях http://old.kpfu.ru/journals/izv_vuz/arch/sample1251.tex +\newcounter{cTheorem} +\newcounter{cDefinition} +\newcounter{cConsequent} +\newcounter{cExample} +\newcounter{cLemma} +\newcounter{cConjecture} +\newtheorem{Theorem}{Теорема}[cTheorem] +\newtheorem{Definition}{Определение}[cDefinition] +\newtheorem{Consequent}{Следствие}[cConsequent] +\newtheorem{Example}{Пример}[cExample] +\newtheorem{Lemma}{Лемма}[cLemma] +\newtheorem{Conjecture}{Гипотеза}[cConjecture] + +\renewcommand{\theTheorem}{\arabic{Theorem}} +\renewcommand{\theDefinition}{\arabic{Definition}} +\renewcommand{\theConsequent}{\arabic{Consequent}} +\renewcommand{\theExample}{\arabic{Example}} +\renewcommand{\theLemma}{\arabic{Lemma}} +\renewcommand{\theConjecture}{\arabic{Conjecture}} +%\makeatletter +\NewDocumentCommand{\Newline}{}{\text{\\}} +\newcommand{\sequence}[2]{\ensuremath \left(#1,\ \dots,\ #2\right)} + +\definecolor{mygreen}{rgb}{0,0.6,0} +\definecolor{mygray}{rgb}{0.5,0.5,0.5} +\definecolor{mymauve}{rgb}{0.58,0,0.82} +\renewcommand{\listalgorithmname}{Список алгоритмов} +\floatname{algorithm}{Листинг} +\renewcommand{\lstlistingname}{Листинг} +\renewcommand{\thealgorithm}{\arabic{algorithm}} + +\newcommand{\refAlgo}[1]{(листинг \ref{#1})} +\newcommand{\refImage}[1]{(рисунок \ref{#1})} + +\renewcommand{\theenumi}{\arabic{enumi}.}% Меняем везде перечисления на цифра.цифра +\renewcommand{\labelenumi}{\arabic{enumi}.}% Меняем везде перечисления на цифра.цифра +\renewcommand{\theenumii}{\arabic{enumii}}% Меняем везде перечисления на цифра.цифра +\renewcommand{\labelenumii}{(\arabic{enumii})}% Меняем везде перечисления на цифра.цифра +\renewcommand{\theenumiii}{\roman{enumiii}}% Меняем везде перечисления на цифра.цифра +\renewcommand{\labelenumiii}{(\roman{enumiii})}% Меняем везде перечисления на цифра.цифра +%\newfontfamily\AnkaCoder[Path=src/fonts/]{AnkaCoder-r.ttf} +\renewcommand{\labelitemi}{---} +\renewcommand{\labelitemii}{---} + +%\usepackage{courier} + +\lstdefinelanguage{Refal}{ + alsodigit = {.,<,>}, + morekeywords = [1]{$ENTRY}, + morekeywords = [2]{Go, Put, Get, Open, Close, Arg, Add, Sub, Mul, Div, Symb, Explode, Implode}, + %keyword4 + morekeywords = [3]{<,>}, + %keyword5 + morekeywords = [4]{e.,t.,s.}, + sensitive = true, + morecomment = [l]{*}, + morecomment = [s]{/*}{*/}, + commentstyle = \color{mygreen}, + morestring = [b]", + morestring = [b]', + stringstyle = \color{purple} +} + +\makeatletter +\def\p@subsection{} +\def\p@subsubsection{\thesection\,\thesubsection\,} +\makeatother +\newcommand{\prog}[1]{{\ttfamily\small#1}} +\lstset{ % + backgroundcolor=\color{white}, % choose the background color; you must add \usepackage{color} or \usepackage{xcolor} + basicstyle=\ttfamily\footnotesize, + %basicstyle=\footnotesize\AnkaCoder, % the size of the fonts that are used for the code + breakatwhitespace=false, % sets if automatic breaks shoulbd only happen at whitespace + breaklines=true, % sets automatic line breaking + captionpos=top, % sets the caption-position to bottom + commentstyle=\color{mygreen}, % comment style + deletekeywords={...}, % if you want to delete keywords from the given language + escapeinside={\%*}{*)}, % if you want to add LaTeX within your code + extendedchars=true, % lets you use non-ASCII characters; for 8-bits encodings only, does not work with UTF-8 + inputencoding=utf8, + frame=single, % adds a frame around the code + keepspaces=true, % keeps spaces in text, useful for keeping indentation of code (possibly needs columns=flexible) + keywordstyle=\bf, % keyword style + language=Refal, % the language of the code + morekeywords={<,>,$ENTRY,Go,Arg, Open, Close, e., s., t., Get, Put}, + % if you want to add more keywords to the set + numbers=left, % where to put the line-numbers; possible values are (none, left, right) + numbersep=5pt, % how far the line-numbers are from the code + xleftmargin=25pt, + xrightmargin=25pt, + numberstyle=\small\color{black}, % the style that is used for the line-numbers + rulecolor=\color{black}, % if not set, the frame-color may be changed on line-breaks within not-black text (e.g. comments (green here)) + showspaces=false, % show spaces everywhere adding particular underscores; it overrides 'showstringspaces' + showstringspaces=false, % underline spaces within strings only + showtabs=false, % show tabs within strings adding particular underscores + stepnumber=1, % the step between two line-numbers. If it's 1, each line will be numbered + stringstyle=\color{mymauve}, % string literal style + tabsize=8, % sets default tabsize to 8 spaces + title=\lstname % show the filename of files included with \lstinputlisting; also try caption instead of title +} +\newcommand{\anonsection}[1]{\cleardoublepage +\phantomsection +\addcontentsline{toc}{section}{\protect\numberline{}#1} +\section*{#1}\vspace*{2.5ex} % По госту положены 3 пустые строки после заголовка ненумеруемого раздела +} +\newcommand{\sectionbreak}{\clearpage} +\renewcommand{\sectionfont}{\normalsize} % Сбиваем стиль оглавления в стандартный +\renewcommand{\cftsecleader}{\cftdotfill{\cftdotsep}} % Точки в оглавлении напротив разделов + +\renewcommand{\cftsecfont}{\normalfont\large} % Переключение на times в содержании +\renewcommand{\cftsubsecfont}{\normalfont\large} % Переключение на times в содержании + +\usepackage{caption} +%\captionsetup[table]{justification=raggedleft} +%\captionsetup[figure]{justification=centering,labelsep=endash} +\usepackage{amsmath} % \bar (матрицы и проч. ...) +\usepackage{amsfonts} % \mathbb (символ для множества действительных чисел и проч. ...) +\usepackage{mathtools} % \abs, \norm + \DeclarePairedDelimiter\abs{\lvert}{\rvert} % операция модуля + \DeclarePairedDelimiter\norm{\lVert}{\rVert} % операция нормы +\DeclareTextCommandDefault{\textvisiblespace}{% + \mbox{\kern.06em\vrule \@height.3ex}% + \vbox{\hrule \@width.3em}% + \hbox{\vrule \@height.3ex}} +\newsavebox{\spacebox} +\begin{lrbox}{\spacebox} +\verb*! ! +\end{lrbox} +\newcommand{\aspace}{\usebox{\spacebox}} +\DeclareTotalCounter{listing} + +\makeatletter +\renewcommand*{\p@subsubsection}{} +\makeatother + +\begin{document} +\sloppy + +\def\figurename{Рисунок} + +\begin{titlepage} + \thispagestyle{empty} + \newpage + + \vspace*{-30pt} + \hspace{-45pt} + \begin{minipage}{0.17\textwidth} + \hspace*{-20pt}\centering + \includegraphics[width=1.3\textwidth]{images/emblem.png} + \end{minipage} + \begin{minipage}{0.82\textwidth}\small \textbf{ + \vspace*{-0.7ex} + \hspace*{-10pt}\centerline{Министерство науки и высшего образования Российской Федерации} + \vspace*{-0.7ex} + \centerline{Федеральное государственное бюджетное образовательное учреждение } + \vspace*{-0.7ex} + \centerline{высшего образования} + \vspace*{-0.7ex} + \centerline{<<Московский государственный технический университет} + \vspace*{-0.7ex} + \centerline{имени Н.Э. Баумана} + \vspace*{-0.7ex} + \centerline{(национальный исследовательский университет)>>} + \vspace*{-0.7ex} + \centerline{(МГТУ им. Н.Э. Баумана)}} + \end{minipage} + + \vspace{-2pt} + \hspace{-34.5pt}\rule{\textwidth}{2.5pt} + + \vspace*{-20.3pt} + \hspace{-34.5pt}\rule{\textwidth}{0.4pt} + + \vspace{0.5ex} + \noindent \small ФАКУЛЬТЕТ\hspace{80pt} <<Информатика и системы управления>> + + \vspace*{-16pt} + \hspace{35pt}\rule{0.855\textwidth}{0.4pt} + + \vspace{0.5ex} + \noindent \small КАФЕДРА\hspace{50pt} <<Теоретическая информатика и компьютерные технологии>> + + \vspace*{-16pt} + \hspace{25pt}\rule{0.875\textwidth}{0.4pt} + + + \vspace{3em} + + \begin{center} + \Large \bf{РАСЧЕТНО-ПОЯСНИТЕЛЬНАЯ ЗАПИСКА\\\textbf{\textit{К КУРСОВОЙ РАБОТЕ\\НА ТЕМУ:}} \\} + \end{center} + + \vspace*{-6ex} + \begin{center} + \Large{\textit{\textbf{<<Расширение возможностей}}} + + \vspace*{-3ex} + \rule{0.9\textwidth}{1.2pt} + + \vspace*{-0.2ex} + \Large{\textit{\textbf{библиотеки parser\_edsl>>}}} + \vspace*{-3ex} + \vspace*{-0.2ex} + \rule{0.9\textwidth}{1.2pt} + + \vspace*{-0.2ex} + \rule{0.9\textwidth}{1.2pt} + + \vspace*{-0.2ex} + \rule{0.9\textwidth}{1.2pt} + + \vspace*{-0.2ex} + \rule{0.9\textwidth}{1.2pt} + \end{center} + + \vspace{\fill} + + + \newlength{\ML} + \settowidth{\ML}{«\underline{\hspace{0.7cm}}» \underline{\hspace{2cm}}} + + \noindent Студент \underline{\text{ИУ9-71Б}} \hfill \underline{ \hspace{4cm}}\quad + \underline{\parbox{4cm}{\centering\raisebox{0.25ex}{ Гречко Г.В.}}} + + \vspace{-2.1ex} + \noindent\hspace{9ex}\scriptsize{(Группа)}\normalsize\hspace{170pt}\hspace{2ex}\scriptsize{(Подпись, дата)}\normalsize\hspace{30pt}\hspace{6ex}\scriptsize{(И.О. Фамилия)}\normalsize + + \bigskip + + \noindent Руководитель \hfill \underline{\hspace{4cm}}\quad + \underline{\parbox{4cm}{\centering\raisebox{0.25ex}{ Коновалов А.В.}}} + + \vspace{-2ex} + \noindent\hspace{13.5ex}\normalsize\hspace{170pt}\hspace{2ex}\scriptsize{(Подпись, дата)}\normalsize\hspace{30pt}\hspace{6ex}\scriptsize{(И.О. Фамилия)}\normalsize + + \bigskip + + \noindent Консультант\hfill \underline{\hspace{4cm}}\quad + \underline{\hspace{4cm}} + + \vspace{-2ex} + \noindent\hspace{13.5ex}\normalsize\hspace{170pt}\hspace{2ex}\scriptsize{(Подпись, дата)}\normalsize\hspace{30pt}\hspace{6ex}\scriptsize{(И.О. Фамилия)}\normalsize + \vfill + + %\vspace{\fill} + + + + \begin{center} + \textsl{2025 г.} + \end{center} +\end{titlepage} + +%\renewcommand{\ttdefault}{pcr} + +\setlength{\tabcolsep}{3pt} +\newpage +\setcounter{page}{2} +%---------------------------------------------------------------------------- +% ОТСЮДА --- СОБСТВЕННО ТЕКСТ +%---------------------------------------------------------------------------- + +\newpage +\renewcommand\contentsname{\hfill{\normalfont{СОДЕРЖАНИЕ}}\hfill} %Оглавление +\tableofcontents +\newpage +\anonsection{Введение} %Введение + +В современном мире разработка компиляторов и интерпретаторов занимает важное место в компьютерных науках, позволяя +создавать эффективные инструменты для анализа и трансляции языков программирования. Для упрощения разработки +компиляторов и повышения их +гибкости широко применяются специализированные библиотеки, реализующие различные методы синтаксического анализа. +Библиотека parser\_edsl является одним из таких инструментов, предоставляющих возможность описания синтаксиса +языков, используя средства объектно-ориентированных языков программирования (python в случае данной библиотеки). + +Цель данной курсовой работы заключается в расширении возможностей библиотеки parser\_edsl +за счёт реализации дополнительной функциональности: + +\begin{itemize} + \item Поддержка лексических ошибок --- функции вычисления атрибутов токенов + могут возбуждать библиотечное исключение, которое затем транслируется + в синтаксическую ошибку. + \item Поддержка режимов разбора <<предсказывающий анализ>> (только для LL(1)- + грамматик) и <<алгоритм Эрли>>. + \item Возможность указания приоритета и ассоциативности в стиле Bison'a. +\end{itemize} + +% \newpage + +\section{Основные теоретические сведения} + +\subsection{Компилирование} + +Компиляция — это процесс преобразования исходного кода, написанного на одном языке программирования, +на другой язык или машинный код, +обычно пригодный для выполнения целевой машиной\cite{conmocons}. Основная цель компиляции заключается в том, чтобы +трансформировать высокоуровневые конструкции, понятные человеку, в низкоуровневые инструкции, исполняемые +процессором. Помимо этого, компиляторы могут выполнять различные оптимизации или находить ошибки в коде программы. + +Стандартный процесс компиляции обычно включает несколько последовательных фаз, каждая из которых выполняет свою задачу: + +\begin{enumerate} + \item \textbf{Лексический анализ (токенизация)} --- На этой фазе исходный текст программы разбивается на + элементарные единицы — лексемы (токены), такие как ключевые слова, идентификаторы, операторы и литералы. + Лексический анализатор также занимается удалением незначащих символов (например, пробелов, комментариев) + и обработкой лексических ошибок. + + \item \textbf{Синтаксический анализ} --- После лексического анализа последовательность токенов анализируется + с точки зрения синтаксиса языка. В ходе синтаксического разбора строится синтаксическое дерево, + которое отражает иерархическую структуру программы согласно грамматике языка. Здесь выявляются синтаксические + ошибки, если входной поток токенов не соответствует правилам языка. + + \item \textbf{Семантический анализ} --- Построенное синтаксическое дерево подвергается дополнительной проверке + на семантическую корректность. На этой фазе осуществляется анализ типов, разрешение идентификаторов, + проверка областей видимости и другие аспекты, не охватываемые синтаксическим анализом. Семантический анализ + позволяет выявить логические и концептуальные ошибки, которые не были обнаружены ранее. + + \item \textbf{Генерация промежуточного кода} --- Для упрощения дальнейших оптимизаций исходный код преобразуется + в промежуточное представление (IR), которое, как правило, является независимым от целевой аппаратной платформы. + Такое представление облегчает проведение оптимизаций и последующую трансляцию в машинный код. + + \item \textbf{Оптимизация} --- На этой фазе производится улучшение промежуточного представления для повышения + эффективности программы. Оптимизации могут включать устранение избыточных вычислений, оптимизацию циклов, + минимизацию использования памяти и прочие техники, направленные на улучшение производительности. + + \item \textbf{Генерация целевого кода} --- Заключительный этап компиляции заключается в преобразовании + оптимизированного промежуточного представления в конечный код, который может быть исполнен на конкретной + аппаратной платформе. Это может быть машинный код, ассемблер или другой специализированный формат. +\end{enumerate} + +Каждый этап решает специфическую задачу, что облегчает обнаружение ошибок и их последующее исправление, а +также позволяет проводить конкретные оптимизации, направленные на ускорение одного из этапов компиляции. +В результате получается надёжный и эффективный компилятор, пригодный для решения серьезных задач. + +Библиотека parser\_edsl предоставляет инструменты для решения задач лексического, синтаксического и семантического +анализов, поэтому рассмотрим эти шаги подробнее. + +\subsection{Лексический разбор} + +Лексический анализатор читает текст из входного потока и разбивает его на лексемы. Если разбить на лексемы не получится, +анализатор вернет ошибку лексического разбора. Псевдокод для лексического анализатора, используемого в библиотеке +parser\_edsl, представлен на листинге \ref{lst:lex}. + +\begin{listing}[H] + \caption{Лексический разбор} + \label{lst:lex} +\begin{algorithm}[H] +\begin{algorithmic}[1] +\Procedure{NextToken}{domains, text, skip\_token} + \State $pos \gets \text{начальная позиция (объект Position)}$ + \While{$pos.offset < \text{length}(text)$} + \State $offset \gets pos.offset$ + \State $matches \gets \varnothing$ + \ForAll{доменов $d$ из \texttt{domains}} + \State $(length, attr) \gets d.match(text, offset)$ + \State $matches \gets matches \cup \{(d, d.priority, length, attr)\}$ + \EndFor + \State $(domain, priority, length, attr) \gets \max(matches,\ \lambda (d, pr, l, a): (l, pr))$ + \State \textbf{assert} $length > 0$ + \If{$attr$ соответствует \texttt{ErrorTerminal}} + \State \textbf{raise} LexerError($pos$, $text$) + \EndIf + \State $new\_pos \gets pos.\text{shift}(text[offset : offset + length])$ + \State $fragment \gets \text{Fragment}(pos, new\_pos)$ + \State $pos \gets new\_pos$ + \If{$attr \neq skip\_token$} + \State \Return Token($domain$, $fragment$, $attr$) + \EndIf + \EndWhile + \State \Return Token(EOF\_SYMBOL, Fragment($pos$, $pos$), \texttt{None}) +\EndProcedure +\end{algorithmic} +\end{algorithm} +\end{listing} + +\subsection{Алгоритмы синтаксического разбора} + +Существует множество алгоритмов синтаксического разбора, каждый из которых имеет свои преимущества и ограничения. +Выбор конкретного метода определяется характеристиками грамматики языка и требованиями к скорости, +объёму памяти и способности обработки неоднозначностей. В данной работе будут затрагиваться алгоритмы LR разбора, +Эрли и предсказывающий разбор для LL(1) грамматик. Рассмотрим их подробнее. + +\subsubsection{LR разбор} + +LR-разбор\cite{lr0} (от англ. Left-to-right, Rightmost derivation in reverse) является одним из самых мощных и широко +используемых методов синтаксического анализа. Основная идея метода заключается в последовательном сканировании +входной строки слева направо с использованием стека для хранения промежуточных состояний. Алгоритм выполняет +так называемые операции сдвига (shift) и свёртки (reduce), преобразуя последовательность токенов в структуру, +соответствующую правилам грамматики. + +К преимуществам LR-разбора относятся: + +\begin{itemize} + \item Обработка широкого класса грамматик --- алгоритм способен работать с грамматиками, для которых невозможно + применить более простые виды анализаторов (например --- LL(1) предсказывающий разбор). + \item Детерминированность --- При корректном построении таблиц переходов алгоритм не требует отката, что + обеспечивает высокую скорость анализа. +\end{itemize} + +Однако, работа LR-анализаторов может оказаться достаточно затратной по вычислительным ресурсам для больших грамматик, +поскольку таблицы переходов могут иметь значительные размеры, что накладывает ограничения на использование +данного алгоритма. + +На листинге \ref{lst:lr} представлен псевдокод алгоритма LR-разбора. + +\begin{listing}[H] + \caption{LR-разбор} + \label{lst:lr} +\begin{algorithm}[H] + \begin{algorithmic}[1] + \Procedure{LRParse}{tokens, ACTION, GOTO} + \State $stack \gets [0]$ + \State $i \gets 0$ + \While{true} + \State $s \gets$ \textbf{top}$(stack)$ + \State $a \gets tokens[i]$ + \State $action \gets$ ACTION[$s, a$] + \If{$action$ имеет вид <>} + \State \textbf{push}($s'$) на $stack$ + \State $i \gets i + 1$ + \ElsIf{$action$ имеет вид <>} + \State \textbf{pop} $2\cdot |\beta|$ элементов из $stack$ + \State $t \gets$ \textbf{top}$(stack)$ + \State \textbf{push}(GOTO[$t, A$]) на $stack$ + \ElsIf{$action$ является <>} + \State \Return ok + \Else + \State \Return fail + \EndIf + \EndWhile + \EndProcedure + \end{algorithmic} + \end{algorithm} +\end{listing} + +\subsubsection{Алгоритм Эрли} + +\label{sec:earleyint} + +Алгоритм Эрли\cite{earley} представляет собой универсальный метод синтаксического анализа, способный обрабатывать любые +контекстно-свободные грамматики, включая неоднозначные. Данный алгоритм основан на принципах динамического +программирования и допускает параллельное рассмотрение нескольких вариантов разбора входной строки. + +Главное преимущество алгоритма Эрли --- способность работать с грамматиками, содержащими неоднозначности. + +Основным недостатком алгоритма является его потенциальная вычислительная сложность. В худших случаях алгоритм +может потребовать значительных вычислительных ресурсов, что так же, как и в случае LR разбора, накладывает +дополнительные ограничения для практического применения. + + +На листинге \ref{lst:earley} представлен псевдокод алгоритма Эрли. + + +\begin{listing}[H] + \caption{Алгоритм Эрли} + \label{lst:earley} +\begin{algorithm}[H] + \begin{algorithmic}[1] + \Procedure{EarleyParse}{tokens, Grammar} + \State $n \gets$ длина(tokens) + \State Инициализировать массив \texttt{chart[0..n]}, где каждая ячейка содержит множество состояний + \State Добавить в \texttt{chart[0]} все состояния вида $(S \to \cdot \alpha, 0)$ для стартового символа $S$ + \For{$k \gets 0$ \textbf{to} $n$} + \Repeat + \ForAll{состояний $(A \to \alpha \cdot B \beta, j)$ в \texttt{chart[k]}} + \If{$B$ --- нетерминал} + \State \textbf{Predictor:} Для каждого правила $B \to \gamma$ добавить состояние $(B \to \cdot \gamma, k)$ в \texttt{chart[k]} + \EndIf + \EndFor + \Until{не появляются новые состояния в \texttt{chart[k]}} + + \ForAll{состояний $(A \to \alpha \cdot a \beta, j)$ в \texttt{chart[k]}} + \If{$a = tokens[k]$} + \State \textbf{Scanner:} Добавить состояние $(A \to \alpha a \cdot \beta, j)$ в \texttt{chart[k+1]} + \EndIf + \EndFor + + \Repeat + \ForAll{состояний $(B \to \gamma \cdot, j)$ в \texttt{chart[k]}} + \ForAll{состояний $(A \to \alpha \cdot B \beta, i)$ в \texttt{chart[j]}} + \State \textbf{Completer:} Добавить состояние $(A \to \alpha B \cdot \beta, i)$ в \texttt{chart[k]} + \EndFor + \EndFor + \Until{не появляются новые состояния в \texttt{chart[k]}} + \EndFor + \State \Return проверка наличия в \texttt{chart[n]} состояния $(S \to \gamma \cdot, 0)$ + \EndProcedure + \end{algorithmic} + \end{algorithm} +\end{listing} + + +\subsubsection{Предсказывающий разбор для LL(1) грамматик} + +\label{sec:ll1int} + +Предсказывающий разбор является одним из наиболее интуитивно понятных и простых методов синтаксического анализа. +Он применяется для грамматик, удовлетворяющих условию LL(1) – то есть таких, для которых по текущему входному +символу и состоянию анализатора можно однозначно определить, какое правило применять. + +Преимуществами данного метода являются простота в его реализации и высокая скорость работы и, благодаря +однозначному выбору следующего шага, обеспечивается высокая скорость разбора. + +На листинге \ref{lst:ll1} представлен псевдокод алгоритма предсказывающего ll1 разбора. + +\begin{listing} + \caption{Предсказывающий разбор для LL(1)-грамматик} + \label{lst:ll1} +\begin{algorithm}[H] + \begin{algorithmic}[1] + \Procedure{LL1Parse}{tokens, table, Start} + \State $stack \gets [\$, Start]$ \Comment{Знак \$ обозначает конец ввода} + \State $i \gets 0$ + \While{$stack$ не пуст} + \State $X \gets$ \textbf{top}$(stack)$ + \State $a \gets tokens[i]$ + \If{$X$ --- терминальный символ или \$} + \If{$X = a$} + \State \textbf{pop}($stack$) + \State $i \gets i + 1$ + \Else + \State \Return ошибка разбора + \EndIf + \Else + \If{table[$X, a$] определена как правило $X \to Y_1 Y_2 \dots Y_k$} + \State \textbf{pop}($stack$) + \State \textbf{push}($Y_k, Y_{k-1}, \dots, Y_1$) на $stack$ + \Else + \State \Return fail + \EndIf + \EndIf + \EndWhile + \State \Return ok + \EndProcedure + \end{algorithmic} +\end{algorithm} +\end{listing} + +\subsection{Семантический разбор} + +Семантический разбор\cite{wilhelm2013compiler}, или семантический анализ, является важным этапом компиляции, который следует +после синтаксического анализа. Его основная задача заключается в проверке корректности программы с +точки зрения смысловых правил языка, которые не могут быть выражены исключительно с помощью синтаксиса. + +К основным задачам семантического разбора относятся: + +\begin{itemize} + \item Проверка типов --- проверка совместимости типов данных в выражениях и операциях + \item Проверка объявления идентификаторов + \item Проверка областей видимости --- проверка, что , что переменные и другие идентификаторы используются в + рамках своих областей видимости, предотвращая возможные конфликты имён или попытки обращения к + несуществующим объектам + \item Проверка корректности использования операций --- например, проверка деления на 0 +\end{itemize} + +Семантический разбор обычно основывается на специальной символической таблице, которая формируется и +обновляется в процессе обработки исходного кода. Эта таблица содержит сведения о типах данных, +объявленных переменных, функциях, классах и других конструкциях языка. При анализе каждой новой +конструкции компилятор проверяет её корректность, используя информацию из таблицы символов, что +позволяет обнаруживать ошибки, не выявляемые на этапе синтаксического анализа. + +\subsection{Ассоциативность и приоритет операций на примере Bison} + +В системах генерации парсеров, таких как Bison\cite{bison}, одной из проблем является разрешение неоднозначностей, +возникающих при разборе арифметических и логических выражений, где один и тот же набор токенов может +соответствовать различным структурам дерева разбора. Для решения этой проблемы Bison предоставляет механизм +задания ассоциативности и приоритета операторов. + +В Bison приоритеты и ассоциативность операторов определяются с помощью директив, таких как +\verb|%left|, \verb|%right| и \verb|%nonassoc|. Эти директивы позволяют: + +\begin{itemize} + \item \textbf{Установить ассоциативность:} + \begin{itemize} + \item \verb|%left| определяет левую ассоциативность, что означает, что цепочка операторов будет + группироваться слева. Например, выражение \verb|a - b - c| интерпретируется как \verb|(a - b) - c|. + \item \verb|%right| задаёт правую ассоциативность, что приводит к группировке справа, как в случае + выражения \verb|a ^ b ^ c|, которое будет разобрано как \verb|a ^ (b ^ c)|. + \item \verb|%nonassoc| используется для операторов, для которых не допускается последовательное + применение без явного указания порядка, что помогает избегать конфликтов. + \end{itemize} + \item \textbf{Определить приоритет:} Операторы, объявленные позже, автоматически получают более высокий + приоритет, чем операторы, указанные ранее. Таким образом, если в грамматике присутствуют, например, + как операторы сложения и вычитания, так и операторы умножения и деления, то умножение и деление будут + иметь более высокий приоритет, что позволяет корректно интерпретировать выражение \verb|a + b * c| как + \verb|a + (b * c)|. + +\end{itemize} + +Пример объявления операторов в Bison может выглядеть следующим образом: + +\begin{verbatim} + %left '+' '-' + %left '*' '/' + %right '^' +\end{verbatim} + +В данном примере: +\begin{itemize} + \item Операторы \verb|+| и \verb|-| обладают одинаковой (левой) + ассоциативностью и более низким приоритетом. + \item Операторы \verb|*| и \verb|/| также левосторонние, + но имеют более высокий приоритет по сравнению с \verb|+| и \verb|-|. + \item Оператор \verb|^| обладает + наивысшим приоритетом и является правоассоциативным. +\end{itemize} + +\subsection{Технический обзор библиотеки parser\_edsl} + +parser\_edsl\cite{parsedsl} – это библиотека на Python\cite{Python}, предназначенная для описания грамматики языка и построения парсеров для них. + Основная идея заключается в использовании Embedded Domain Specific Language (EDSL) +для определения лексических доменов посредством регулярных выражений и синтаксиса грамматики в стиле, похожем +на Бэкусову нормальную форму (BNF). Такая реализация позволяет в исходном тексте программы задавать правила +языка в декларативном виде, что упрощает процесс построения компилятора для учебных и исследовательских целей. + +Основные компоненты библиотеки включают в себя: + +\begin{itemize} + \item Инструменты для описания грамматики --- такие классы, как \texttt{Terminal}, \texttt{NonTerminal}, позволяют легко и понятно описывать правила грамматики. + Также, для класса\texttt{NonTerminal} поддержана перегрузка оператора \texttt{|=}, которая позволяет не только описывать + правило, но и добавить семантическое действие(свертку), которое определяет, как будут + вычисляться атрибуты найденных элементов. + \item Класс \texttt{Lexer}, реализующий разбиение входного потока на лексемы. + \item Класс \texttt{Parser}, реализующий алгоритм LR-разбора. Библиотека строит LR(0)-автомат, реализует + функции closure и goto, а затем формирует таблицу разбора (представлена классом \texttt{ParsingTable}). + \item Для информирования пользователя о возникших ошибках при синтаксическом анализе реализован класс \texttt{ParseError}. + При обнаружении неожидаемого символа парсер генерирует исключение с подробным сообщением, в + котором указываются позиция ошибки и символы, которые ожидались на вход. +\end{itemize} + +Также, библиотека написана с объектно-ориентированным подходом, то есть структурирована в виде набора классов, +что способствует модульности и удобству расширения. Каждый класс содержит только свою функциональность +(например, лексический анализ, описание грамматики, синтаксический анализ и обработка ошибок). + +\section{Разработка расширений для библиотеки} + +\subsection{Планируемые изменения в библиотеку} + +Основная цель работы заключается в доработке функциональности библиотеки за счёт добавления поддержки лексических +ошибок, внедрения альтернативных режимов синтаксического разбора (предсказывающий анализ для LL(1)-грамматик +и алгоритм Эрли), а также реализации механизма задания приоритета и ассоциативности в стиле Bison. В данном разделе +рассмотрим подробно, что потребуется реализовать в каждом из пунктов технического задания. + +\subsubsection{Поддержка ошибки вычисления атрибутов} + +Аттрибуты у токенов --- важная часть в работе компилятора. Например, благодаря этому можно находить такие ошибки, как +деление на 0 и сообщать о них пользователю до выполнения программы. Такие ошибки можно находить и обрабатывать +на стадии семантического анализа, но что делать, если атрибут вычислить не удается? + +Например, у нас идет разбор +выражения семантического версионирования SemVer. Если проверку на формат и используемые символы можно сделать с помощью +регулярного выражения, то что делать, если, например, версия должна быть в определенном числовом диапазоне? То есть +вроде токен и распознался лексическим анализатором, но вычислить его атрибут не получится. В таком случае будет +крайне полезным выбросить исключение, говорящее об этом, которое парсер правильно обработает и пробросит наверх. + +Однако, сейчас в библиотеке есть два типа исключений: \texttt{LexerError} и \texttt{ParseError}, интерфейс которых +приведен на листинге \ref{lst:curerrros}. Как видно, их интерфейс не позволяет нативно передать информацию об ошибке +вычисления атрибута. + +\begin{listing}[H] + \caption{Интерфейс исключений \texttt{LexerError} и \texttt{ParseError}.} + \label{lst:curerrros} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/curerrros.py} +\end{listing} + +Следовательно, необходимо разработать новый класс ошибок \texttt{TokenAttributeError}, конструктор которого будет принимать +сообщение об ошибке и обрабатываться программой для получения пользователем понятной ошибки с указанием позиции, +где произошла исключительная ситуация. Программа должна будет обернуть исключение в LexerError и потом обернуть его +в ParseError исключение, которое уже должно будет обрабатываться пользователем. + +Также, для улучшения пользовательского +опыта необходимо предусмотреть в классе ParseError переопределение стандартного сообщения об ошибке, чтобы +сообщение для пользователя выглядело более лаконично и понятно. +Например, ошибка может выглядеть так: +\texttt{Ошибка (1, 4): Слишком большое значение: 300}. + +Для тестирования данной функциональности предлагается использовать библиотеку pytest, так как в ней есть все необходимые +инструменты для проверки, что программа выбрасывает исключение и что именно это за исключение. + +\subsubsection{Разработка предсказывающего разбора} + +Принцип работы и особенности данного алгоритма разбора были описаны в разделе \ref{sec:ll1int}. Попробуем оценить, +как лучше этот алгоритм реализовать на основе уже существующей кодовой базы библиотеки, чтобы интегрировать его +максимально удобно и не вызывая поломок в других частях библиотеки. + +В текущей версии \texttt{parser\_edsl} уже реализована хорошая структура кода, отделяющая различные этапы работы +библиотеки. В реализации нового алгоритма стоит выделить следующие компоненты: + +\begin{itemize} + \item \textbf{ParseTreeNode} --- узел дерева разбора для LL(1). Узел должен в себе хранить терминал/нетерминал + и информацию о них(Token --- в случае терминала и список дочерних узлов в случае нетерминала). Так как класс будет + содержать только данные и не обладать никакой сложной логикой, для упрощения разработки стоит использовать для его + описания \texttt{dataclass} декоратор из библиотеки \texttt{dataclasses}. + \item \textbf{PredictiveParsingTable} --- Таблица предсказывающего анализа. Она должна в себе для пар вида + (Нетерминал, Терминал) хранить правило переписывания и функцию свертки, если таковая была задана. Очевидно, + что класс таблицы должен обладать методами построения \texttt{Follow} множеств и построения самой таблицы. + Оба метода, конечно же, должны будут вызываться в конструкторе таблицы. Также, для упрощения отладки, стоит + реализовать метод для красивого текстового вывода таблицы. + \item Дополнительные методы для класса \texttt{Parser}: класс нужно будет расширить методами для проведения ll1 разбора, + построения таблицы и вычисления атрибутов токенов. +\end{itemize} + +Также необходимо научить алгоритм работать с объектами этой библиотеки, классами для терминалов и нетерминалов, а ещё +научить алгоритм правильно обрабатывать и выбрасывать библиотечные ошибки, такие как \texttt{LexerError} и \texttt{ParseError}. + +\subsubsection{Разработка разбора по алгоритму Эрли} + +Принцип работы и особенности данного алгоритма разбора были описаны в разделе \ref{sec:earleyint}. Аналогично +алгоритму предсказывающего разбора, выделим основные компоненты в реализации и особенности, которые нужно будет учесть +при реализации. + +В реализации данного алгоритма стоит выделить следующие компоненты: + +\begin{itemize} + \item Класс \texttt{EarleyState}, который представляет собой состояние в алгоритме Эрли. Он должен хранить в себе + правило, по которому идет разбор, позицию в этом правиле, позицию в исходном коде, а также должен хранить + координаты и атрибуты для завершенных состояний. Для удобства работы с классом стоит воспользоваться декоратором + \texttt{dataclass} с опцией \texttt{(frozen=True)}, которая позволяет вычислять хэш для объектов класса. + \item Также необходимо реализовать сам алгоритм разбора со всеми его вспомогательными методами, такими как + \texttt{predict}, \texttt{scan}, \texttt{complete}. Возможно несколько вариантов реализации: + добавить все новые методы в исходный класс или реализовать отдельный класс \texttt{EarleyParser}, который будет + выполнять разбор. +\end{itemize} + +Вне зависимости от выбранного пути реализации алгоритма, нужно будет не забыть добавить возможность вызывать разбор +методом Эрли из старого класса \texttt{Parser}. + +\subsubsection{Поддержка ассоциативности и приоритета операций в стиле Bison} + +Механизм задания ассоциативности и приоритета операторов, реализованный в Bison, позволяет интуитивно и +эффективно управлять группировкой выражений, избегая конфликтов при построении таблицы синтаксического анализа. + +В реализации данной функциональности в библиотеке \texttt{parser\_edsl} целесообразно выделить следующие компоненты: + +\begin{itemize} + \item Для представления информации о приоритете и ассоциативности операторов можно добавить базовый класс + \texttt{Precedence} и его наследники: + \begin{itemize} + \item \texttt{Left} для левой ассоциативности, + \item \texttt{Right} для правой ассоциативности, + \item \texttt{NonAssoc} для операторов без ассоциативности. + \end{itemize} + Каждый из этих классов будет хранить уровень приоритета (\texttt{level}) и строковое обозначение + ассоциативности (\texttt{associativity}), что позволит в дальнейшем сравнивать приоритеты операторов. + \item В классе \texttt{NonTerminal} необходимо будет дополнительно перегрузить оператор \verb|__ior__| для добавления + возможности описания правил с операциями с ассоциативностью и приоритетом. + \item В основном алгоритме разбора, описанном в классе \texttt{Parser}, при обнаружении конфликта + shift/reduce будет необходимо: + \begin{itemize} + \item Из набора возможных действий выделять операция сдвига (\texttt{Shift}) и операция редукции + (\texttt{Reduce}). + \item Если для правила редукции задана информация о приоритете (объект \texttt{Precedence}), + то сравнивать уровень приоритета токена, полученного от лексера, и уровень приоритета правила. + \item Приоритет токена выше должен приводить к выполнению сдвига, а ниже --- к редукции. + \item При равенстве приоритетов должна учитываться ассоциативность: для левых операторов выбирается редукция, + для правых --- сдвиг; отсутствие ассоциативности должно приводить к генерации ошибки. + \end{itemize} +\end{itemize} + +\subsection{Анализ существующей архитектуры и оценка изменений} + +Перед внесением изменений необходимо тщательно изучить текущую архитектуру библиотеки \texttt{parser\_edsl}. +Планируется провести следующий анализ: +\begin{itemize} + \item Оценка текущей модульности системы, что позволит в дальнейшем определить потенциальные места для внедрения новой + функциональности и определить, что в библиотеке уже имеется, а что придется реализовывать. + \item Оценка риска потенциального ухудшения качества работы библиотеки, как по корректности работы, так и по + времени выполнения. Пользователь должен обладать возможностью легко использовать новые сценарии, + но при этом важно сохранить обратную совместимость со старой + версией библиотеки. + \item Оценка наилучших мест в библиотеке для интеграции новой функциональности. Важно спланировать добавление + новой функциональности так, чтобы код остался как можно более чистым и без ненужного дублирования каких-то + фрагментов. +\end{itemize} + +\subsubsection{Оценка модульности системы} + +В процессе анализа исходного кода библиотеки было установлено, что структура проекта соответствует принципам +объектно-ориентированного программирования. Основные компоненты реализованы следующим образом: + +\begin{itemize} + \item Лексический анализ --- классы \texttt{Terminal}, \texttt{LiteralTerminal} и \texttt{ErrorTerminal} содержат всю логику, + связанную с распознаванием лексем на основе регулярных выражений и точного сопоставления литералов. + Эта часть системы достаточно изолирована, что позволяет в будущем добавлять обработку дополнительных исключительных + ситуаций (например, дополнительные лексические ошибки) без глобальных изменений. + \item Описание грамматики --- класс \texttt{NonTerminal} и связанные с ним механизмы (перегрузка оператора |= для + добавления правил, использование семантических действий через \texttt{ExAction}) реализуют декларативное + задание грамматики, предоставляя возможность добавлять новую функциональность не нарушая работу парсеров по правилам, + написанным в старом формате. + \item Класс \texttt{Parser} обладает методом \texttt{parse}, который умеет выполнять только LR(0) разбор. + Однако, возможно есть способ переписать функцию так, чтобы она позволяла использовать разные методы разбора, + При этом по умолчанию выполняя LR(0) разбор. +\end{itemize} + +\subsubsection{Поиск наилучших мест для интеграции новой функциональности} + +После анализа текущего кода библиотеки можно составить следующий план внедрения новой функциональности: + +\begin{itemize} + \item Для поддержки ошибок вычисления атрибутов токенов наиболее подходящим местом является расширение классов, + отвечающих за вычисление атрибутов токенов (например, метод \texttt{match} в классе \texttt{Terminal}). + Добавление обработки исключительных ситуаций в классе \texttt{Terminal} позволит обойтись изменениями в + пределах лексического анализатора, не затрагивая модуль синтаксического разбора. + \item Класс \texttt{Parser} является оптимальным местом для добавления новых методов разбора. Можно не реализовать + дополнительные классы Parser, усложняя код. Важно сохранить общий интерфейс для парсера, чтобы + пользователи могли выбирать нужный режим без изменения существующего кода. + \item Добавление приоритета и ассоциативности операций потребует доработки перегрузки оператора \texttt{|=} + у класса \texttt{NonTerminal}, а также в алгоритме разбора нужно будет учитывать ассоциативность операций при + shift-reduce конфликтах. +\end{itemize} + +\subsection{Разработка тестирования библиотеки} + +Тщательное тестирование является неотъемлемой частью процесса разработки библиотеки, обеспечивая её надёжность, +корректность и устойчивость при внесении изменений. В рамках разработки тестирования для \texttt{parser\_edsl} +планируется реализация следующих уровней проверки: + +\begin{itemize} + \item Написание unit тестов. Unit тесты позволяют тестировать различные сценарии: + \begin{itemize} + \item Проверка, что уже написанный код работает, также, как и до этого + \item Написание тестов на ещё не реализованные части библиотеки, чтобы ускорить процесс реализации + благодаря автоматизированному тестированию. + \end{itemize} + \item Все unit тесты будут встроены в процесс непрерывной интеграции (CI), что позволит автоматически + запускать набор тестов при каждом изменении кода. Это обеспечит своевременное обнаружение ошибок и + повысит качество конечного кода. +\end{itemize} + +Тестами стоит покрыть уже существующий алгоритм LR(0) разбора, а так же написать тесты под каждую новую функциональность, +добавляемую в библиотеку. + +\section{Реализация расширений для библиотеки} + +Для выполнения поставленной задачи необходимо реализовать возможность выбрасывания ошибок вычисления атрибутов, +новые алгоритмы разбора, а также ассоциативность и приоритет операций. + +\subsection{Выбор инструментов разработки} + +Вся разработка велась на языке Python, так как на нем реализована библиотека \texttt{parser\_edsl}, а переписывание +исходной библиотеки на другой язык нецелесообразно. Для разработки использовались следующие, встроенные в +язык библиотеки: + +\begin{itemize} + \item abc\cite{abc} --- библиотека предоставляет возможность создания абстрактных базовых классов. Это позволяет + создавать интерфейсы с обязательной реализацией определённых методов в + наследниках, что помогает структурировать и стандартизировать код. + \item collections\cite{collections} --- библиотека содержит специализированные типы данных контейнеров, такие как + \texttt{namedtuple}, \texttt{deque}, \texttt{Counter} и другие. Она упрощает работу с коллекциями и + предоставляет удобные инструменты для организации данных. + \item dataclasses\cite{dataclasses} --- библиотека облегчает создание классов, предназначенных для хранения данных. С помощью + декоратора \texttt{@dataclass} автоматически генерируются методы \texttt{\_\_init\_\_}, \texttt{\_\_repr\_\_}, + \texttt{\_\_eq\_\_} и другие, что сокращает количество шаблонного кода и повышает читаемость. +\end{itemize} + +Для тестирования проекта использовалась библиотека pytest, обладающая всей необходимой функциональностью, включая +возможность проверять, что программа выбрасывает конкретные типы исключений. Чтобы автоматизировать тестирование, +был настроен автозапуск тестов в Github Actions\cite{CI}. + +\subsection{Реализация расширенной функциональности} + +Данный раздел содержит подробное описание изменений в библиотеке, связанных с добавлением новой функциональности. + +\subsubsection{Реализация поддержки ошибки вычисления атрибутов} + +В результате реализации был добавлен новый тип исключения \texttt{TokenAttributeError}, который наследуется от +базового класса ошибок библиотеки. + +При вызове метода \texttt{match} класса \texttt{Terminal} происходит попытка вычисления атрибута с помощью +заданной функции. Если функция генерирует исключение \texttt{TokenAttributeError}, оно перехватывается, +и вызывается функция \texttt{pos\_from\_offset} для вычисления текущей позиции в исходном тексте. +Затем генерируется исключение \texttt{LexerError} с информацией о месте возникновения проблемы и с +соответствующим сообщением. Данный механизм позволяет локализовать и корректно обрабатывать ошибки на этапе +лексического анализа, а в дальнейшем они транслируются в синтаксические ошибки, что облегчает их перехват +и диагностику. Детали реализации представлены на листинге \ref{lst:error_impl}. + +\subsubsection{Реализация предсказывающего разбора} + +Для реализации предсказывающего разбора (LL(1)) в библиотеке разработан отдельный механизм, основанный на +построении таблицы предсказывающего разбора. + +Для грамматики создаётся экземпляр класса \texttt{PredictiveParsingTable}, который вычисляет follow-множества +для всех нетерминалов и заполняет таблицу разбора. При возникновении конфликтов (если для одного и того же +нетерминала и терминала возможно более одного правила) генерируется исключение +\texttt{PredictiveTableConflictError}. Детали реализации представлены на листингах \ref{lst:ll1_impl_1} +- \ref{lst:ll1_impl_3}. + +Метод \texttt{parse\_ll1} класса \texttt{Parser} реализует предсказывающий разбор с использованием стека, +содержащего узлы синтаксического дерева (типа \texttt{ParseTreeNode}). При обработке входного потока токенов +алгоритм поочерёдно извлекает символ верхушки стека: +\begin{itemize} + \item Если символ является терминалом, он сравнивается с текущим токеном. При совпадении токен добавляется + к узлу, и алгоритм переходит к следующему токену. + \item Если символ является нетерминалом, производится обращение к LL(1) таблице для выбора соответствующей + продукции, после чего нетерминал заменяется на правую часть выбранного правила. +\end{itemize} +После завершения разбора синтаксическое дерево обходится с помощью рекурсивного обхода +(методом \texttt{\_evaluate\_parse\_tree}), что позволяет вычислить итоговый семантический атрибут. + +\subsubsection{Реализация разбора по алгоритму Эрли} + +Для поддержки более сложных и неоднозначных грамматик реализован разбор по алгоритму Эрли. + +Класс \texttt{EarleyState} представляет отдельное состояние алгоритма. Каждый объект этого класса хранит +правило разбора, позицию точки (dot), начальную и конечную позиции в потоке токенов, а также +накопленные атрибуты и координаты. Благодаря декоратору \texttt{@dataclass(frozen=True)} объекты состояний +могут быть использованы в качестве элементов множества. Детали реализации представлены на листингах +\ref{lst:earley_impl_1} --- \ref{lst:earley_impl_3}. + +Класс \texttt{EarleyParser} реализует три ключевые операции: +\begin{itemize} + \item \texttt{predict} --- предсказывает возможные правила для нетерминалов, ожидаемых в текущем состоянии. + \item \texttt{scan} --- осуществляет сопоставление терминальных символов с текущим токеном. + \item \texttt{complete} --- завершает обработку состояний, если правило разобрано + полностью, и добавляет полученные результаты в предшествующие состояния. +\end{itemize} +Эти операции используются для поэтапного построения \emph{chart} (таблицы состояний), где на +каждой позиции входного потока фиксируются все возможные состояния разбора. Итоговая семантика +возвращается, если найдено единственное корректное состояние, начинающееся с начального символа. + +\subsubsection{Реализация поддержки ассоциативности и приоритета операций в стиле Bison} + +Для корректного разрешения конфликтов при разборе выражений реализован механизм задания ассоциативности и +приоритета операций, аналогичный функциональности, предлагаемой Bison. + +Введён базовый класс \texttt{Precedence} и его наследники: \texttt{Left}, \texttt{Right} и \texttt{NonAssoc}. +Каждый из этих классов хранит уровень приоритета (\texttt{level}) и тип ассоциативности (\texttt{associativity}), +что позволяет однозначно определить порядок применения операторов. Детали реализации представлены на листинге +\ref{lst:bison_impl}. + +В классе \texttt{NonTerminal} изменена перегрузка оператора \texttt{\_\_ior\_\_} для добавления правил +грамматики с указанием приоритета операцией. +Если правило передаётся в виде кортежа, где предпоследний элемент является объектом типа \texttt{Precedence}, +то для соответствующего терминала устанавливается приоритет, а информация о приоритете сохраняется вместе с +продукцией. Это позволяет в дальнейшем использовать эти данные при разрешении конфликтов. + +В методе \texttt{parse} класса \texttt{Parser} при обнаружении конфликта между операциями сдвига и редукции +происходит следующее: +\begin{itemize} + \item Из набора возможных действий выделяются операция сдвига и операция редукции. + \item Если для продукции редукции задана информация о приоритете, сравниваются приоритеты токена и + продукции. Приоритет токена, вычисляемый по значению \texttt{priority} терминала, используется для + определения, какую операцию выполнить. + \item При равенстве уровней приоритета используется ассоциативность: для левых операторов выбирается + редукция, для правых --- сдвиг; при отсутствии ассоциативности генерируется ошибка. +\end{itemize} + + +\subsection{Руководство пользователя} + +\textbf{1. Установка и подключение библиотеки} + +Библиотека написана на Python и не требует сложной установки. Для начала работы достаточно добавить файлы +библиотеки в проект и импортировать необходимые классы и функции, как представлено на листинге \ref{lst:guide1}. + +\begin{listing}[H] + \caption{Подключение библиотеки} + \label{lst:guide1} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/guide/1.py} +\end{listing} + +\textbf{2. Определение лексических доменов} + +Лексический анализ основан на описании терминалов. Для этого используются классы \texttt{Terminal} и +\texttt{LiteralTerminal}. Можно задать регулярное выражение и функцию преобразования для каждого терминала. +Пример представлен на листинге \ref{lst:guide2}. Также имеется возможность задавать приоритет терминалов. + +\begin{listing}[H] + \caption{Описание терминалов} + \label{lst:guide2} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/guide/2.py} +\end{listing} + +\textbf{3. Описание грамматики} + +Грамматика задаётся через объекты класса \texttt{NonTerminal}. Правила грамматики добавляются с помощью +оператора \verb||=|, что позволяет декларативно описывать продукции. При необходимости можно включить +семантические действия и информацию о приоритете операций, используя объекты классов +\texttt{Left}, \texttt{Right} или \texttt{NonAssoc}. Пример представлен на листинге \ref{lst:guide3}. + +\begin{listing}[H] + \caption{Описание нетерминалов} + \label{lst:guide3} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/guide/3.py} +\end{listing} + +\textbf{4. Инициализация парсера и выбор алгоритма разбора} + +После определения грамматики создаётся объект класса \texttt{Parser}. По умолчанию используется алгоритм LALR(1), +однако библиотека поддерживает и альтернативные режимы, такие как предсказывающий разбор (LL(1)) и +алгоритм Эрли. Для использования альтернативных алгоритмов доступны методы \texttt{parse\_ll1} и +\texttt{parse\_earley}. Пример представлен на листинге \ref{lst:guide4}. + +\begin{listing}[H] + \caption{Создание парсера и выполнение разбора} + \label{lst:guide4} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/guide/4.py} +\end{listing} + +\textbf{5. Обработка ошибок} + +При разборе возможны ошибки лексического или синтаксического анализа. Библиотека генерирует +исключения типа \texttt{ParseError} с подробной информацией о позиции ошибки и ожидаемых символах. +Это позволяет быстро находить и устранять проблемы в описании грамматики или входном тексте. + +\textbf{6. Тестирование и отладка} + +Для проверки корректности работы парсера рекомендуется использовать встроенные функции для печати таблицы разбора +(\texttt{print\_table}) и предсказывающей таблицы (\texttt{stringify\_ll1\_table}). + +\section{Тестирование} + +\subsection{Подготовка тестов} + +Для тестирования библиотеки были подготовлены следующие наборы unit тестов: + +\begin{itemize} + \item Тесты для LALR разбора. + \item Тесты для выбрасывания \texttt{TokenAttributeError} исключений + \item Тесты для разбора по алгоритму предсказывающего анализатора. + \item Тесты для алгоритма Эрли + \item Тесты на арифметические выражения с разной ассоциативностью и различным приоритетом. +\end{itemize} + +\subsection{Проведение тестирования} + +Тестирование проводилось путем запуска pytest и проверки на успешное выполнение всех тестов. +Чтобы процесс тестирования был более прозрачным и автоматическим, их запуск был добавлен в Github Actions репозитория +с курсовой работой. Конфигурация github actions представлена на листинге \ref{lst:ghact}. + +\begin{listing}[H] + \caption{Создание парсера и выполнение разбора} + \label{lst:ghact} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/a.yaml} +\end{listing} + +В результате удалось добиться успешного выполнения 100\% тестов. + +\anonsection{Заключение} + +В ходе выполнения данной курсовой работы была проведен анализ и оценка кодовой базы и архитектуры библиотеки +\texttt{parser\_edsl}, что позволило разработать добавление расширенной функциональности в библиотеку и в последствии +успешно реализовать её. + +В результате работы были реализованы следующие расширения функциональности: + +\begin{itemize} + \item Добавлен механизм перехвата и обработки исключений, возникающих при вычислении атрибутов токенов, + что позволяет обнаруживать ошибки на этапе лексического анализа и улучшает диагностику проблем. + \item Разработан и интегрирован модуль, позволяющий строить предсказывающую таблицу и выполнять разбор + с использованием алгоритма предсказывающего разбора. Это обеспечивает возможность эффективной работы с + грамматиками, удовлетворяющими требованию LL(1), и повышает гибкость библиотеки. + \item Внедрение алгоритма Эрли позволило расширить область применения библиотеки, + обеспечив поддержку более сложных и неоднозначных грамматик. + \item Реализован механизм задания приоритетов и ассоциативности операторов, что позволяет автоматически + разрешать конфликтные ситуации типа shift/reduce и корректно обрабатывать выражения, + содержащие операторы с различными уровнями приоритета. +\end{itemize} + +Несмотря на достигнутые успехи, остаётся ряд направлений для дальнейшего улучшения библиотеки, включая оптимизацию +алгоритмов, добавление других алгоритмов разбора, расширение тестового покрытия и добавление взаимодействия +с другими инструментами анализа. В целом, проделанная работа является важным шагом в развитии инструментов +для конструирования компиляторов и может служить основой для будущих исследований и улучшений в данной области. + +\renewcommand\refname{СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ} +% Список литературы +\clearpage +%\bibliographystyle{ugost2008s} %utf8gost71u.bst} %utf8gost705u} %gost2008s} +{\catcode`"\active\def"{\relax} +\addcontentsline{toc}{section}{\protect\numberline{}\refname}% +%\bibliography{biblio} %здесь ничего не меняем, кроме, возможно, имени bib-файла +\printbibliography +} + +\newpage + +\anonsection{ПРИЛОЖЕНИЕ А} +\vspace{-30pt} + +\begin{listing}[H] + \caption{Реализация поддержки ошибки вычисления токенов.} + \label{lst:error_impl} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/error.py} +\end{listing} + +\begin{listing}[H] + \caption{Реализация алгоритма предсказывающего разбора.} + \label{lst:ll1_impl_1} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/ll1_1.py} +\end{listing} + +\begin{listing}[H] + \caption{Класс PredictiveParsingTable.} + \label{lst:ll1_impl_2} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/ll1_2.py} +\end{listing} + +\begin{listing}[H] + \caption{Класс PredictiveParsingTable. Продолжение.} + \label{lst:ll1_impl_3} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/ll1_3.py} +\end{listing} + +\begin{listing}[H] + \caption{Реализация алгоритма Эрли.} + \label{lst:earley_impl_1} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/earley_1.py} +\end{listing} + +\begin{listing}[H] + \caption{Реализация алгоритма Эрли.} + \label{lst:earley_impl_2} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/earley_2.py} +\end{listing} + +\begin{listing}[H] + \caption{Реализация алгоритма Эрли.} + \label{lst:earley_impl_3} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/earley_3.py} +\end{listing} + + +\begin{listing}[H] + \caption{Реализация ассоциативности и приоритета операций.} + \label{lst:bison_impl} + \inputminted[style=bw, frame=single,fontsize = \footnotesize, linenos=false, xleftmargin = 1.5em]{python}{./listings/bison.py} +\end{listing} + +\end{document} diff --git a/coursework_text.pdf "b/\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\236\320\261\321\203\321\211\320\260\321\200\320\276\320\262\320\276\320\271 2022.pdf" similarity index 100% rename from coursework_text.pdf rename to "\320\227\320\260\320\277\320\270\321\201\320\272\320\260 \320\236\320\261\321\203\321\211\320\260\321\200\320\276\320\262\320\276\320\271 2022.pdf"