dpda/main.py

348 lines
11 KiB
Python

#!/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)