From 55e5c4e0def5fd01ef28b013c6b8692d8661a966 Mon Sep 17 00:00:00 2001 From: ascendforever Date: Wed, 7 May 2025 11:20:29 -0400 Subject: [PATCH] Code from fall 2024 --- main.py | 348 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..ffd8839 --- /dev/null +++ b/main.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 + + + +import abc +import collections +import io +import itertools +import re +import sys +import types + + + +# for formatting +UTF8_BOX_DRAWING_CHARACTERS = dict( + bar='│', + bar_down='┬', + bar_up='┴', + bar_left='┤', + bar_right='├', + corner_bottom_left='└', + corner_bottom_right='┘', + corner_top_left='┌', + corner_top_right='┐', + cross='┼', + hr='─', +) +ASCII_BOX_DRAWING_CHARACTERS = dict( + bar='|', + bar_down='+', + bar_up='+', + bar_left='+', + bar_right='+', + corner_bottom_left='+', + corner_bottom_right='+', + corner_top_left='+', + corner_top_right='+', + cross='+', + hr='-', +) + + + +class DPDA(abc.ABC): + """ + Deterministic Push-Down Automata + """ + + + # needs overwriting + DELTA = ... + START_STATE = ... + END_STATE = ... + + def __init_subclass__(cls, **kwargs): + """Set up variables that vary betweens DPDAs""" + super().__init_subclass__(**kwargs) + if not hasattr(cls, 'DELTA'): + raise TypeError("DELTA must be defined in subclass") + for l in ('START', 'END'): + if not hasattr(cls, f'{l}_STATE'): + raise TypeError(f"{l}_STATE must be defined in subclass") + cls.R_RULES = r_rules = frozenset(itertools.chain.from_iterable((t[3] for t in d.values() if t[3]) for d in cls.DELTA.values())) + cls.R_RULE_USED_FORMAT_WIDTH = max(len('R'), max(map(len, r_rules))) + cls.DELTA_RULE_COUNT = drc = sum(map(len, cls.DELTA.values())) + cls.DELTA_RULE_USED_FORMAT_WIDTH = max(len('Delta'), len(str(drc))) + cls.STATES = states = frozenset(itertools.chain((cls.START_STATE,cls.END_STATE), cls.DELTA.keys())) + cls.STATE_FORMAT_WIDTH = max(len('State'), max(map(len, states))) + + + def __init__(self): + self.reset() + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__}() " + f"step={self.step} state={self.state} input={''.join(self.input)} stack={''.join(self.stack)} " + f"delta_rule_used={self.delta_rule_used!r} r_rule_used={self.r_rule_used!r}>" + ) + + def reset(self) -> None: + self.step:int = -1 + self.state = self.__class__.START_STATE + self.input = None + self.stack = None + self.delta_rule_used = None + self.r_rule_used = None + + + @property + def stack(self) -> collections.deque[str]: + return self._stack + @stack.setter + def stack(self, value): + self._stack = collections.deque(value) if value else collections.deque() + + @property + def input(self) -> collections.deque[str]: + return self._input + @input.setter + def input(self, value) -> None: + self._input = value = collections.deque(value) if value else collections.deque() + self._stack_format_width = max(len('Stack'), round((len(value)-1)/2) + 2) + self._input_format_width = max(len('Unread input'), len(value)) + + @property + def stack_format_width(self) -> int: + return self._stack_format_width + @property + def input_format_width(self) -> int: + return self._input_format_width + @property + def r_rule_used_format_width(self) -> int: + return self.__class__.R_RULE_USED_FORMAT_WIDTH + @property + def delta_rule_used_format_width(self) -> int: + return self.__class__.DELTA_RULE_USED_FORMAT_WIDTH + @property + def state_format_width(self) -> int: + return self.__class__.STATE_FORMAT_WIDTH + + @property + def state(self) -> str: + return self._state + @state.setter + def state(self, value) -> None: + if value not in self.__class__.STATES: + raise ValueError(f"Invalid state: {value!r}; possible states: {', '.join(map(str, self.__class__.STATES))}") + self._state = value + + @property + def delta_rule_used(self) -> None|str: + return self._delta_rule_used + @delta_rule_used.setter + def delta_rule_used(self, value) -> None: + if value is not None and not 1 <= value <= self.__class__.DELTA_RULE_COUNT: + raise ValueError(f"Invalid delta rule: {value!r}") + self._delta_rule_used = value + + @property + def r_rule_used(self) -> None|str: + return self._r_rule_used + @r_rule_used.setter + def r_rule_used(self, value) -> None: + if value is not None and value not in self.__class__.R_RULES: + raise ValueError(f"Invalid r rule: {value!r}") + self._r_rule_used = value + + + def process(self, + input:str, + output_file:None|io.TextIOBase=None, + *, + box_drawing_characters=ASCII_BOX_DRAWING_CHARACTERS + ) -> bool: + """Returns success or failure""" + self.input = collections.deque(input) + self.step = -1 + bdc = box_drawing_characters + + result = None + + for i in itertools.count(): + self.step = i + if output_file: + if i == 0: + print(self.table_output_start(bdc), file=output_file) + print(self.table_output_current_step(bdc), file=output_file) + + if self.state == self.__class__.END_STATE: + result = True + break + + # get current information + input_front = self.input[0] if self.input else None + stack_top = self.stack[0] if self.stack else None + + # determine what needs to be done + for (take_input,take_stack), (new_state,add_to_stack,new_delta_rule_used,new_r_rule_used) in self.__class__.DELTA[self.state].items(): + if (not take_input or take_input == input_front) and (not take_stack or take_stack == stack_top): + break # compatible choice + else: # no match found + result = False + break + + # perform appropriate actions + if take_input: self.input.popleft() + if take_stack: self.stack.popleft() + self.state = new_state + self.delta_rule_used = new_delta_rule_used + self.r_rule_used = new_r_rule_used + if add_to_stack: + self.stack.extendleft(reversed(add_to_stack)) + + if output_file: + print(self.table_output_end(bdc), file=output_file) + return result + + + # overly complex formatting but we need it to be nice for the presentation! + def table_output_start(self, bdc=UTF8_BOX_DRAWING_CHARACTERS) -> str: + table_title = self.fmt_step('Step', 'State', 'Unread input', 'Stack', 'Delta', 'R', bdc=bdc) + len_left = table_title.find('Delta') - 3 + len_right = len(table_title) - table_title.find('Delta') + title = f" {bdc['bar']} ".join(( + f"{bdc['bar']} {'DPDA Processing State':^{len_left-2}}", + f"{'Rules used':^{len_right-2}} {bdc['bar']}", + )) + i = title[1:].find(bdc['bar'])+1 # this is where the divider line is for the left and right + first_line = self.fmt_step(join=bdc['hr'], bdc=bdc, left=bdc['corner_top_left'], right=bdc['corner_top_right']).replace(' ', bdc['hr']) + second_line = self.fmt_step(join=bdc['bar_down'], bdc=bdc, left=bdc['bar_right'], right=bdc['bar_left'] ).replace(' ', bdc['hr']) + first_line = first_line[:i] + bdc['bar_down'] + first_line[i+1:] + second_line = second_line[:i] + bdc['cross'] + second_line[i+1:] + third_line = second_line.replace(bdc['bar_down'], bdc['cross']) + return '\n'.join(( + first_line, + title, + second_line, + table_title, + third_line + )) + def table_output_current_step(self, bdc=UTF8_BOX_DRAWING_CHARACTERS) -> str: + return self.fmt_step( + self.step, self.state, + (''.join(self.input) or 'e'), (''.join(self.stack) or 'e'), + (self.delta_rule_used or ''), (self.r_rule_used or ''), + bdc=bdc + ) + def table_output_end(self, bdc=UTF8_BOX_DRAWING_CHARACTERS) -> str: + return self.fmt_step(join=bdc['bar_up'], bdc=bdc, left=bdc['corner_bottom_left'], right=bdc['corner_bottom_right']).replace(' ', bdc['hr']) + + @property + def table_output_width(self) -> int: + try: + return self._table_output_width + except AttributeError: + self._table_output_width = v = len(self.fmt_step()) + return v + + def fmt_step(self, + step:str='', state:str='', + input:str='', stack:str='', + dru:str='', rru:str='', + *, left:None|str=None, right:None|str=None, + join:None|str=None, bdc=ASCII_BOX_DRAWING_CHARACTERS + ) -> str: + return (f" {bdc['bar'] if join is None else join} ").join(( + f"{left or bdc['bar']} {step:>4}", + f"{state:<{self.state_format_width}}", + f"{input:<{self.input_format_width}}", + f"{stack:>{self.stack_format_width}}", + f"{dru:>{self.delta_rule_used_format_width}}", + f"{rru:<{self.r_rule_used_format_width}} {right or bdc['bar']}", + )) + + + +class ProjectDPDA(DPDA): + """ + Deterministic pushdown automata accepting L = { a^nb^n | n >= 1 } + """ + + # Structure: + # { + # state: { + # (take_input, take_stack): (new_state, push_stack, delta_rule_number, r_rule_used) + # } + # } + DELTA = { + 'p': { + (None, None): ('q' , 'S' , 1, None), + }, + 'q': { + ('a' , None): ('qa', None , 2, None), + ('b' , None): ('qb', None , 4, None), + ('$' , None): ('q$', None , 6, None), + }, + 'qa': { + (None, 'a' ): ('q' , None , 3, None), + (None, 'S' ): ('qa', 'aSb', 7, 'S -> aSb'), + }, + 'qb': { + (None, 'b' ): ('q' , None , 5, None), + (None, 'S' ): ('qb', None , 8, 'S -> e'), + }, + } + START_STATE:str = 'p' + END_STATE:str = 'q$' + + + +def main() -> int: + args = sys.argv[1:] + if not args or any(v in ('-h', '--help') for v in sys.argv[1:]): + import textwrap + import os + file = os.path.basename(__file__) + print(textwrap.dedent( + f""" + Process an input of language L = {{ a^nb^n$ | n >= 0 }} by deterministic pushdown automata + + Usage: {file} [input] + + Arguments: + -h / --help : Help message + --ascii : Disable utf-8 table formatting + input : Input string or a value for n + For example: `{file} aaabbb$` is the same as `{file} 3` + """ + ).strip()) + return 0 + + ascii = False + for v in args: + if v == '--ascii': + ascii = True + break + if ascii: + args.remove('--ascii') + + s = args[0] + + try: + n = int(s) + except ValueError: + pass + else: + s = 'a'*n + 'b'*n + '$' + + print(f"Processing {s}") + result = ProjectDPDA().process( + s, + sys.stdout, + box_drawing_characters=ASCII_BOX_DRAWING_CHARACTERS if ascii else UTF8_BOX_DRAWING_CHARACTERS + ) + if result: + print("Success") + return 0 + else: + print("Failure") + return 1 + + + +if __name__ == '__main__': + ec = main() or 0 + if not ec: + sys.exit(ec)