348 lines
11 KiB
Python
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)
|