├── .cspell.jsonc ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .ruff.toml ├── LICENSE ├── README.md ├── computation ├── __init__.py ├── automata │ ├── __init__.py │ ├── dfa.py │ ├── farule.py │ ├── nfa.py │ ├── pattern.py │ ├── pda.py │ └── state.py ├── exceptions.py ├── interpreter │ ├── __init__.py │ ├── expressions │ │ ├── __init__.py │ │ ├── abstract.py │ │ └── concrete.py │ ├── interpreter.py │ ├── parser.py │ └── statements.py ├── lambda_calculus │ ├── __init__.py │ ├── basic.py │ ├── calculation.py │ ├── condition.py │ ├── func.py │ ├── generics.py │ ├── number.py │ ├── reduction.py │ ├── streams.py │ └── struct.py ├── turing_machine │ ├── __init__.py │ ├── rule.py │ └── tape.py └── utils.py ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── automata ├── __init__.py ├── test_dfa.py ├── test_nfa.py ├── test_pattern.py └── test_pda.py ├── interpreter ├── __init__.py ├── test_expr.py ├── test_interpreter.py └── test_stmt.py ├── lambda_calculus ├── __init__.py └── test_lambda.py ├── test_utils.py └── turing_machine ├── __init__.py ├── test_rule.py └── test_tape.py /.cspell.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreWords": [ 3 | "lalr" 4 | ], 5 | "ignorePaths": [ 6 | "LICENSE", 7 | ".gitignore", 8 | ".github/", 9 | ".cspell.jsonc", 10 | "pyproject.toml", 11 | "poetry.lock", 12 | ], 13 | "words": [ 14 | "biexpr", 15 | "Codecov", 16 | "Endofunctor", 17 | "farule", 18 | "isort", 19 | "rulebook", 20 | "SICP", 21 | "UNSHIFT", 22 | "venv" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 3 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install poetry 17 | run: pipx install poetry 18 | 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.13" 22 | cache: poetry 23 | 24 | - run: poetry install 25 | 26 | - run: poetry run pytest --cov-report=xml -s 27 | 28 | - uses: codecov/codecov-action@v3 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | .vscode/ 108 | .DS_Store 109 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 88 2 | 3 | [lint] 4 | ignore = ["E501", "E731", "E741"] 5 | 6 | [lint.mccabe] 7 | max-complexity = 8 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2024 Weiliang Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # computation-py 2 | 3 | [![License](https://img.shields.io/github/license/kigawas/computation-py.svg)](https://github.com/kigawas/computation-py) 4 | [![CI](https://img.shields.io/github/actions/workflow/status/kigawas/computation-py/ci.yml?branch=master)](https://github.com/kigawas/computation-py/actions) 5 | [![Codecov](https://img.shields.io/codecov/c/github/kigawas/computation-py.svg)](https://codecov.io/gh/kigawas/computation-py) 6 | 7 | Python implementation for [Understanding Computation](http://computationbook.com/). 8 | 9 | ## Introduction 10 | 11 | [Understanding Computation](http://computationbook.com/) is an awesome book about computation theory, which explains profound and complicated concepts by using short and concise Ruby code snippets. 12 | 13 | I don't want to evangelize, but if you are curious about how a program functions, you must read this book. It's just like SICP's ruby version in a way, yet with much more fun. 14 | 15 | ## What does this repository cover 16 | 17 | I just implemented equivalent codes from chapter to chapter, those contents are: 18 | 19 | 1. Two kinds of interpreters for a simple language with a parser based on [lark](https://github.com/lark-parser/lark) 20 | 21 | 2. Generating Python code for this language instead of Ruby (Because Python's anonymous functions are quite more limited than Ruby, there are some hacks like [Y-combinator](https://kigawas.me/posts/y-combinator-in-python/)) 22 | 23 | 3. Simulating automata such as DFA, NFA, PDA 24 | 25 | 4. Using automata to build a simple regular expression engine 26 | 27 | 5. Simulating a Turing machine 28 | 29 | 6. Lambda calculus and Church numbers 30 | 31 | 7. Stay tuned.. 32 | 33 | ## What is your Python's version 34 | 35 | Python 3.10+ 36 | -------------------------------------------------------------------------------- /computation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/computation/__init__.py -------------------------------------------------------------------------------- /computation/automata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/computation/automata/__init__.py -------------------------------------------------------------------------------- /computation/automata/dfa.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Optional 5 | 6 | from .farule import DFARulebook, State 7 | 8 | 9 | @dataclass 10 | class DFA: 11 | current_state: State 12 | accept_states: list[State] 13 | rulebook: DFARulebook 14 | 15 | @property 16 | def accepting(self) -> bool: 17 | return self.current_state in self.accept_states 18 | 19 | def read_character(self, character: Optional[str]) -> DFA: 20 | self.current_state = self.rulebook.next_state(self.current_state, character) 21 | return self 22 | 23 | def read_string(self, string: str) -> DFA: 24 | for c in string: 25 | self.read_character(c) 26 | return self 27 | 28 | 29 | @dataclass 30 | class DFADesign: 31 | start_state: State 32 | accept_states: list[State] 33 | rulebook: DFARulebook 34 | 35 | @property 36 | def to_dfa(self) -> DFA: 37 | return DFA(self.start_state, self.accept_states, self.rulebook) 38 | 39 | def accepts(self, string: str) -> bool: 40 | return self.to_dfa.read_string(string).accepting 41 | -------------------------------------------------------------------------------- /computation/automata/farule.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Any, FrozenSet, Iterable, Optional, Union 5 | 6 | from ..exceptions import Unreachable 7 | from ..utils import detect 8 | 9 | State = Union[int, Any] 10 | 11 | 12 | @dataclass(frozen=True) 13 | class FARule: 14 | state: State 15 | character: Optional[str] 16 | next_state: State 17 | 18 | @property 19 | def follow(self) -> State: 20 | return self.next_state 21 | 22 | def applies_to(self, state: State, character: Optional[str]) -> bool: 23 | if character is None: 24 | return self.state == state and self.character is None 25 | 26 | return self.state == state and self.character == character 27 | 28 | 29 | @dataclass(frozen=True) 30 | class DFARulebook: 31 | rules: list[FARule] 32 | 33 | def rule_for(self, state: State, character: Optional[str]) -> Optional[FARule]: 34 | return detect(self.rules, lambda rule: rule.applies_to(state, character)) 35 | 36 | def next_state(self, state: State, character: Optional[str]) -> State: 37 | rule = self.rule_for(state, character) 38 | if not rule: 39 | raise Unreachable 40 | return rule.follow 41 | 42 | 43 | @dataclass(frozen=True) 44 | class NFARulebook: 45 | rules: list[FARule] 46 | 47 | @property 48 | def alphabet(self) -> FrozenSet[str]: 49 | return frozenset( 50 | [rule.character for rule in self.rules if rule.character is not None] 51 | ) 52 | 53 | def rules_for(self, state: State, character: Optional[str]) -> list[FARule]: 54 | return [r for r in self.rules if r.applies_to(state, character)] 55 | 56 | def follow_rules_for(self, state: State, character: Optional[str]) -> list[State]: 57 | return [r.follow for r in self.rules_for(state, character)] 58 | 59 | def next_states( 60 | self, states: Iterable[State], character: Optional[str] 61 | ) -> FrozenSet[State]: 62 | next_states = [self.follow_rules_for(s, character) for s in states] 63 | return frozenset(sum(next_states, [])) 64 | 65 | def follow_free_moves(self, states: Iterable[int]) -> Iterable[State]: 66 | more_states = self.next_states(states, None) 67 | if more_states.issubset(states): 68 | return states 69 | else: 70 | return self.follow_free_moves(more_states.union(states)) 71 | -------------------------------------------------------------------------------- /computation/automata/nfa.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Iterable, Optional, Tuple 5 | 6 | from .dfa import DFADesign 7 | from .farule import DFARulebook, FARule, NFARulebook, State 8 | 9 | 10 | @dataclass 11 | class NFA: 12 | def __init__( 13 | self, 14 | current_states: Iterable[State], 15 | accept_states: Iterable[State], 16 | rulebook: NFARulebook, 17 | ): 18 | self.current_states = rulebook.follow_free_moves(frozenset(current_states)) 19 | self.accept_states = frozenset(accept_states) 20 | self.rulebook = rulebook 21 | 22 | @property 23 | def accepting(self): 24 | return not frozenset.isdisjoint(self.current_states, self.accept_states) 25 | 26 | def read_character(self, character: Optional[str]) -> NFA: 27 | next_states = self.rulebook.next_states(self.current_states, character) 28 | self.current_states = self.rulebook.follow_free_moves(next_states) 29 | return self 30 | 31 | def read_string(self, string: str) -> NFA: 32 | for c in string: 33 | self.read_character(c) 34 | return self 35 | 36 | 37 | @dataclass 38 | class NFADesign: 39 | start_state: State 40 | accept_states: list[State] 41 | rulebook: NFARulebook 42 | 43 | @property 44 | def to_nfa(self) -> NFA: 45 | return NFA([self.start_state], self.accept_states, self.rulebook) 46 | 47 | def to_nfa_from(self, current_states: Iterable[State]) -> NFA: 48 | return NFA(current_states, self.accept_states, self.rulebook) 49 | 50 | def accepts(self, string: str): 51 | return self.to_nfa.read_string(string).accepting 52 | 53 | 54 | @dataclass 55 | class NFASimulation: 56 | nfa_design: NFADesign 57 | 58 | def next_state(self, state: Iterable[State], character: Optional[str]): 59 | return ( 60 | self.nfa_design.to_nfa_from(set(state)) 61 | .read_character(character) 62 | .current_states 63 | ) 64 | 65 | def rules_for(self, state: Iterable[State]) -> list[FARule]: 66 | alphabet = self.nfa_design.rulebook.alphabet 67 | return [ 68 | FARule(frozenset(state), character, self.next_state(state, character)) 69 | for character in alphabet 70 | ] 71 | 72 | def discover_states_and_rules( 73 | self, states: frozenset 74 | ) -> Tuple[Iterable[Iterable[State]], list[FARule]]: 75 | rules_list = [self.rules_for(state) for state in states] 76 | rules: list[FARule] = sum(rules_list, []) 77 | more_states = frozenset([rule.follow for rule in rules]) 78 | 79 | if more_states.issubset(states): 80 | return states, rules 81 | else: 82 | return self.discover_states_and_rules(more_states.union(states)) 83 | 84 | @property 85 | def to_dfa_design(self): 86 | start_state = self.nfa_design.to_nfa.current_states 87 | states, rules = self.discover_states_and_rules(frozenset([start_state])) 88 | accept_states = [ 89 | state for state in states if self.nfa_design.to_nfa_from(state).accepting 90 | ] 91 | return DFADesign(start_state, accept_states, DFARulebook(rules)) 92 | -------------------------------------------------------------------------------- /computation/automata/pattern.py: -------------------------------------------------------------------------------- 1 | from .farule import FARule 2 | from .nfa import NFADesign, NFARulebook 3 | from .state import State 4 | 5 | 6 | class Pattern: 7 | def braket(self, outer_precedence): 8 | if self.precedence < outer_precedence: 9 | return "(" + str(self) + ")" 10 | else: 11 | return str(self) 12 | 13 | def __repr__(self): 14 | return f"/{self}/" 15 | 16 | def matches(self, string): 17 | return self.to_nfa_design.accepts(string) 18 | 19 | 20 | class Empty(Pattern): 21 | def __str__(self): 22 | return "" 23 | 24 | @property 25 | def precedence(self): 26 | return 3 27 | 28 | @property 29 | def to_nfa_design(self): 30 | start_state = State() 31 | accept_states = [start_state] 32 | rulebook = NFARulebook([]) 33 | return NFADesign(start_state, accept_states, rulebook) 34 | 35 | 36 | class Literal(Pattern): 37 | def __init__(self, character): 38 | self.character = character 39 | 40 | def __str__(self): 41 | return self.character 42 | 43 | @property 44 | def precedence(self): 45 | return 3 46 | 47 | @property 48 | def to_nfa_design(self): 49 | start_state = State() 50 | accept_state = State() 51 | rule = FARule(start_state, self.character, accept_state) 52 | rulebook = NFARulebook([rule]) 53 | return NFADesign(start_state, [accept_state], rulebook) 54 | 55 | 56 | class Concatenate(Pattern): 57 | def __init__(self, first, second): 58 | self.first, self.second = first, second 59 | 60 | def __str__(self): 61 | return "".join([p.braket(self.precedence) for p in [self.first, self.second]]) 62 | 63 | @property 64 | def precedence(self): 65 | return 1 66 | 67 | @property 68 | def to_nfa_design(self): 69 | first_nfa_design, second_nfa_design = ( 70 | self.first.to_nfa_design, 71 | self.second.to_nfa_design, 72 | ) 73 | start_state, accept_states = ( 74 | first_nfa_design.start_state, 75 | second_nfa_design.accept_states, 76 | ) 77 | 78 | rules = first_nfa_design.rulebook.rules + second_nfa_design.rulebook.rules 79 | 80 | extra_rules = [ 81 | FARule(state, None, second_nfa_design.start_state) 82 | for state in first_nfa_design.accept_states 83 | ] 84 | rulebook = NFARulebook(rules + extra_rules) 85 | return NFADesign(start_state, accept_states, rulebook) 86 | 87 | 88 | class Choose(Pattern): 89 | def __init__(self, first, second): 90 | self.first, self.second = first, second 91 | 92 | def __str__(self): 93 | return "|".join([p.braket(self.precedence) for p in [self.first, self.second]]) 94 | 95 | @property 96 | def precedence(self): 97 | return 0 98 | 99 | @property 100 | def to_nfa_design(self): 101 | first_nfa_design, second_nfa_design = ( 102 | self.first.to_nfa_design, 103 | self.second.to_nfa_design, 104 | ) 105 | start_state = State() 106 | accept_states = first_nfa_design.accept_states + second_nfa_design.accept_states 107 | 108 | rules = first_nfa_design.rulebook.rules + second_nfa_design.rulebook.rules 109 | extra_rules = [ 110 | FARule(start_state, None, nfa_design.start_state) 111 | for nfa_design in [first_nfa_design, second_nfa_design] 112 | ] 113 | rulebook = NFARulebook(rules + extra_rules) 114 | return NFADesign(start_state, accept_states, rulebook) 115 | 116 | 117 | class Repeat(Pattern): 118 | def __init__(self, pattern): 119 | self.pattern = pattern 120 | 121 | def __str__(self): 122 | return self.pattern.braket(self.precedence) + "*" 123 | 124 | @property 125 | def precedence(self): 126 | return 2 127 | 128 | @property 129 | def to_nfa_design(self): 130 | pattern_nfa_design = self.pattern.to_nfa_design 131 | start_state = State() 132 | accept_states = pattern_nfa_design.accept_states + [start_state] 133 | rules = pattern_nfa_design.rulebook.rules 134 | extra_rules = [ 135 | FARule(accept_state, None, pattern_nfa_design.start_state) 136 | for accept_state in pattern_nfa_design.accept_states 137 | ] + [FARule(start_state, None, pattern_nfa_design.start_state)] 138 | rulebook = NFARulebook(rules + extra_rules) 139 | return NFADesign(start_state, accept_states, rulebook) 140 | -------------------------------------------------------------------------------- /computation/automata/pda.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Iterable, Optional 3 | 4 | from ..utils import detect 5 | from .farule import State 6 | from .state import State as _State 7 | 8 | 9 | @dataclass(frozen=True) 10 | class Stack: 11 | contents: list[State] 12 | 13 | def push(self, character): 14 | return Stack(self.contents + [character]) 15 | 16 | @property 17 | def pop(self): 18 | return Stack(self.contents[:-1]) 19 | 20 | @property 21 | def top(self): 22 | return self.contents[-1] 23 | 24 | def __hash__(self): 25 | return hash(tuple(self.contents)) 26 | 27 | 28 | @dataclass(eq=True) 29 | class PDAConfiguration: 30 | state: State 31 | stack: Stack 32 | 33 | STUCK_STATE = _State() 34 | 35 | def __hash__(self): 36 | return hash(self.state) ^ hash("".join(self.stack.contents)) 37 | 38 | @property 39 | def stuck(self): 40 | return PDAConfiguration(PDAConfiguration.STUCK_STATE, self.stack) 41 | 42 | @property 43 | def is_stuck(self): 44 | return self.state == PDAConfiguration.STUCK_STATE 45 | 46 | 47 | @dataclass 48 | class PDARule: 49 | state: State 50 | character: Optional[str] 51 | next_state: State 52 | pop_character: str 53 | push_characters: Iterable[str] 54 | 55 | def applies_to(self, configuration, character): 56 | return ( 57 | self.state == configuration.state 58 | and self.pop_character == configuration.stack.top 59 | and self.character == character 60 | ) 61 | 62 | def next_stack(self, configuration): 63 | popped_stack = configuration.stack.pop 64 | for c in reversed(self.push_characters): 65 | popped_stack = popped_stack.push(c) 66 | return popped_stack 67 | 68 | def follow(self, configuration): 69 | return PDAConfiguration(self.next_state, self.next_stack(configuration)) 70 | 71 | 72 | @dataclass 73 | class DPDARulebook: 74 | rules: list[PDARule] 75 | 76 | def rule_for(self, configuration, character): 77 | return detect( 78 | self.rules, lambda rule: rule.applies_to(configuration, character) 79 | ) 80 | 81 | def next_configuration(self, configuration, character): 82 | return self.rule_for(configuration, character).follow(configuration) 83 | 84 | def applies_to(self, configuration, character): 85 | return self.rule_for(configuration, character) is not None 86 | 87 | def follow_free_moves(self, configuration): 88 | if self.applies_to(configuration, None): 89 | return self.follow_free_moves(self.next_configuration(configuration, None)) 90 | else: 91 | return configuration 92 | 93 | 94 | @dataclass 95 | class DPDA: 96 | _current_configuration: PDAConfiguration 97 | accept_states: list[State] 98 | rulebook: DPDARulebook 99 | 100 | @property 101 | def accepting(self): 102 | return self.current_configuration.state in self.accept_states 103 | 104 | def read_character(self, character): 105 | self._current_configuration = self.next_configuration(character) 106 | return self 107 | 108 | def read_string(self, string): 109 | for c in string: 110 | if not self.is_stuck: 111 | self.read_character(c) 112 | return self 113 | 114 | def next_configuration(self, character): 115 | if self.rulebook.applies_to(self.current_configuration, character): 116 | return self.rulebook.next_configuration( 117 | self.current_configuration, character 118 | ) 119 | else: 120 | return self.current_configuration.stuck 121 | 122 | @property 123 | def is_stuck(self): 124 | return self.current_configuration.is_stuck 125 | 126 | @property 127 | def current_configuration(self): 128 | return self.rulebook.follow_free_moves(self._current_configuration) 129 | 130 | 131 | @dataclass 132 | class DPDADesign: 133 | start_state: State 134 | bottom_character: str 135 | accept_states: list[State] 136 | rulebook: DPDARulebook 137 | 138 | @property 139 | def to_dpda(self): 140 | start_stack = Stack([self.bottom_character]) 141 | start_configuration = PDAConfiguration(self.start_state, start_stack) 142 | return DPDA(start_configuration, self.accept_states, self.rulebook) 143 | 144 | def accepts(self, string): 145 | return self.to_dpda.read_string(string).accepting 146 | 147 | 148 | @dataclass 149 | class NPDARulebook: 150 | rules: list[PDARule] 151 | 152 | def rule_for(self, configuration, character): 153 | return [ 154 | rule for rule in self.rules if rule.applies_to(configuration, character) 155 | ] 156 | 157 | def follow_rules_for(self, configuration, character): 158 | return [ 159 | rule.follow(configuration) 160 | for rule in self.rule_for(configuration, character) 161 | ] 162 | 163 | def next_configurations(self, configurations, character): 164 | return set( 165 | sum( 166 | [self.follow_rules_for(config, character) for config in configurations], 167 | [], 168 | ) 169 | ) 170 | 171 | def follow_free_moves(self, configurations): 172 | more_configurations = self.next_configurations(configurations, None) 173 | if more_configurations.issubset(configurations): 174 | return configurations 175 | else: 176 | return self.follow_free_moves(more_configurations.union(configurations)) 177 | 178 | 179 | @dataclass 180 | class NPDA: 181 | _current_configurations: PDAConfiguration 182 | accept_states: Iterable[State] 183 | rulebook: NPDARulebook 184 | 185 | @property 186 | def accepting(self): 187 | return any( 188 | [ 189 | config.state in self.accept_states 190 | for config in self.current_configurations 191 | ] 192 | ) 193 | 194 | def read_character(self, character): 195 | self._current_configurations = self.rulebook.next_configurations( 196 | self.current_configurations, character 197 | ) 198 | return self 199 | 200 | def read_string(self, string): 201 | for c in string: 202 | self.read_character(c) 203 | return self 204 | 205 | @property 206 | def current_configurations(self): 207 | return self.rulebook.follow_free_moves(self._current_configurations) 208 | 209 | 210 | @dataclass 211 | class NPDADesign: 212 | start_state: State 213 | bottom_character: str 214 | accept_states: list[State] 215 | rulebook: NPDARulebook 216 | 217 | @property 218 | def to_npda(self): 219 | start_stack = Stack([self.bottom_character]) 220 | start_configuration = PDAConfiguration(self.start_state, start_stack) 221 | return NPDA( 222 | [start_configuration], 223 | self.accept_states, 224 | self.rulebook, 225 | ) 226 | 227 | def accepts(self, string): 228 | return self.to_npda.read_string(string).accepting 229 | -------------------------------------------------------------------------------- /computation/automata/state.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(eq=False, frozen=True) 5 | class State: 6 | # each state is unique 7 | pass 8 | -------------------------------------------------------------------------------- /computation/exceptions.py: -------------------------------------------------------------------------------- 1 | class Unreachable(ValueError): 2 | pass 3 | -------------------------------------------------------------------------------- /computation/interpreter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/computation/interpreter/__init__.py -------------------------------------------------------------------------------- /computation/interpreter/expressions/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "Add", 3 | "And", 4 | "Boolean", 5 | "EqualTo", 6 | "LessThan", 7 | "Multiply", 8 | "Number", 9 | "Or", 10 | "Sub", 11 | "Variable", 12 | ] 13 | 14 | from .concrete import ( 15 | Add, 16 | And, 17 | Boolean, 18 | EqualTo, 19 | LessThan, 20 | Multiply, 21 | Number, 22 | Or, 23 | Sub, 24 | Variable, 25 | ) 26 | -------------------------------------------------------------------------------- /computation/interpreter/expressions/abstract.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from typing import Any 3 | 4 | OP_MAP = { 5 | "+": operator.add, 6 | "-": operator.sub, 7 | "*": operator.mul, 8 | "<": operator.lt, 9 | "and": operator.and_, 10 | "or": operator.or_, 11 | "==": operator.eq, 12 | } 13 | 14 | 15 | class Expression: 16 | @property 17 | def reducible(self): 18 | raise NotImplementedError 19 | 20 | @property 21 | def to_python(self): 22 | """ 23 | Python lambda code generator 24 | """ 25 | raise NotImplementedError 26 | 27 | def evaluate(self, _environment: dict): 28 | """ 29 | Evaluate everything in a "recursive descent" way 30 | """ 31 | raise NotImplementedError 32 | 33 | def reduce(self, _environment: dict): 34 | """ 35 | Reduce step by step 36 | """ 37 | raise NotImplementedError 38 | 39 | 40 | class Statement(Expression): 41 | @property 42 | def reducible(self): 43 | return True 44 | 45 | 46 | class Atom(Expression): 47 | value: Any 48 | 49 | def __str__(self): 50 | return f"{self.value}" 51 | 52 | @property 53 | def reducible(self): 54 | return False 55 | 56 | @property 57 | def to_python(self): 58 | return f"lambda _: {self.value}" 59 | 60 | def evaluate(self, _environment): 61 | return self 62 | 63 | 64 | class BinaryExpression(Expression): 65 | left: Expression 66 | right: Expression 67 | 68 | def __init__(self, _left, _right): 69 | raise NotImplementedError 70 | 71 | def __str__(self): 72 | op = self.op_str() 73 | return f"{self.left} {op} {self.right}" 74 | 75 | @staticmethod 76 | def op_str() -> str: 77 | raise NotImplementedError 78 | 79 | @staticmethod 80 | def build_atom(value: Any) -> Atom: 81 | raise NotImplementedError 82 | 83 | @classmethod 84 | def op(cls, a: Any, b: Any) -> Any: 85 | op = cls.op_str() 86 | return OP_MAP[op](a, b) 87 | 88 | @property 89 | def to_python(self): 90 | op = self.op_str() 91 | return f"lambda e:({self.left.to_python})(e) {op} ({self.right.to_python})(e)" 92 | 93 | @property 94 | def reducible(self): 95 | return True 96 | 97 | def reduce(self, environment): 98 | if self.left.reducible: 99 | return self.__class__(self.left.reduce(environment), self.right) 100 | elif self.right.reducible: 101 | return self.__class__(self.left, self.right.reduce(environment)) 102 | else: 103 | assert isinstance(self.left, Atom) 104 | assert isinstance(self.right, Atom) 105 | return self.build_atom(self.op(self.left.value, self.right.value)) 106 | 107 | def evaluate(self, environment): 108 | return self.build_atom( 109 | self.op( 110 | self.left.evaluate(environment).value, 111 | self.right.evaluate(environment).value, 112 | ) 113 | ) 114 | -------------------------------------------------------------------------------- /computation/interpreter/expressions/concrete.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from .abstract import Atom, BinaryExpression, Expression 4 | 5 | 6 | @dataclass(order=True) 7 | class Number(Atom): 8 | value: int 9 | 10 | 11 | @dataclass 12 | class Boolean(Atom): 13 | value: bool 14 | 15 | 16 | @dataclass 17 | class Variable(Expression): 18 | name: str 19 | 20 | def __str__(self): 21 | return f"{self.name}" 22 | 23 | @property 24 | def reducible(self): 25 | return True 26 | 27 | def reduce(self, environment): 28 | return environment[self.name] 29 | 30 | def evaluate(self, environment): 31 | return environment[self.name] 32 | 33 | @property 34 | def to_python(self): 35 | return f"lambda e: e['{self.name}']" 36 | 37 | 38 | @dataclass 39 | class Add(BinaryExpression): 40 | left: Expression 41 | right: Expression 42 | 43 | @staticmethod 44 | def op_str() -> str: 45 | return "+" 46 | 47 | @staticmethod 48 | def build_atom(value): 49 | return Number(value) 50 | 51 | 52 | @dataclass 53 | class Sub(BinaryExpression): 54 | left: Expression 55 | right: Expression 56 | 57 | @staticmethod 58 | def op_str() -> str: 59 | return "-" 60 | 61 | @staticmethod 62 | def build_atom(value): 63 | return Number(value) 64 | 65 | 66 | @dataclass 67 | class Multiply(BinaryExpression): 68 | left: Expression 69 | right: Expression 70 | 71 | @staticmethod 72 | def op_str() -> str: 73 | return "*" 74 | 75 | @staticmethod 76 | def build_atom(value): 77 | return Number(value) 78 | 79 | 80 | @dataclass 81 | class LessThan(BinaryExpression): 82 | left: Expression 83 | right: Expression 84 | 85 | @classmethod 86 | def op_str(cls) -> str: 87 | return "<" 88 | 89 | @staticmethod 90 | def build_atom(value): 91 | return Boolean(value) 92 | 93 | 94 | @dataclass 95 | class EqualTo(BinaryExpression): 96 | left: Expression 97 | right: Expression 98 | 99 | @classmethod 100 | def op_str(cls) -> str: 101 | return "==" 102 | 103 | @staticmethod 104 | def build_atom(value): 105 | return Boolean(value) 106 | 107 | 108 | @dataclass 109 | class And(BinaryExpression): 110 | left: Expression 111 | right: Expression 112 | 113 | @classmethod 114 | def op_str(cls) -> str: 115 | return "and" 116 | 117 | @staticmethod 118 | def build_atom(value): 119 | return Boolean(value) 120 | 121 | 122 | @dataclass 123 | class Or(BinaryExpression): 124 | left: Expression 125 | right: Expression 126 | 127 | @classmethod 128 | def op_str(cls) -> str: 129 | return "or" 130 | 131 | @staticmethod 132 | def build_atom(value): 133 | return Boolean(value) 134 | -------------------------------------------------------------------------------- /computation/interpreter/interpreter.py: -------------------------------------------------------------------------------- 1 | class Machine: 2 | def __init__(self, expression, environment: dict, debug: bool = False): 3 | self.expression = expression 4 | self.environment = environment 5 | self.debug = debug 6 | 7 | def step(self): 8 | self.expression, self.environment = self.expression.reduce(self.environment) 9 | 10 | def log(self): 11 | if self.debug: 12 | print(f"{self.expression}, {self.environment}") 13 | 14 | def run(self): 15 | self.log() 16 | while self.expression.reducible: 17 | self.step() 18 | self.log() 19 | return self 20 | -------------------------------------------------------------------------------- /computation/interpreter/parser.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from lark import Lark, Token 4 | from lark import Transformer as _Transformer 5 | 6 | from ..exceptions import Unreachable 7 | from .expressions import ( 8 | Add, 9 | And, 10 | EqualTo, 11 | LessThan, 12 | Multiply, 13 | Number, 14 | Or, 15 | Sub, 16 | Variable, 17 | ) 18 | from .expressions.abstract import BinaryExpression, Expression 19 | from .statements import Assign, DoNothing, If, Sequence, While 20 | 21 | # inspired by 22 | # https://medium.com/@gvanrossum_83706/building-a-peg-parser-d4869b5958fb 23 | # https://medium.com/@gvanrossum_83706/left-recursive-peg-grammars-65dab3c580e1 24 | # note: lark is not exhaustive, so `expr: term ('+' term)*` won't work 25 | GRAMMAR = r""" 26 | %import common.ESCAPED_STRING -> STRING 27 | %import common.INT -> NUMBER 28 | %import common.CNAME -> NAME 29 | %import common.WS 30 | %ignore WS 31 | 32 | OP_AND: "&&" 33 | OP_OR: "||" 34 | OP_EQ: "<" | "==" 35 | OP_ADD: "+" | "-" 36 | OP_MUL: "*" 37 | atom: NUMBER | NAME | "(" expr ")" 38 | 39 | expr: expr OP_ADD mul_expr | mul_expr 40 | mul_expr: mul_expr OP_MUL atom | atom 41 | 42 | or_expr: or_expr OP_OR and_expr | and_expr 43 | and_expr: and_expr OP_AND eq_expr | eq_expr 44 | eq_expr: expr OP_EQ expr 45 | 46 | if_stmt: "if" "(" or_expr ")" "{" stmts "}" [("else" "{" stmts "}")] 47 | while_stmt: "while" "(" or_expr ")" "{" stmts "}" 48 | assign_stmt: NAME "=" expr 49 | 50 | stmt: expr | if_stmt | while_stmt | assign_stmt 51 | stmts: stmt* 52 | """ 53 | 54 | parser = Lark(GRAMMAR, start="stmts", parser="lalr") 55 | 56 | 57 | def token_to_atom(token: Token) -> Expression: 58 | if token.type == "NUMBER": 59 | return Number(int(token.value)) 60 | elif token.type == "NAME": 61 | return Variable(token.value) 62 | else: 63 | raise Unreachable 64 | 65 | 66 | def eval_binary_expr( 67 | left, 68 | op, 69 | right, 70 | expected_ops: list[str], 71 | expr_classes: list[Type[BinaryExpression]], 72 | ): 73 | for expected_op, expr_class in zip(expected_ops, expr_classes): 74 | if op.value == expected_op: 75 | return [expr_class(left[0], right[0])] 76 | raise Unreachable 77 | 78 | 79 | class Transformer(_Transformer): 80 | def atom(self, items) -> list[Expression]: 81 | res = [] 82 | for item in items: 83 | if isinstance(item, Token): 84 | res.append(token_to_atom(item)) 85 | else: 86 | res.append(item[0]) 87 | return res 88 | 89 | def _biexpr(self, items, expected_ops, expr_classes): 90 | if len(items) == 1: 91 | return items[0] 92 | left, op, right = items[0], items[1], items[2] 93 | return eval_binary_expr(left, op, right, expected_ops, expr_classes) 94 | 95 | def and_expr(self, items): 96 | return self._biexpr(items, ["&&"], [And]) 97 | 98 | def or_expr(self, items): 99 | return self._biexpr(items, ["||"], [Or]) 100 | 101 | def eq_expr(self, items): 102 | return self._biexpr(items, ["<", "=="], [LessThan, EqualTo]) 103 | 104 | def expr(self, items): 105 | return self._biexpr(items, ["+", "-"], [Add, Sub]) 106 | 107 | def mul_expr(self, items): 108 | return self._biexpr(items, ["*"], [Multiply]) 109 | 110 | def if_stmt(self, items): 111 | cond, seq, alt = items[0][0], items[1], items[2] 112 | return If(cond, seq, alt or DoNothing()) 113 | 114 | def while_stmt(self, items): 115 | return While(items[0][0], self.stmt(items[1:])) 116 | 117 | def assign_stmt(self, items): 118 | return Assign(items[0].value, items[1][0]) 119 | 120 | def stmt(self, items): 121 | if len(items) == 1: 122 | return items[0] 123 | else: 124 | raise Unreachable 125 | 126 | def stmts(self, items): 127 | if len(items) == 0: 128 | return DoNothing() 129 | elif len(items) == 1: 130 | return items[0] 131 | 132 | return Sequence(items[0], self.stmts(items[1:])) 133 | 134 | 135 | def parse(program: str): 136 | tree = parser.parse(program) 137 | return Transformer().transform(tree) 138 | -------------------------------------------------------------------------------- /computation/interpreter/statements.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from ..exceptions import Unreachable 4 | from .expressions import Boolean 5 | from .expressions.abstract import Expression, Statement 6 | 7 | 8 | @dataclass 9 | class DoNothing(Statement): 10 | def __str__(self): 11 | return "do-nothing" 12 | 13 | @property 14 | def reducible(self): 15 | return False 16 | 17 | def evaluate(self, environment): 18 | return environment 19 | 20 | @property 21 | def to_python(self): 22 | return "lambda e: e" 23 | 24 | 25 | @dataclass 26 | class Assign(Statement): 27 | name: str 28 | expression: Expression 29 | 30 | def __str__(self): 31 | return f"{self.name} = {self.expression}" 32 | 33 | def reduce(self, environment): 34 | if self.expression.reducible: 35 | return Assign(self.name, self.expression.reduce(environment)), environment 36 | else: 37 | return DoNothing(), environment | {self.name: self.expression} 38 | 39 | def evaluate(self, environment): 40 | return environment | {self.name: self.expression.evaluate(environment)} 41 | 42 | @property 43 | def to_python(self): 44 | """ 45 | Use dict comprehension or Python 3.9 new dict merge expression 46 | to eliminate outer function dependency 47 | 48 | This also works before Python 3.9: 49 | ```python 50 | lambda e: {{k: v for d in (e, {{"{self.name}": ({self.expression.to_python})(e)}}) for k, v in d.items()}} 51 | ``` 52 | """ 53 | 54 | return f'lambda e: e | {{"{self.name}": ({self.expression.to_python})(e)}}' 55 | 56 | 57 | @dataclass 58 | class If(Statement): 59 | condition: Expression 60 | consequence: Expression 61 | alternative: Expression 62 | 63 | def __str__(self): 64 | return f"if ({self.condition}) {{ {self.consequence} }} else {{ {self.alternative} }}" 65 | 66 | def reduce(self, environment): 67 | if self.condition.reducible: 68 | return ( 69 | If( 70 | self.condition.reduce(environment), 71 | self.consequence, 72 | self.alternative, 73 | ), 74 | environment, 75 | ) 76 | else: 77 | if self.condition == Boolean(True): 78 | return self.consequence, environment 79 | elif self.condition == Boolean(False): 80 | return self.alternative, environment 81 | else: 82 | raise Unreachable 83 | 84 | def evaluate(self, environment): 85 | if self.condition.evaluate(environment) == Boolean(True): 86 | return self.consequence.evaluate(environment) 87 | elif self.condition.evaluate(environment) == Boolean(False): 88 | return self.alternative.evaluate(environment) 89 | else: 90 | raise Unreachable 91 | 92 | @property 93 | def to_python(self): 94 | return f"lambda e: ({self.consequence.to_python})(e) if ({self.condition.to_python})(e) else ({self.alternative.to_python})(e)" 95 | 96 | 97 | @dataclass 98 | class Sequence(Statement): 99 | first: Expression 100 | second: Expression 101 | 102 | def __str__(self): 103 | return f"{self.first}; {self.second}" 104 | 105 | def reduce(self, environment): 106 | if self.first == DoNothing(): 107 | return self.second, environment 108 | else: 109 | reduced_first, reduced_env = self.first.reduce(environment) 110 | return Sequence(reduced_first, self.second), reduced_env 111 | 112 | def evaluate(self, environment): 113 | return self.second.evaluate(self.first.evaluate(environment)) 114 | 115 | @property 116 | def to_python(self): 117 | return f"lambda e: ({self.second.to_python})(({self.first.to_python})(e))" 118 | 119 | 120 | @dataclass 121 | class While(Statement): 122 | condition: Expression 123 | body: Expression 124 | 125 | def __str__(self): 126 | return f"while ({self.condition}) {{ {self.body} }}" 127 | 128 | def reduce(self, environment): 129 | return If(self.condition, Sequence(self.body, self), DoNothing()), environment 130 | 131 | def evaluate(self, environment): 132 | """Optimize tail recursion""" 133 | while True: 134 | if self.condition.evaluate(environment) == Boolean(False): 135 | return environment 136 | elif self.condition.evaluate(environment) == Boolean(True): 137 | environment = self.body.evaluate(environment) 138 | 139 | def evaluate_with_recursion(self, environment): 140 | if self.condition.evaluate(environment) == Boolean(True): 141 | return self.evaluate_with_recursion(self.body.evaluate(environment)) 142 | elif self.condition.evaluate(environment) == Boolean(False): 143 | return environment 144 | 145 | @property 146 | def to_python(self): 147 | """ 148 | This is a workaround using Y-combinator because Python doesn't allow lambda expression including `while`, 149 | so we have to implement while using recursion. 150 | 151 | But notice that Python does no tail recursion optimization, 152 | so it may raise `RuntimeError` when looping too many times, 153 | check the limit by `import sys; sys.getrecursionlimit()` 154 | """ 155 | return f"(lambda f: (lambda x: x(x))(lambda x: f(lambda *args: x(x)(*args))))(lambda wh: lambda e: e if ({self.condition.to_python})(e) is False else wh(({self.body.to_python})(e)))" 156 | -------------------------------------------------------------------------------- /computation/lambda_calculus/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "ZERO", 3 | "ONE", 4 | "TWO", 5 | "THREE", 6 | "FOUR", 7 | "FIVE", 8 | "SEVEN", 9 | "IF", 10 | "NOT", 11 | "TRUE", 12 | "FALSE", 13 | "IS_ZERO", 14 | "INCREMENT", 15 | "SLIDE", 16 | "DECREMENT", 17 | "ADD", 18 | "SUB", 19 | "MUL", 20 | "POW", 21 | "IS_LESS_OR_EQUAL", 22 | "IS_LESS", 23 | "FOUR", 24 | "SEVEN", 25 | "TEN", 26 | "FIFTEEN", 27 | "HUNDRED", 28 | "Z_COMBINATOR", 29 | "MOD", 30 | "RANGE", 31 | "CLOSED_RANGE", 32 | "FOLD", 33 | "MAP", 34 | "ZEROS", 35 | "UPWARDS_OF", 36 | "MULTIPLES_OF", 37 | "PAIR", 38 | "LEFT", 39 | "RIGHT", 40 | "EMPTY", 41 | "UNSHIFT", 42 | "IS_EMPTY", 43 | "FIRST", 44 | "REST", 45 | "to_array", 46 | "to_boolean", 47 | "to_integer", 48 | "to_generator", 49 | "to_integer_array", 50 | ] 51 | 52 | from .basic import FALSE, ONE, TRUE, ZERO 53 | from .calculation import ADD, DECREMENT, INCREMENT, MUL, POW, SLIDE, SUB 54 | from .condition import IF, IS_LESS_OR_EQUAL, IS_ZERO, NOT 55 | from .func import CLOSED_RANGE, FOLD, MAP, MOD, RANGE, Z_COMBINATOR 56 | from .number import FIFTEEN, FIVE, FOUR, HUNDRED, SEVEN, TEN, THREE, TWO 57 | from .reduction import to_array, to_boolean, to_generator, to_integer, to_integer_array 58 | from .streams import MULTIPLES_OF, UPWARDS_OF, ZEROS 59 | from .struct import EMPTY, FIRST, IS_EMPTY, LEFT, PAIR, REST, RIGHT, UNSHIFT 60 | -------------------------------------------------------------------------------- /computation/lambda_calculus/basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Book of Changes: 3 | 4 | Therefore there is in the Changes the Great Primal Beginning. 5 | This generates the two primary forces. 6 | The two primary forces generate the four images. 7 | The four images generate the eight trigrams 8 | """ 9 | 10 | ZERO = lambda _: lambda x: x 11 | ONE = lambda f: lambda x: f(x) 12 | TRUE = lambda x: lambda _: x 13 | FALSE = lambda _: lambda y: y 14 | -------------------------------------------------------------------------------- /computation/lambda_calculus/calculation.py: -------------------------------------------------------------------------------- 1 | from .basic import ONE, ZERO 2 | from .struct import LEFT, PAIR, RIGHT 3 | 4 | INCREMENT = lambda n: lambda f: lambda x: f(n(f)(x)) 5 | SLIDE = lambda f: PAIR(RIGHT(f))(INCREMENT(RIGHT(f))) # (a, b) -> (b, b+1) 6 | DECREMENT = lambda n: LEFT(n(SLIDE)(PAIR(ZERO)(ZERO))) 7 | ADD = lambda m: lambda n: n(INCREMENT)(m) 8 | SUB = lambda m: lambda n: n(DECREMENT)(m) 9 | MUL = lambda m: lambda n: n(ADD(m))(ZERO) 10 | POW = lambda m: lambda n: n(MUL(m))(ONE) 11 | -------------------------------------------------------------------------------- /computation/lambda_calculus/condition.py: -------------------------------------------------------------------------------- 1 | from .basic import FALSE, TRUE 2 | from .calculation import SUB 3 | 4 | IF = lambda b: b # reduced from `lambda b: lambda x: lambda y: b(x)(y)` 5 | NOT = lambda b: b(FALSE)(TRUE) 6 | 7 | IS_ZERO = lambda n: n(lambda _: FALSE)(TRUE) 8 | IS_LESS_OR_EQUAL = lambda m: lambda n: IS_ZERO(SUB(m)(n)) 9 | IS_LESS = lambda m: lambda n: NOT(IS_LESS_OR_EQUAL(n)(m)) 10 | -------------------------------------------------------------------------------- /computation/lambda_calculus/func.py: -------------------------------------------------------------------------------- 1 | from .calculation import INCREMENT, SUB 2 | from .condition import IF, IS_LESS, IS_LESS_OR_EQUAL 3 | from .struct import EMPTY, FIRST, IS_EMPTY, REST, UNSHIFT 4 | 5 | # Y combinator equiv 6 | Z_COMBINATOR = lambda f: (lambda x: x(x))(lambda x: f(lambda *args: x(x)(*args))) 7 | 8 | 9 | # recursive algorithms, see equivalent python code in test_lambda.py 10 | MOD = Z_COMBINATOR( 11 | lambda mod: lambda m: lambda n: IF(IS_LESS_OR_EQUAL(n)(m))( 12 | lambda *args: mod(SUB(m)(n))(n)(*args) 13 | )(m) 14 | ) 15 | 16 | RANGE = Z_COMBINATOR( 17 | lambda range: lambda m: lambda n: IF(IS_LESS(m)(n))( 18 | lambda *args: UNSHIFT(range(INCREMENT(m))(n))(m)(*args) 19 | )(EMPTY) 20 | ) 21 | CLOSED_RANGE = lambda m: lambda n: RANGE(m)(INCREMENT(n)) 22 | 23 | FOLD = Z_COMBINATOR( 24 | lambda fold: lambda l: lambda init: lambda func: IF(IS_EMPTY(l))(init)( 25 | lambda *args: func(FIRST(l))(fold(REST(l))(init)(func))(*args) 26 | ) 27 | ) 28 | 29 | MAP = lambda l: lambda func: FOLD(l)(EMPTY)( 30 | lambda x: lambda result: UNSHIFT(result)(func(x)) 31 | ) 32 | -------------------------------------------------------------------------------- /computation/lambda_calculus/generics.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar 2 | 3 | # generics 4 | T = TypeVar("T") 5 | IdentityF = Callable[[T], T] 6 | IntF = IdentityF[int] 7 | BoolF = IdentityF[bool] 8 | Endofunctor = Callable[[Callable[[T], T]], Callable[[T], T]] # IdentityF[IdentityF] 9 | ReductionFunctor = Callable[ 10 | [Callable[[Callable[[T], T]], Callable[[T], T]]], T 11 | ] # Callable[[Endofunctor], T] 12 | -------------------------------------------------------------------------------- /computation/lambda_calculus/number.py: -------------------------------------------------------------------------------- 1 | from .basic import ONE 2 | from .calculation import ADD, INCREMENT, MUL, POW 3 | 4 | # Church numbers 5 | # n(F) means call F n times, e.g. THREE(F) = F(F(F(x))) 6 | TWO = INCREMENT(ONE) # == lambda f: lambda x: f(f(x)) 7 | THREE = INCREMENT(TWO) # == lambda f: lambda x: f(f(f(x))) 8 | FOUR = ADD(TWO)(TWO) 9 | FIVE = INCREMENT(FOUR) 10 | SEVEN = ADD(TWO)(FIVE) 11 | TEN = MUL(TWO)(FIVE) 12 | FIFTEEN = MUL(THREE)(FIVE) 13 | HUNDRED = POW(TEN)(TWO) 14 | -------------------------------------------------------------------------------- /computation/lambda_calculus/reduction.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Generator 2 | 3 | from .generics import BoolF, Endofunctor, IntF, ReductionFunctor, T 4 | from .struct import FIRST, IS_EMPTY, REST 5 | 6 | 7 | def to_integer(lb: Callable[[IntF], IntF]) -> int: 8 | return lb(lambda n: n + 1)(0) 9 | 10 | 11 | def to_boolean(lb: Callable[[bool], BoolF]) -> bool: 12 | return lb(True)(False) 13 | 14 | 15 | def to_generator( 16 | lb: Endofunctor, callback: ReductionFunctor[T] 17 | ) -> Generator[T, None, None]: 18 | while not to_boolean(IS_EMPTY(lb)): 19 | first = FIRST(lb) 20 | yield callback(first) 21 | lb = REST(lb) 22 | 23 | 24 | def to_array(lb: Endofunctor, callback: ReductionFunctor[T]) -> list[T]: 25 | return list(to_generator(lb, callback)) 26 | 27 | 28 | def to_integer_array(lb: Endofunctor) -> list[int]: 29 | return to_array(lb, to_integer) 30 | -------------------------------------------------------------------------------- /computation/lambda_calculus/streams.py: -------------------------------------------------------------------------------- 1 | from .basic import ZERO 2 | from .calculation import ADD, INCREMENT 3 | from .func import Z_COMBINATOR 4 | from .struct import UNSHIFT 5 | 6 | # streams, see equivalent python code in test_lambda.py 7 | ZEROS = Z_COMBINATOR(lambda zeros: UNSHIFT(zeros)(ZERO)) 8 | UPWARDS_OF = Z_COMBINATOR( 9 | lambda upwards: lambda n: UNSHIFT(lambda *args: upwards(INCREMENT(n))(*args))(n) 10 | ) 11 | MULTIPLES_OF = lambda m: Z_COMBINATOR( 12 | lambda multiples: lambda n: UNSHIFT(lambda *args: multiples(ADD(m)(n))(*args))(n) 13 | )(m) 14 | -------------------------------------------------------------------------------- /computation/lambda_calculus/struct.py: -------------------------------------------------------------------------------- 1 | from .basic import FALSE, TRUE 2 | 3 | # pair 4 | PAIR = lambda x: lambda y: lambda f: f(x)(y) 5 | LEFT = lambda f: f(lambda x: lambda _: x) 6 | RIGHT = lambda f: f(lambda _: lambda y: y) 7 | IS_EMPTY = LEFT 8 | 9 | # list 10 | EMPTY = PAIR(TRUE)(TRUE) 11 | UNSHIFT = lambda l: lambda x: PAIR(FALSE)(PAIR(x)(l)) # Add x to front 12 | FIRST = lambda l: LEFT(RIGHT(l)) 13 | REST = lambda l: RIGHT(RIGHT(l)) 14 | -------------------------------------------------------------------------------- /computation/turing_machine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/computation/turing_machine/__init__.py -------------------------------------------------------------------------------- /computation/turing_machine/rule.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum, auto 3 | 4 | from ..exceptions import Unreachable 5 | from ..utils import detect 6 | from .tape import Tape, TMConfiguration 7 | 8 | 9 | class Direction(Enum): 10 | LEFT = auto() 11 | RIGHT = auto() 12 | 13 | 14 | @dataclass 15 | class TMRule: 16 | state: int 17 | character: str 18 | next_state: int 19 | write_character: str 20 | direction: Direction 21 | 22 | def applies_to(self, configuration: TMConfiguration) -> bool: 23 | return ( 24 | self.state == configuration.state 25 | and self.character == configuration.tape.middle 26 | ) 27 | 28 | def next_tape(self, configuration: TMConfiguration) -> Tape: 29 | written_tape = configuration.tape.write(self.write_character) 30 | if self.direction == Direction.LEFT: 31 | return written_tape.move_head_left 32 | elif self.direction == Direction.RIGHT: 33 | return written_tape.move_head_right 34 | else: 35 | raise Unreachable 36 | 37 | def follow(self, configuration: TMConfiguration) -> TMConfiguration: 38 | return TMConfiguration(self.next_state, self.next_tape(configuration)) 39 | 40 | 41 | @dataclass 42 | class DTMRulebook: 43 | rules: list[TMRule] 44 | 45 | def applies_to(self, configuration: TMConfiguration): 46 | return self.rule_for(configuration) is not None 47 | 48 | def rule_for(self, configuration: TMConfiguration): 49 | return detect(self.rules, lambda rule: rule.applies_to(configuration)) 50 | 51 | def next_configuration(self, configuration: TMConfiguration): 52 | return self.rule_for(configuration).follow(configuration) 53 | 54 | 55 | @dataclass 56 | class DTM: 57 | current_configuration: TMConfiguration 58 | accept_states: list 59 | rulebook: DTMRulebook 60 | 61 | @property 62 | def accepting(self): 63 | return self.current_configuration.state in self.accept_states 64 | 65 | @property 66 | def is_stuck(self): 67 | return not self.accepting and not self.rulebook.applies_to( 68 | self.current_configuration 69 | ) 70 | 71 | def step(self): 72 | self.current_configuration = self.rulebook.next_configuration( 73 | self.current_configuration 74 | ) 75 | 76 | def run(self, debug: bool = False): 77 | if debug: 78 | print(self.current_configuration) 79 | 80 | while True: 81 | if self.accepting or self.is_stuck: 82 | return 83 | 84 | self.step() 85 | if debug: 86 | print(self.current_configuration) 87 | -------------------------------------------------------------------------------- /computation/turing_machine/tape.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Tape: 6 | left: list[str] 7 | middle: str 8 | right: list[str] 9 | blank: str = "_" 10 | 11 | def __str__(self): 12 | return f"" 13 | 14 | def write(self, character): 15 | return Tape(self.left, character, self.right, self.blank) 16 | 17 | @property 18 | def move_head_left(self): 19 | left = self.left[:-1] 20 | middle = self.left[-1] if self.left != [] else self.blank 21 | right = [self.middle] + self.right 22 | return Tape(left, middle, right, self.blank) 23 | 24 | @property 25 | def move_head_right(self): 26 | left = self.left + [self.middle] 27 | middle = self.right[0] if self.right != [] else self.blank 28 | right = self.right[1:] 29 | return Tape(left, middle, right, self.blank) 30 | 31 | 32 | @dataclass 33 | class TMConfiguration: 34 | state: int 35 | tape: Tape 36 | 37 | def __str__(self) -> str: 38 | return f"" 39 | -------------------------------------------------------------------------------- /computation/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional, TypeVar 2 | 3 | T = TypeVar("T") 4 | 5 | 6 | def detect(arr: list[T], func: Callable[[T], bool]) -> Optional[T]: 7 | # generator comprehension 8 | return next((i for i in arr if func(i)), None) 9 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asttokens" 5 | version = "2.4.1" 6 | description = "Annotate AST trees with source code positions" 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, 11 | {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, 12 | ] 13 | 14 | [package.dependencies] 15 | six = ">=1.12.0" 16 | 17 | [package.extras] 18 | astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] 19 | test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] 20 | 21 | [[package]] 22 | name = "colorama" 23 | version = "0.4.6" 24 | description = "Cross-platform colored terminal text." 25 | optional = false 26 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 27 | files = [ 28 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 29 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 30 | ] 31 | 32 | [[package]] 33 | name = "coverage" 34 | version = "7.6.8" 35 | description = "Code coverage measurement for Python" 36 | optional = false 37 | python-versions = ">=3.9" 38 | files = [ 39 | {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, 40 | {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, 41 | {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, 42 | {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, 43 | {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, 44 | {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, 45 | {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, 46 | {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, 47 | {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, 48 | {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, 49 | {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, 50 | {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, 51 | {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, 52 | {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, 53 | {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, 54 | {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, 55 | {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, 56 | {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, 57 | {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, 58 | {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, 59 | {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, 60 | {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, 61 | {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, 62 | {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, 63 | {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, 64 | {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, 65 | {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, 66 | {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, 67 | {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, 68 | {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, 69 | {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, 70 | {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, 71 | {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, 72 | {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, 73 | {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, 74 | {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, 75 | {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, 76 | {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, 77 | {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, 78 | {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, 79 | {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, 80 | {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, 81 | {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, 82 | {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, 83 | {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, 84 | {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, 85 | {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, 86 | {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, 87 | {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, 88 | {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, 89 | {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, 90 | {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, 91 | {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, 92 | {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, 93 | {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, 94 | {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, 95 | {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, 96 | {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, 97 | {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, 98 | {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, 99 | {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, 100 | {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, 101 | ] 102 | 103 | [package.dependencies] 104 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 105 | 106 | [package.extras] 107 | toml = ["tomli"] 108 | 109 | [[package]] 110 | name = "decorator" 111 | version = "5.1.1" 112 | description = "Decorators for Humans" 113 | optional = false 114 | python-versions = ">=3.5" 115 | files = [ 116 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 117 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 118 | ] 119 | 120 | [[package]] 121 | name = "exceptiongroup" 122 | version = "1.2.2" 123 | description = "Backport of PEP 654 (exception groups)" 124 | optional = false 125 | python-versions = ">=3.7" 126 | files = [ 127 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 128 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 129 | ] 130 | 131 | [package.extras] 132 | test = ["pytest (>=6)"] 133 | 134 | [[package]] 135 | name = "executing" 136 | version = "2.1.0" 137 | description = "Get the currently executing AST node of a frame, and other information" 138 | optional = false 139 | python-versions = ">=3.8" 140 | files = [ 141 | {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, 142 | {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, 143 | ] 144 | 145 | [package.extras] 146 | tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] 147 | 148 | [[package]] 149 | name = "iniconfig" 150 | version = "2.0.0" 151 | description = "brain-dead simple config-ini parsing" 152 | optional = false 153 | python-versions = ">=3.7" 154 | files = [ 155 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 156 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 157 | ] 158 | 159 | [[package]] 160 | name = "ipython" 161 | version = "8.31.0" 162 | description = "IPython: Productive Interactive Computing" 163 | optional = false 164 | python-versions = ">=3.10" 165 | files = [ 166 | {file = "ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6"}, 167 | {file = "ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b"}, 168 | ] 169 | 170 | [package.dependencies] 171 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 172 | decorator = "*" 173 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 174 | jedi = ">=0.16" 175 | matplotlib-inline = "*" 176 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} 177 | prompt_toolkit = ">=3.0.41,<3.1.0" 178 | pygments = ">=2.4.0" 179 | stack_data = "*" 180 | traitlets = ">=5.13.0" 181 | typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} 182 | 183 | [package.extras] 184 | all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] 185 | black = ["black"] 186 | doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"] 187 | kernel = ["ipykernel"] 188 | matplotlib = ["matplotlib"] 189 | nbconvert = ["nbconvert"] 190 | nbformat = ["nbformat"] 191 | notebook = ["ipywidgets", "notebook"] 192 | parallel = ["ipyparallel"] 193 | qtconsole = ["qtconsole"] 194 | test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] 195 | test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] 196 | 197 | [[package]] 198 | name = "jedi" 199 | version = "0.19.2" 200 | description = "An autocompletion tool for Python that can be used for text editors." 201 | optional = false 202 | python-versions = ">=3.6" 203 | files = [ 204 | {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, 205 | {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, 206 | ] 207 | 208 | [package.dependencies] 209 | parso = ">=0.8.4,<0.9.0" 210 | 211 | [package.extras] 212 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 213 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 214 | testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] 215 | 216 | [[package]] 217 | name = "lark" 218 | version = "1.2.2" 219 | description = "a modern parsing library" 220 | optional = false 221 | python-versions = ">=3.8" 222 | files = [ 223 | {file = "lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c"}, 224 | {file = "lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80"}, 225 | ] 226 | 227 | [package.extras] 228 | atomic-cache = ["atomicwrites"] 229 | interegular = ["interegular (>=0.3.1,<0.4.0)"] 230 | nearley = ["js2py"] 231 | regex = ["regex"] 232 | 233 | [[package]] 234 | name = "matplotlib-inline" 235 | version = "0.1.7" 236 | description = "Inline Matplotlib backend for Jupyter" 237 | optional = false 238 | python-versions = ">=3.8" 239 | files = [ 240 | {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, 241 | {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, 242 | ] 243 | 244 | [package.dependencies] 245 | traitlets = "*" 246 | 247 | [[package]] 248 | name = "mypy" 249 | version = "1.13.0" 250 | description = "Optional static typing for Python" 251 | optional = false 252 | python-versions = ">=3.8" 253 | files = [ 254 | {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, 255 | {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, 256 | {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, 257 | {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, 258 | {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, 259 | {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, 260 | {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, 261 | {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, 262 | {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, 263 | {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, 264 | {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, 265 | {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, 266 | {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, 267 | {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, 268 | {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, 269 | {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, 270 | {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, 271 | {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, 272 | {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, 273 | {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, 274 | {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, 275 | {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, 276 | {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, 277 | {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, 278 | {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, 279 | {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, 280 | {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, 281 | {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, 282 | {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, 283 | {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, 284 | {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, 285 | {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, 286 | ] 287 | 288 | [package.dependencies] 289 | mypy-extensions = ">=1.0.0" 290 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 291 | typing-extensions = ">=4.6.0" 292 | 293 | [package.extras] 294 | dmypy = ["psutil (>=4.0)"] 295 | faster-cache = ["orjson"] 296 | install-types = ["pip"] 297 | mypyc = ["setuptools (>=50)"] 298 | reports = ["lxml"] 299 | 300 | [[package]] 301 | name = "mypy-extensions" 302 | version = "1.0.0" 303 | description = "Type system extensions for programs checked with the mypy type checker." 304 | optional = false 305 | python-versions = ">=3.5" 306 | files = [ 307 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 308 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 309 | ] 310 | 311 | [[package]] 312 | name = "packaging" 313 | version = "24.2" 314 | description = "Core utilities for Python packages" 315 | optional = false 316 | python-versions = ">=3.8" 317 | files = [ 318 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 319 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 320 | ] 321 | 322 | [[package]] 323 | name = "parso" 324 | version = "0.8.4" 325 | description = "A Python Parser" 326 | optional = false 327 | python-versions = ">=3.6" 328 | files = [ 329 | {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, 330 | {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, 331 | ] 332 | 333 | [package.extras] 334 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 335 | testing = ["docopt", "pytest"] 336 | 337 | [[package]] 338 | name = "pexpect" 339 | version = "4.9.0" 340 | description = "Pexpect allows easy control of interactive console applications." 341 | optional = false 342 | python-versions = "*" 343 | files = [ 344 | {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, 345 | {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, 346 | ] 347 | 348 | [package.dependencies] 349 | ptyprocess = ">=0.5" 350 | 351 | [[package]] 352 | name = "pluggy" 353 | version = "1.5.0" 354 | description = "plugin and hook calling mechanisms for python" 355 | optional = false 356 | python-versions = ">=3.8" 357 | files = [ 358 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 359 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 360 | ] 361 | 362 | [package.extras] 363 | dev = ["pre-commit", "tox"] 364 | testing = ["pytest", "pytest-benchmark"] 365 | 366 | [[package]] 367 | name = "prompt-toolkit" 368 | version = "3.0.48" 369 | description = "Library for building powerful interactive command lines in Python" 370 | optional = false 371 | python-versions = ">=3.7.0" 372 | files = [ 373 | {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, 374 | {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, 375 | ] 376 | 377 | [package.dependencies] 378 | wcwidth = "*" 379 | 380 | [[package]] 381 | name = "ptyprocess" 382 | version = "0.7.0" 383 | description = "Run a subprocess in a pseudo terminal" 384 | optional = false 385 | python-versions = "*" 386 | files = [ 387 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 388 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 389 | ] 390 | 391 | [[package]] 392 | name = "pure-eval" 393 | version = "0.2.3" 394 | description = "Safely evaluate AST nodes without side effects" 395 | optional = false 396 | python-versions = "*" 397 | files = [ 398 | {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, 399 | {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, 400 | ] 401 | 402 | [package.extras] 403 | tests = ["pytest"] 404 | 405 | [[package]] 406 | name = "pygments" 407 | version = "2.18.0" 408 | description = "Pygments is a syntax highlighting package written in Python." 409 | optional = false 410 | python-versions = ">=3.8" 411 | files = [ 412 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 413 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 414 | ] 415 | 416 | [package.extras] 417 | windows-terminal = ["colorama (>=0.4.6)"] 418 | 419 | [[package]] 420 | name = "pytest" 421 | version = "8.3.3" 422 | description = "pytest: simple powerful testing with Python" 423 | optional = false 424 | python-versions = ">=3.8" 425 | files = [ 426 | {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, 427 | {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, 428 | ] 429 | 430 | [package.dependencies] 431 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 432 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 433 | iniconfig = "*" 434 | packaging = "*" 435 | pluggy = ">=1.5,<2" 436 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 437 | 438 | [package.extras] 439 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 440 | 441 | [[package]] 442 | name = "pytest-cov" 443 | version = "6.0.0" 444 | description = "Pytest plugin for measuring coverage." 445 | optional = false 446 | python-versions = ">=3.9" 447 | files = [ 448 | {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, 449 | {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, 450 | ] 451 | 452 | [package.dependencies] 453 | coverage = {version = ">=7.5", extras = ["toml"]} 454 | pytest = ">=4.6" 455 | 456 | [package.extras] 457 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 458 | 459 | [[package]] 460 | name = "ruff" 461 | version = "0.8.0" 462 | description = "An extremely fast Python linter and code formatter, written in Rust." 463 | optional = false 464 | python-versions = ">=3.7" 465 | files = [ 466 | {file = "ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea"}, 467 | {file = "ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b"}, 468 | {file = "ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a"}, 469 | {file = "ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99"}, 470 | {file = "ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c"}, 471 | {file = "ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9"}, 472 | {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362"}, 473 | {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df"}, 474 | {file = "ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3"}, 475 | {file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"}, 476 | {file = "ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2"}, 477 | {file = "ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70"}, 478 | {file = "ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd"}, 479 | {file = "ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426"}, 480 | {file = "ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468"}, 481 | {file = "ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f"}, 482 | {file = "ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6"}, 483 | {file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"}, 484 | ] 485 | 486 | [[package]] 487 | name = "six" 488 | version = "1.16.0" 489 | description = "Python 2 and 3 compatibility utilities" 490 | optional = false 491 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 492 | files = [ 493 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 494 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 495 | ] 496 | 497 | [[package]] 498 | name = "stack-data" 499 | version = "0.6.3" 500 | description = "Extract data from python stack frames and tracebacks for informative displays" 501 | optional = false 502 | python-versions = "*" 503 | files = [ 504 | {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, 505 | {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, 506 | ] 507 | 508 | [package.dependencies] 509 | asttokens = ">=2.1.0" 510 | executing = ">=1.2.0" 511 | pure-eval = "*" 512 | 513 | [package.extras] 514 | tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 515 | 516 | [[package]] 517 | name = "tomli" 518 | version = "2.1.0" 519 | description = "A lil' TOML parser" 520 | optional = false 521 | python-versions = ">=3.8" 522 | files = [ 523 | {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, 524 | {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, 525 | ] 526 | 527 | [[package]] 528 | name = "traitlets" 529 | version = "5.14.3" 530 | description = "Traitlets Python configuration system" 531 | optional = false 532 | python-versions = ">=3.8" 533 | files = [ 534 | {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, 535 | {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, 536 | ] 537 | 538 | [package.extras] 539 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 540 | test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] 541 | 542 | [[package]] 543 | name = "typing-extensions" 544 | version = "4.12.2" 545 | description = "Backported and Experimental Type Hints for Python 3.8+" 546 | optional = false 547 | python-versions = ">=3.8" 548 | files = [ 549 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 550 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 551 | ] 552 | 553 | [[package]] 554 | name = "wcwidth" 555 | version = "0.2.13" 556 | description = "Measures the displayed width of unicode strings in a terminal" 557 | optional = false 558 | python-versions = "*" 559 | files = [ 560 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 561 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 562 | ] 563 | 564 | [metadata] 565 | lock-version = "2.0" 566 | python-versions = "^3.10" 567 | content-hash = "5563b8edd801ff205efd51d9d08abee00142eb10d1446971ff367be52758f771" 568 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "computation-py" 3 | version = "0.1.0" 4 | # doc 5 | authors = ["Weiliang Li "] 6 | description = "Python implementation for Understanding Computation book." 7 | license = "MIT" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10" 11 | 12 | # 3rd party 13 | lark = "^1.2.2" 14 | 15 | [tool.poetry.group.dev.dependencies] 16 | ipython = "^8.31.0" 17 | mypy = "^1.13.0" 18 | ruff = "^0.8.0" 19 | 20 | [tool.poetry.group.test.dependencies] 21 | pytest = "^8.3.3" 22 | pytest-cov = "^6.0.0" 23 | 24 | [build-system] 25 | build-backend = "poetry.core.masonry.api" 26 | requires = ["poetry-core>=1.0.0"] 27 | 28 | [tool.pytest.ini_options] 29 | addopts = "--cov=computation" 30 | 31 | [tool.coverage.report] 32 | exclude_lines = [ 33 | # Have to re-enable the standard pragma 34 | "pragma: no cover", 35 | "def __", 36 | # Don't complain if tests don't hit defensive assertion code: 37 | "raise Unreachable", 38 | "raise NotImplementedError", 39 | "print", 40 | ] 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/tests/__init__.py -------------------------------------------------------------------------------- /tests/automata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/tests/automata/__init__.py -------------------------------------------------------------------------------- /tests/automata/test_dfa.py: -------------------------------------------------------------------------------- 1 | from computation.automata.dfa import DFA, DFADesign, DFARulebook 2 | from computation.automata.farule import FARule 3 | 4 | 5 | def test_dfa_rulebook(): 6 | rulebook = DFARulebook( 7 | [ 8 | FARule(1, "a", 2), 9 | FARule(1, "b", 1), 10 | FARule(2, "a", 2), 11 | FARule(2, "b", 3), 12 | FARule(3, "a", 3), 13 | FARule(3, "b", 3), 14 | ] 15 | ) 16 | assert rulebook.next_state(1, "a") == 2 17 | assert rulebook.next_state(1, "b") == 1 18 | assert rulebook.next_state(2, "b") == 3 19 | 20 | 21 | def test_dfa(): 22 | rulebook = DFARulebook( 23 | [ 24 | FARule(1, "a", 2), 25 | FARule(1, "b", 1), 26 | FARule(2, "a", 2), 27 | FARule(2, "b", 3), 28 | FARule(3, "a", 3), 29 | FARule(3, "b", 3), 30 | ] 31 | ) 32 | dfa = DFA(1, [3], rulebook) 33 | assert not (dfa.read_character("b").accepting) 34 | assert not (any([dfa.read_character("a").accepting for i in range(3)])) 35 | assert dfa.read_character("b").accepting 36 | 37 | dfa = DFA(1, [3], rulebook) 38 | assert dfa.read_string("baaab").accepting 39 | 40 | 41 | def test_dfa_design(): 42 | rulebook = DFARulebook( 43 | [ 44 | FARule(1, "a", 2), 45 | FARule(1, "b", 1), 46 | FARule(2, "a", 2), 47 | FARule(2, "b", 3), 48 | FARule(3, "a", 3), 49 | FARule(3, "b", 3), 50 | ] 51 | ) 52 | 53 | dfa_design = DFADesign(1, [3], rulebook) 54 | assert not (dfa_design.accepts("a")) 55 | assert not (dfa_design.accepts("baa")) 56 | assert dfa_design.accepts("baba") 57 | -------------------------------------------------------------------------------- /tests/automata/test_nfa.py: -------------------------------------------------------------------------------- 1 | from computation.automata.nfa import NFA, FARule, NFADesign, NFARulebook, NFASimulation 2 | 3 | 4 | def test_nfa_rulebook(): 5 | rulebook = NFARulebook( 6 | [ 7 | FARule(1, "a", 1), 8 | FARule(1, "b", 1), 9 | FARule(1, "b", 2), 10 | FARule(2, "a", 3), 11 | FARule(2, "b", 3), 12 | FARule(3, "a", 4), 13 | FARule(3, "b", 4), 14 | ] 15 | ) 16 | assert rulebook.next_states([1], "b") == set([1, 2]) 17 | assert rulebook.next_states([1, 2], "a") == set([1, 3]) 18 | assert rulebook.next_states([1, 3], "b") == set([1, 2, 4]) 19 | 20 | 21 | def test_nfa(): 22 | rulebook = NFARulebook( 23 | [ 24 | FARule(1, "a", 1), 25 | FARule(1, "b", 1), 26 | FARule(1, "b", 2), 27 | FARule(2, "a", 3), 28 | FARule(2, "b", 3), 29 | FARule(3, "a", 4), 30 | FARule(3, "b", 4), 31 | ] 32 | ) 33 | assert not (NFA([1], [4], rulebook).accepting) 34 | assert NFA([1, 2, 4], [4], rulebook).accepting 35 | 36 | nfa = NFA([1], [4], rulebook) 37 | assert not (nfa.accepting) 38 | assert not (nfa.read_character("b").accepting) 39 | assert not (nfa.read_character("a").accepting) 40 | assert nfa.read_character("b").accepting 41 | 42 | nfa = NFA([1], [4], rulebook) 43 | assert nfa.read_string("bbbbb").accepting 44 | 45 | 46 | def test_nfa_design(): 47 | rulebook = NFARulebook( 48 | [ 49 | FARule(1, "a", 1), 50 | FARule(1, "b", 1), 51 | FARule(1, "b", 2), 52 | FARule(2, "a", 3), 53 | FARule(2, "b", 3), 54 | FARule(3, "a", 4), 55 | FARule(3, "b", 4), 56 | ] 57 | ) 58 | nfa = NFADesign(1, [4], rulebook) 59 | 60 | assert nfa.accepts("bab") 61 | assert nfa.accepts("bbbbb") 62 | assert not (nfa.accepts("bbabb")) 63 | 64 | rulebook = NFARulebook( 65 | [ 66 | FARule(1, "a", 1), 67 | FARule(1, "a", 2), 68 | FARule(1, None, 2), 69 | FARule(2, "b", 3), 70 | FARule(3, "b", 1), 71 | FARule(3, None, 2), 72 | ] 73 | ) 74 | assert rulebook.alphabet == set(["a", "b"]) 75 | 76 | nfa_design = NFADesign(1, [3], rulebook) 77 | assert nfa_design.to_nfa.current_states == set([1, 2]) 78 | assert nfa_design.to_nfa_from([3]).current_states == set([2, 3]) 79 | 80 | nfa = nfa_design.to_nfa_from([2, 3]) 81 | nfa.read_character("b") 82 | assert nfa.current_states == set([1, 2, 3]) 83 | 84 | simulation = NFASimulation(nfa_design) 85 | assert simulation.next_state([1, 2], "a") == set([1, 2]) 86 | assert simulation.next_state([1, 2], "b") == set([3, 2]) 87 | assert simulation.next_state([3, 2], "b") == set([1, 3, 2]) 88 | assert simulation.next_state([1, 3, 2], "b") == set([1, 3, 2]) 89 | assert simulation.next_state([1, 3, 2], "a") == set([1, 2]) 90 | 91 | dfa_design = simulation.to_dfa_design 92 | assert not (dfa_design.accepts("aaa")) 93 | assert dfa_design.accepts("aab") 94 | assert dfa_design.accepts("bbbabb") 95 | 96 | 97 | def test_free_move(): 98 | rulebook = NFARulebook( 99 | [ 100 | FARule(1, None, 2), 101 | FARule(1, None, 4), 102 | FARule(2, "a", 3), 103 | FARule(3, "a", 2), 104 | FARule(4, "a", 5), 105 | FARule(5, "a", 6), 106 | FARule(6, "a", 4), 107 | ] 108 | ) 109 | assert rulebook.next_states([1], None) == set([2, 4]) 110 | assert rulebook.follow_free_moves([1]) == set([1, 2, 4]) 111 | 112 | nfa_design = NFADesign(1, [2, 4], rulebook) 113 | assert nfa_design.accepts("aaaaaa") 114 | assert nfa_design.accepts("aaa") 115 | assert nfa_design.accepts("aa") 116 | assert not (nfa_design.accepts("aaaaa")) 117 | assert not (nfa_design.accepts("a")) 118 | -------------------------------------------------------------------------------- /tests/automata/test_pattern.py: -------------------------------------------------------------------------------- 1 | from computation.automata.pattern import Choose, Concatenate, Empty, Literal, Repeat 2 | 3 | 4 | def test_pattern(): 5 | pattern = Repeat(Choose(Concatenate(Literal("a"), Literal("b")), Literal("a"))) 6 | assert repr(pattern) == "/(ab|a)*/" 7 | assert str(pattern) == "(ab|a)*" 8 | 9 | assert not (Empty().matches("a")) 10 | assert Literal("a").matches("a") 11 | 12 | pattern = Repeat(Concatenate(Literal("a"), Choose(Empty(), Literal("b")))) 13 | assert repr(pattern) == "/(a(|b))*/" 14 | assert pattern.matches("") 15 | assert pattern.matches("a") 16 | assert pattern.matches("ab") 17 | assert pattern.matches("aba") 18 | assert pattern.matches("abab") 19 | assert pattern.matches("abaab") 20 | assert not (pattern.matches("bbba")) 21 | 22 | 23 | def test_nfa_design(): 24 | nfa_d = Empty().to_nfa_design 25 | assert nfa_d.accepts("") 26 | assert not (nfa_d.accepts("a")) 27 | 28 | nfa_d = Literal("a").to_nfa_design 29 | assert not (nfa_d.accepts("")) 30 | assert nfa_d.accepts("a") 31 | assert not (nfa_d.accepts("b")) 32 | 33 | pattern = Concatenate(Literal("a"), Literal("b")) 34 | assert pattern.matches("ab") 35 | assert not (pattern.matches("a")) 36 | assert not (pattern.matches("b")) 37 | assert not (pattern.matches("abc")) 38 | 39 | pattern = Concatenate(Literal("a"), Concatenate(Literal("b"), Literal("c"))) 40 | assert pattern.matches("abc") 41 | assert not (pattern.matches("a")) 42 | assert not (pattern.matches("b")) 43 | assert not (pattern.matches("ab")) 44 | 45 | pattern = Choose(Literal("a"), Literal("b")) 46 | assert pattern.matches("a") 47 | assert pattern.matches("b") 48 | assert not (pattern.matches("c")) 49 | 50 | pattern = Repeat(Literal("a")) 51 | assert pattern.matches("") 52 | assert pattern.matches("a") 53 | assert pattern.matches("aa") 54 | assert pattern.matches("aaaaaaaaaa") 55 | assert not (pattern.matches("b")) 56 | -------------------------------------------------------------------------------- /tests/automata/test_pda.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from computation.automata.pda import ( 4 | DPDA, 5 | NPDA, 6 | DPDADesign, 7 | DPDARulebook, 8 | NPDADesign, 9 | NPDARulebook, 10 | PDAConfiguration, 11 | PDARule, 12 | Stack, 13 | ) 14 | 15 | # check parentheses 16 | rulebook = DPDARulebook( 17 | [ 18 | PDARule(1, "(", 2, "$", ["b", "$"]), 19 | PDARule(2, "(", 2, "b", ["b", "b"]), 20 | PDARule(2, ")", 2, "b", []), 21 | PDARule(2, None, 1, "$", ["$"]), 22 | ] 23 | ) 24 | 25 | 26 | def test_pda_rule(): 27 | rule = PDARule(1, "(", 2, "$", ["b", "$"]) 28 | configuration = PDAConfiguration(1, Stack(["$"])) 29 | assert rule.applies_to(configuration, "(") 30 | 31 | 32 | def test_pda_config(): 33 | config1 = PDAConfiguration(3, Stack(["$"])) 34 | config2 = PDAConfiguration(3, Stack(["$"])) 35 | assert config1 == config2 36 | assert set([config1, config2]) == set([config1]) 37 | 38 | 39 | def test_pda_rulebook(): 40 | configuration = PDAConfiguration(1, Stack(["$"])) 41 | configuration = rulebook.next_configuration(configuration, "(") 42 | assert configuration.stack == Stack(["$", "b"]) 43 | 44 | 45 | def test_dpda(): 46 | dpda = DPDA(PDAConfiguration(1, Stack(["$"])), [1], rulebook) 47 | assert dpda.accepting 48 | assert not (dpda.read_string("(()").accepting) 49 | assert dpda.current_configuration.state == 2 50 | 51 | with pytest.raises(RuntimeError): 52 | DPDARulebook([PDARule(1, None, 1, "$", ["$"])]).follow_free_moves( 53 | PDAConfiguration(1, Stack(["$"])) 54 | ) 55 | 56 | dpda = DPDA(PDAConfiguration(1, Stack(["$"])), [1], rulebook) 57 | assert not (dpda.read_string("(()(").accepting) 58 | assert dpda.read_string("))()").accepting 59 | 60 | dpda = DPDA(PDAConfiguration(1, Stack(["$"])), [1], rulebook) 61 | dpda.read_string("())") 62 | assert dpda.current_configuration.state == PDAConfiguration.STUCK_STATE 63 | assert not dpda.accepting 64 | assert dpda.is_stuck 65 | 66 | 67 | def test_dpda_design(): 68 | dpda_design = DPDADesign(1, "$", [1], rulebook) 69 | assert dpda_design.accepts("(((((((((())))))))))") 70 | assert dpda_design.accepts("()(())((()))(()(()))") 71 | assert not (dpda_design.accepts("(()(()(()()(()()))()")) 72 | assert not (dpda_design.accepts("())")) 73 | 74 | 75 | def test_npda_design(): 76 | rulebook = NPDARulebook( 77 | [ 78 | PDARule(1, "a", 1, "$", ["a", "$"]), 79 | PDARule(1, "a", 1, "a", ["a", "a"]), 80 | PDARule(1, "a", 1, "b", ["a", "b"]), 81 | PDARule(1, "b", 1, "$", ["b", "$"]), 82 | PDARule(1, "b", 1, "a", ["b", "a"]), 83 | PDARule(1, "b", 1, "b", ["b", "b"]), 84 | PDARule(1, None, 2, "$", ["$"]), 85 | PDARule(1, None, 2, "a", ["a"]), 86 | PDARule(1, None, 2, "b", ["b"]), 87 | PDARule(2, "a", 2, "a", []), 88 | PDARule(2, "b", 2, "b", []), 89 | PDARule(2, None, 3, "$", ["$"]), 90 | ] 91 | ) 92 | configuration = PDAConfiguration(1, Stack(["$"])) 93 | npda = NPDA([configuration], [3], rulebook) 94 | assert npda.accepting 95 | assert not (npda.read_string("abb").accepting) 96 | assert PDAConfiguration( 97 | 1, Stack(["$", "a", "b", "b"]) in npda.current_configurations 98 | ) 99 | assert npda.read_character("a").accepting 100 | assert PDAConfiguration( 101 | 1, Stack(["$", "a", "b", "b", "a"]) in npda.current_configurations 102 | ) 103 | npda_design = NPDADesign(1, "$", [3], rulebook) 104 | assert npda_design.accepts("abba") 105 | assert npda_design.accepts("babbaabbab") 106 | assert not (npda_design.accepts("abb")) 107 | -------------------------------------------------------------------------------- /tests/interpreter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/tests/interpreter/__init__.py -------------------------------------------------------------------------------- /tests/interpreter/test_expr.py: -------------------------------------------------------------------------------- 1 | from computation.interpreter.expressions import ( 2 | Add, 3 | Boolean, 4 | LessThan, 5 | Multiply, 6 | Number, 7 | Variable, 8 | ) 9 | 10 | 11 | def test_number(): 12 | assert Number(3) == Number(3) 13 | assert Number(3) != Number(4) 14 | assert Number(2) < Number(4) 15 | assert Number(4) > Number(2) 16 | 17 | 18 | def test_boolean(): 19 | assert Boolean(True) and Boolean(True) 20 | assert Boolean(True), Boolean(True) 21 | assert Boolean(False), Boolean(False) 22 | assert Boolean(True) != Boolean(False) 23 | 24 | 25 | def test_add(): 26 | expr = Add(Variable("x"), Variable("y")) 27 | en = {"x": Number(1), "y": Number(2)} 28 | while expr.reducible: 29 | expr = expr.reduce(en) 30 | assert expr == Number(3) 31 | 32 | 33 | def test_mul(): 34 | expr = Multiply(Variable("x"), Variable("y")) 35 | en = {"x": Number(3), "y": Number(4)} 36 | while expr.reducible: 37 | expr = expr.reduce(en) 38 | assert expr == Number(12) 39 | 40 | 41 | def test_less(): 42 | expr = LessThan(Variable("x"), Variable("y")) 43 | en = {"x": Number(1), "y": Number(3)} 44 | while expr.reducible: 45 | expr = expr.reduce(en) 46 | assert expr == Boolean(True) 47 | 48 | 49 | def test_type(): 50 | assert Number(23).evaluate({}) == Number(23) 51 | assert Variable("x").evaluate({"x": Number(23)}) == Number(23) 52 | assert LessThan(Add(Variable("x"), Number(2)), Variable("y")).evaluate( 53 | {"x": Number(2), "y": Number(5)} 54 | ) 55 | 56 | n1 = Number(5) 57 | b1 = Boolean(False) 58 | v1 = Variable("x") 59 | 60 | assert eval(n1.to_python)({}) == 5 61 | assert not eval(b1.to_python)({}) 62 | assert eval(v1.to_python)({"x": 7}) == 7 63 | 64 | 65 | def test_expr(): 66 | a1 = Add(Variable("x"), Number(1)) 67 | m1 = Multiply(Variable("x"), Number(9)) 68 | l1 = LessThan(Variable("x"), Variable("y")) 69 | 70 | assert eval(a1.to_python)({"x": 7}) == 8 71 | assert eval(m1.to_python)({"x": 9}) == 81 72 | assert eval(l1.to_python)({"x": 7, "y": 8}) 73 | -------------------------------------------------------------------------------- /tests/interpreter/test_interpreter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from lark.exceptions import UnexpectedCharacters 3 | 4 | from computation.interpreter.interpreter import Machine 5 | from computation.interpreter.parser import parse 6 | from computation.interpreter.statements import DoNothing 7 | 8 | 9 | def check_source(source: str, expected_env: dict, debug: bool = False): 10 | seq = parse(source) 11 | env = Machine(seq, {}, debug=debug).run().environment 12 | 13 | for k, v in env.items(): 14 | env[k] = v.value 15 | assert env == expected_env 16 | assert eval(seq.to_python)({}) == expected_env 17 | 18 | return seq 19 | 20 | 21 | def test_success(): 22 | source = " \n\n" 23 | seq = parse(source) 24 | assert seq == DoNothing() 25 | 26 | check_source(source, {}) 27 | 28 | source = """ 29 | x = 5 30 | x = x + 1 31 | x = x - 2 - 3 32 | x = x + 3 + 2 33 | """ 34 | check_source(source, {"x": 6}) 35 | 36 | source = """ 37 | x = 1 + 2 + 1 - 2 38 | y = x * 2 39 | """ 40 | check_source(source, {"x": 2, "y": 4}) 41 | 42 | source = """ 43 | x = 1 - (0 - 1) 44 | y = x * 1 45 | """ 46 | check_source(source, {"x": 2, "y": 2}) 47 | 48 | source = """ 49 | x = 5 50 | x = 1 + x * 2 51 | """ 52 | check_source(source, {"x": 11}) 53 | 54 | source = """ 55 | x = 5 56 | x = (1 + x) * 2 57 | """ 58 | check_source(source, {"x": 12}) 59 | 60 | source = """ 61 | x = 1 62 | while (x < 50) { 63 | x = x + 3 64 | } 65 | """ 66 | check_source(source, {"x": 52}) 67 | 68 | source = """ 69 | a = 1 70 | b = 1 71 | c = 0 72 | d = 0 73 | if (a < 2 && b < 2) { 74 | a = a + 1 75 | c = a 76 | } else { 77 | d = b 78 | } 79 | if (a + 1 < 1 * 3) { 80 | b = b + 1 81 | } else { 82 | d = d + 1 83 | } 84 | if (b == 1 || a < 2) { 85 | b = b + 1 86 | } 87 | """ 88 | check_source(source, {"a": 2, "b": 2, "c": 2, "d": 1}) 89 | 90 | source = """ 91 | a = 1 92 | b = 1 93 | if (a < 0 + 2 && b < 2) { 94 | a = a + 1 95 | } else { 96 | b = b + 1 97 | } 98 | if (a < 2 || b < 1) { 99 | b = b + 1 100 | } 101 | """ 102 | check_source(source, {"a": 2, "b": 1}) 103 | 104 | source = """ 105 | a = 0 106 | b = 0 107 | while (a < 5) { 108 | a = a + 1 109 | b = b + a + 1 + 1 110 | b = b - 1 111 | } 112 | """ 113 | check_source(source, {"a": 5, "b": 20}) 114 | 115 | 116 | def test_error(): 117 | with pytest.raises(UnexpectedCharacters): 118 | parse("a=0;b=1") 119 | -------------------------------------------------------------------------------- /tests/interpreter/test_stmt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from computation.interpreter.expressions import ( 4 | Add, 5 | Boolean, 6 | LessThan, 7 | Multiply, 8 | Number, 9 | Variable, 10 | ) 11 | from computation.interpreter.statements import Assign, DoNothing, If, Sequence, While 12 | 13 | 14 | def test_donothing(): 15 | assert DoNothing() == DoNothing() 16 | assert str(DoNothing()) == "do-nothing" 17 | assert DoNothing() != 1 18 | assert DoNothing().evaluate({"x": 1}) == {"x": 1} 19 | 20 | 21 | def test_assign(): 22 | st = Assign("x", Add(Variable("x"), Number(1))) 23 | assert str(st) == "x = x + 1" 24 | en = {"x": Number(2)} 25 | while st.reducible: 26 | st, en = st.reduce(en) 27 | assert en["x"] == Number(3) 28 | 29 | st = Assign("y", Add(Variable("x"), Number(1))) 30 | assert eval(st.to_python)({"x": 1}) == {"x": 1, "y": 2} 31 | 32 | 33 | def test_if_true(): 34 | st = If(Variable("x"), Assign("y", Number(1)), Assign("y", Number(2))) 35 | assert str(st) == "if (x) { y = 1 } else { y = 2 }" 36 | en = {"x": Boolean(True)} 37 | while st.reducible: 38 | st, en = st.reduce(en) 39 | assert en["y"] == Number(1) 40 | assert en["x"] == Boolean(True) 41 | 42 | 43 | def test_if_false(): 44 | st = If(Variable("x"), Assign("y", Number(1)), DoNothing()) 45 | en = {"x": Boolean(False)} 46 | while st.reducible: 47 | st, en = st.reduce(en) 48 | assert st == DoNothing() 49 | assert en["x"] == Boolean(False) 50 | 51 | 52 | def test_sequence(): 53 | seq = Sequence( 54 | Assign("x", Add(Number(1), Number(2))), 55 | Assign("y", Add(Variable("x"), Number(3))), 56 | ) 57 | assert str(seq) == "x = 1 + 2; y = x + 3" 58 | en = {} 59 | while seq.reducible: 60 | seq, en = seq.reduce(en) 61 | assert seq == DoNothing() 62 | assert en["x"] == Number(3) 63 | st = Sequence( 64 | Assign("x", Add(Number(1), Number(1))), 65 | Assign("y", Add(Variable("x"), Number(3))), 66 | ) 67 | en = st.evaluate({}) 68 | assert en["x"] == Number(2) 69 | assert en["y"] == Number(5) 70 | 71 | st = Sequence( 72 | Assign("x", Add(Number(1), Number(1))), 73 | Assign("y", Add(Variable("x"), Number(3))), 74 | ) 75 | assert eval(st.to_python)({"x": 2, "y": 1}) == {"x": 2, "y": 5} 76 | 77 | 78 | def test_while(): 79 | seq = While( 80 | LessThan(Variable("x"), Number(5)), 81 | Assign("x", Multiply(Variable("x"), Number(2))), 82 | ) 83 | en = {"x": Number(1)} 84 | assert str(seq) == "while (x < 5) { x = x * 2 }" 85 | while seq.reducible: 86 | seq, en = seq.reduce(en) 87 | assert en["x"] == Number(8) 88 | 89 | st = While( 90 | LessThan(Variable("x"), Number(1000)), 91 | Assign("x", Add(Variable("x"), Number(1))), 92 | ) 93 | en = st.evaluate({"x": Number(1)}) 94 | assert en["x"] == Number(1000) 95 | 96 | en = st.evaluate_with_recursion({"x": Number(500)}) 97 | assert en["x"] == Number(1000) 98 | 99 | with pytest.raises(RuntimeError): 100 | st.evaluate_with_recursion({"x": Number(-1000)}) 101 | 102 | st = While( 103 | LessThan(Variable("x"), Number(100)), 104 | Assign("x", Add(Variable("x"), Number(1))), 105 | ) 106 | assert eval(st.to_python)({"x": 1}) == {"x": 100} 107 | 108 | 109 | def test_if_true_and_false(): 110 | st = If( 111 | LessThan(Variable("x"), Number(5)), 112 | Assign("x", Number(2)), 113 | Assign("x", Multiply(Variable("x"), Variable("x"))), 114 | ) 115 | en = st.evaluate({"x": Number(2)}) 116 | assert en["x"] == Number(2) 117 | 118 | st = If( 119 | LessThan(Variable("x"), Number(5)), 120 | Assign("x", Number(2)), 121 | Assign("x", Multiply(Variable("x"), Variable("x"))), 122 | ) 123 | en = st.evaluate({"x": Number(10)}) 124 | assert en["x"] == Number(100) 125 | 126 | 127 | def test_if(): 128 | st = If( 129 | LessThan(Variable("x"), Number(5)), 130 | Assign("x", Number(2)), 131 | Assign("x", Multiply(Variable("x"), Variable("x"))), 132 | ) 133 | assert eval(st.to_python)({"x": 1}) == {"x": 2} 134 | -------------------------------------------------------------------------------- /tests/lambda_calculus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/tests/lambda_calculus/__init__.py -------------------------------------------------------------------------------- /tests/lambda_calculus/test_lambda.py: -------------------------------------------------------------------------------- 1 | from itertools import islice 2 | from typing import Callable, Generator, TypeVar 3 | 4 | from computation.lambda_calculus import ( 5 | ADD, 6 | CLOSED_RANGE, 7 | DECREMENT, 8 | EMPTY, 9 | FALSE, 10 | FIFTEEN, 11 | FIRST, 12 | FIVE, 13 | FOLD, 14 | FOUR, 15 | HUNDRED, 16 | IF, 17 | INCREMENT, 18 | IS_EMPTY, 19 | IS_LESS_OR_EQUAL, 20 | IS_ZERO, 21 | LEFT, 22 | MAP, 23 | MOD, 24 | MUL, 25 | MULTIPLES_OF, 26 | NOT, 27 | ONE, 28 | PAIR, 29 | RANGE, 30 | REST, 31 | RIGHT, 32 | SEVEN, 33 | SUB, 34 | TEN, 35 | THREE, 36 | TRUE, 37 | TWO, 38 | UNSHIFT, 39 | UPWARDS_OF, 40 | ZERO, 41 | ZEROS, 42 | to_boolean, 43 | to_generator, 44 | to_integer, 45 | to_integer_array, 46 | ) 47 | 48 | T = TypeVar("T") 49 | U = TypeVar("U") 50 | 51 | 52 | def test_reference_algorithms(): 53 | def mod(m: int, n: int) -> int: 54 | return mod(m - n, n) if m >= n else m 55 | 56 | def range_(m: int, n: int): 57 | # [m, n) 58 | return [m] + range_(m + 1, n) if m < n else [] 59 | 60 | def fold(arr: list[T], init: U, func: Callable[[T, U], U]) -> U: 61 | return func(arr[0], fold(arr[1:], init, func)) if arr else init 62 | 63 | def map_(arr: list[T], func: Callable[[T], T]) -> list[T]: 64 | return fold(arr, [], lambda x, l: [func(x)] + l) 65 | 66 | def unshift(gen: Generator[int, None, None], x: int): 67 | yield x 68 | yield from gen 69 | 70 | def zeros(): 71 | # infinite zeros 72 | yield from unshift(zeros(), 0) 73 | 74 | def upwards_of(n: int): 75 | # infinite, starting from n 76 | yield from unshift(upwards_of(n + 1), n) 77 | 78 | def multiples_of(m: int): 79 | # infinite, multiples of m 80 | # e.g. 2, 4, 6, 8, ... 81 | def g(n: int): 82 | yield from unshift(g(n + m), n) 83 | 84 | return g(m) 85 | 86 | assert mod(5, 3) == 2 87 | assert mod(15, 4) == 3 88 | assert range_(1, 3) == [1, 2] 89 | assert fold([1, 2, 3, 4, 5], 0, lambda x, y: x + y) == 15 90 | assert fold([1, 2, 3, 4, 5], 1, lambda x, y: x * y) == 120 91 | assert map_([], lambda _: 0) == [] 92 | assert map_([1, 2, 3], lambda x: x + 1) == [2, 3, 4] 93 | assert list(islice(zeros(), 100)) == [0] * 100 94 | assert list(islice(upwards_of(5), 100)) == list(range(5, 105)) 95 | assert list(islice(multiples_of(5), 3)) == [5, 10, 15] 96 | 97 | 98 | def test_number(): 99 | church_numbers = [ 100 | ZERO, 101 | ONE, 102 | TWO, 103 | THREE, 104 | FOUR, 105 | FIVE, 106 | SEVEN, 107 | TEN, 108 | FIFTEEN, 109 | HUNDRED, 110 | ] 111 | numbers = [0, 1, 2, 3, 4, 5, 7, 10, 15, 100] 112 | for church_number, number in zip(church_numbers, numbers): 113 | assert to_integer(church_number) == number 114 | 115 | 116 | def test_cond(): 117 | assert IF(TRUE)("happy")("sad") == "happy" 118 | assert IF(FALSE)("happy")("sad") == "sad" 119 | assert TRUE("happy")("sad") == "happy" 120 | assert FALSE("happy")("sad") == "sad" 121 | assert to_boolean(IS_ZERO(ZERO)) 122 | assert not to_boolean(IS_ZERO(ONE)) 123 | assert not to_boolean(NOT(IS_ZERO(ZERO))) 124 | assert to_boolean(NOT(IS_ZERO(ONE))) 125 | 126 | 127 | def test_calc(): 128 | my_pair = PAIR(ONE)(TWO) 129 | assert to_integer(LEFT(my_pair)) == 1 130 | assert to_integer(RIGHT(my_pair)) == 2 131 | assert to_integer(INCREMENT(ZERO)) == 1 132 | assert to_integer(INCREMENT(TWO)) == 3 133 | assert to_integer(DECREMENT(ONE)) == 0 134 | assert to_integer(DECREMENT(TWO)) == 1 135 | assert to_integer(DECREMENT(ZERO)) == 0 136 | assert to_integer(ADD(ONE)(TWO)) == 3 137 | assert to_integer(SUB(FIVE)(THREE)) == 2 138 | assert to_integer(SUB(THREE)(FIVE)) == 0 139 | 140 | assert to_boolean(IS_LESS_OR_EQUAL(TWO)(TWO)) 141 | assert to_boolean(IS_LESS_OR_EQUAL(TWO)(FIVE)) 142 | assert not (to_boolean(IS_LESS_OR_EQUAL(HUNDRED)(FIVE))) 143 | 144 | assert to_integer(MOD(THREE)(TWO)) == 1 145 | assert to_integer(MOD(TEN)(FOUR)) == 2 146 | assert to_integer(MOD(HUNDRED)(TWO)) == 0 147 | assert to_integer(MOD(HUNDRED)(THREE)) == 1 148 | 149 | 150 | def test_list(): 151 | my_list = UNSHIFT(UNSHIFT(UNSHIFT(EMPTY)(THREE))(TWO))(ONE) 152 | 153 | assert to_integer(FIRST(my_list)) == 1 154 | assert to_integer(FIRST(REST(my_list))) == 2 155 | assert to_integer(FIRST(REST(REST(my_list)))) == 3 156 | assert to_boolean(IS_EMPTY(EMPTY)) 157 | assert not to_boolean(IS_EMPTY(my_list)) 158 | 159 | assert to_integer_array(my_list) == [1, 2, 3] 160 | assert to_integer_array(RANGE(ONE)(FOUR)) == [1, 2, 3] 161 | assert to_integer_array(CLOSED_RANGE(ONE)(THREE)) == [1, 2, 3] 162 | 163 | 164 | def test_higher_order_func(): 165 | assert to_integer(FOLD(CLOSED_RANGE(ONE)(FIVE))(ZERO)(ADD)) == 15 166 | assert to_integer(FOLD(CLOSED_RANGE(ONE)(FIVE))(ONE)(MUL)) == 120 167 | 168 | one_to_three = MAP(RANGE(ZERO)(THREE))(INCREMENT) 169 | assert to_integer_array(one_to_three) == [1, 2, 3] 170 | 171 | 172 | def test_streams(): 173 | def test_gen(n: int): 174 | assert list(islice(to_generator(ZEROS, to_integer), n)) == [0] * n 175 | assert list(islice(to_generator(UPWARDS_OF(FIVE), to_integer), n)) == list( 176 | range(5, 5 + n) 177 | ) 178 | assert list(islice(to_generator(MULTIPLES_OF(TWO), to_integer), n)) == list( 179 | range(2, 2 * n + 2, 2) 180 | ) 181 | 182 | test_gen(0) 183 | test_gen(1) 184 | test_gen(10) 185 | test_gen(100) 186 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from computation.utils import detect 2 | 3 | 4 | def test_detect(): 5 | assert detect([1, 2, 3], lambda x: x > 1 and x < 3) == 2 6 | assert detect([1, 2, 3], lambda x: False) is None 7 | assert detect([1, 2, 3], lambda x: True) == 1 8 | assert detect([], lambda x: True) is None 9 | -------------------------------------------------------------------------------- /tests/turing_machine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kigawas/computation-py/7e96ef3ae50188550bc92a4aed41948534cf3696/tests/turing_machine/__init__.py -------------------------------------------------------------------------------- /tests/turing_machine/test_rule.py: -------------------------------------------------------------------------------- 1 | from computation.turing_machine.rule import ( 2 | DTM, 3 | Direction, 4 | DTMRulebook, 5 | Tape, 6 | TMConfiguration, 7 | TMRule, 8 | ) 9 | 10 | # increment binary number rulebook 11 | rulebook = DTMRulebook( 12 | [ 13 | TMRule(1, "0", 2, "1", Direction.RIGHT), 14 | TMRule(1, "1", 1, "0", Direction.LEFT), 15 | TMRule(1, "_", 2, "1", Direction.RIGHT), 16 | TMRule(2, "0", 2, "0", Direction.RIGHT), 17 | TMRule(2, "1", 2, "1", Direction.RIGHT), 18 | TMRule(2, "_", 3, "_", Direction.LEFT), 19 | ] 20 | ) 21 | 22 | 23 | def test_rule(): 24 | dtm = DTM( 25 | TMConfiguration(1, Tape(["1", "0", "1"], "1", ["_"])), 26 | [3], 27 | rulebook, 28 | ) 29 | dtm.run() 30 | assert dtm.current_configuration.state == 3 31 | assert dtm.current_configuration.tape == Tape(["1", "1", "0"], "0", ["_"]) 32 | 33 | dtm = DTM( 34 | TMConfiguration(1, Tape(["1", "2", "1"], "1", ["_"])), 35 | [3], 36 | rulebook, 37 | ) 38 | dtm.run() 39 | assert dtm.is_stuck 40 | -------------------------------------------------------------------------------- /tests/turing_machine/test_tape.py: -------------------------------------------------------------------------------- 1 | from computation.turing_machine.rule import Direction, TMRule 2 | from computation.turing_machine.tape import Tape, TMConfiguration 3 | 4 | 5 | def test_tape(): 6 | tape = Tape(["1", "0", "1"], "1", []) 7 | assert tape.move_head_left == Tape(["1", "0"], "1", ["1"]) 8 | assert tape.move_head_left.write("0") == Tape(["1", "0"], "0", ["1"]) 9 | assert tape.move_head_right == Tape(["1", "0", "1", "1"], "_", []) 10 | assert tape.move_head_right.write("0") == Tape(["1", "0", "1", "1"], "0", []) 11 | 12 | rule = TMRule(1, "0", 2, "1", Direction.RIGHT) 13 | config = TMConfiguration(1, Tape([], "0", [])) 14 | assert rule.follow(config) == TMConfiguration(2, Tape(["1"], "_", [])) 15 | --------------------------------------------------------------------------------