Source code for mutwo.common_generators.chomksy
"""Algorithms which are related to US-American linguist N. Chomsky."""
import dataclasses
import functools
import operator
import typing
import treelib
from mutwo import core_utilities
__all__ = ("NonTerminal", "Terminal", "ContextFreeGrammarRule", "ContextFreeGrammar")
[docs]class NonTerminal(object):
"""Can be used as a Mixin to define context-free grammar."""
[docs]class Terminal(object):
"""Can be used as a Mixin to define context-free grammar."""
[docs]@dataclasses.dataclass
class ContextFreeGrammarRule(object):
"""Describe a context_free_grammar_rule for a :class:`ContextFreeGrammar`"""
left_side: NonTerminal
right_side: tuple[typing.Union[NonTerminal, Terminal], ...]
[docs]class ContextFreeGrammar(object):
"""Describe a context-free grammar and resolve non-terminals
:param context_free_grammar_rule_sequence: A sequence of :class:`ContextFreeGrammarRule` objects. It is
allowed to provide multiple context_free_grammar_rules with the same :attribute:`left_side`.
:type context_free_grammar_rule_sequence: typing.Sequence[ContextFreeGrammarRule]
This is a very reduced implementation of a context-free grammar
which only provides the most basic functions. It is not made for
the purpose of parsing text but rather as a technique to generate
algorithmic data (for the sake of art creation). Therefore it is
all about the resolution of start objects to variants of this start.
"""
def __init__(
self,
context_free_grammar_rule_sequence: typing.Sequence[ContextFreeGrammarRule],
):
non_terminal_list = []
for context_free_grammar_rule in context_free_grammar_rule_sequence:
non_terminal_list.append(context_free_grammar_rule.left_side)
for terminal_or_non_terminal in context_free_grammar_rule.right_side:
if isinstance(terminal_or_non_terminal, NonTerminal):
non_terminal_list.append(terminal_or_non_terminal)
self._non_terminal_tuple = tuple(
core_utilities.uniqify_sequence(non_terminal_list)
)
self._terminal_tuple = core_utilities.uniqify_sequence(
tuple(
item
for item in functools.reduce(
operator.add,
tuple(
context_free_grammar_rule.right_side
for context_free_grammar_rule in context_free_grammar_rule_sequence
),
)
if isinstance(item, Terminal)
)
)
divided_context_free_grammar_rule_list = [[] for _ in self._non_terminal_tuple]
for context_free_grammar_rule in context_free_grammar_rule_sequence:
index = self._non_terminal_tuple.index( # type: ignore
context_free_grammar_rule.left_side
)
divided_context_free_grammar_rule_list[index].append(
context_free_grammar_rule
)
self._divided_context_free_grammar_rule_tuple = tuple(
tuple(context_free_grammar_rule_list)
for context_free_grammar_rule_list in divided_context_free_grammar_rule_list
)
self._context_free_grammar_rule_tuple = tuple(
context_free_grammar_rule_sequence
)
def _data_to_tag(
self, data: tuple[typing.Union[NonTerminal, Terminal], ...]
) -> str:
return str(data)
def _add_node(
self,
tree: treelib.Tree,
data: tuple[typing.Union[NonTerminal, Terminal], ...],
parent: typing.Optional[treelib.Node] = None,
):
tree.create_node(self._data_to_tag(data), data=data, parent=parent)
def _resolve_content(
self, content: tuple[typing.Union[NonTerminal, Terminal], ...]
) -> tuple[tuple[typing.Union[NonTerminal, Terminal], ...], ...]:
new_data_list = []
for nth_element, element in enumerate(content):
if isinstance(element, NonTerminal):
context_free_grammar_rule_tuple = (
self.get_context_free_grammar_rule_tuple(element)
)
for context_free_grammar_rule in context_free_grammar_rule_tuple:
data = (
content[:nth_element]
+ context_free_grammar_rule.right_side
+ content[nth_element + 1 :]
)
new_data_list.append(data)
return tuple(new_data_list)
@property
def non_terminal_tuple(self) -> tuple[NonTerminal, ...]:
return self._non_terminal_tuple # type: ignore
@property
def terminal_tuple(self) -> tuple[Terminal, ...]:
return self._terminal_tuple # type: ignore
@property
def context_free_grammar_rule_tuple(self) -> tuple[ContextFreeGrammarRule, ...]:
"""Get all defined rules"""
return self._context_free_grammar_rule_tuple
[docs] def get_context_free_grammar_rule_tuple(
self, non_terminal: NonTerminal
) -> tuple[ContextFreeGrammarRule, ...]:
"""Find all defined context_free_grammar_rules for the provided :class:`NonTerminal`.
:param non_terminal: The left side element of the :class:`ContextFreeGrammarRule`.
:type non_terminal: NonTerminal
"""
index = self._non_terminal_tuple.index(non_terminal) # type: ignore
return self._divided_context_free_grammar_rule_tuple[index]
[docs] def resolve_one_layer(self, tree: treelib.Tree) -> bool:
"""Resolve all leaves of the tree.
:param tree: The tree from which all leaves should
be resolved.
:type tree: treelib.Tree
:return: `True` if any leaf has been resolved and `False`
if no resolution has happened (e.g. if there are only
:class:`Terminal` left).
"""
new_node = False
for leaf in tree.leaves():
data_tuple = self._resolve_content(leaf.data)
for data in data_tuple:
self._add_node(tree, data, leaf)
new_node = True
return new_node
[docs] def resolve(
self, start: NonTerminal, limit: typing.Optional[int] = None
) -> treelib.Tree:
"""Resolve until only :class:`Terminal` are left or the limit is reached.
:param start: The start value.
:type start: NonTerminal
:param limit: The maximum node levels until the function returns a tree.
If it is set to `None` it will only stop once all nodes are
:class:`Terminal`.
:type limit: typing.Optional[int]
"""
def is_limit_reached() -> bool:
if limit is None:
return False
else:
return limit <= counter
tree = treelib.Tree()
self._add_node(tree, (start,))
is_not_resolved = True
counter = 0
while is_not_resolved and not is_limit_reached():
is_not_resolved = self.resolve_one_layer(tree)
counter += 1
return tree