vp2py 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vp2py-3.0.0/PKG-INFO +13 -0
- vp2py-3.0.0/README.md +2 -0
- vp2py-3.0.0/pyproject.toml +28 -0
- vp2py-3.0.0/pyproject.toml.orig +27 -0
- vp2py-3.0.0/src/vp2py/__init__.py +3 -0
- vp2py-3.0.0/src/vp2py/cli.py +38 -0
- vp2py-3.0.0/src/vp2py/compiler.py +110 -0
- vp2py-3.0.0/src/vp2py/lib/__init__.py +12 -0
- vp2py-3.0.0/src/vp2py/lib/exceptions.py +2 -0
- vp2py-3.0.0/src/vp2py/lib/functions.py +44 -0
- vp2py-3.0.0/src/vp2py/lib/io.py +111 -0
- vp2py-3.0.0/src/vp2py/lib/model.py +274 -0
vp2py-3.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: vp2py
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: An implementation of a Varphi-to-Python compiler.
|
|
5
|
+
Author: Varphi
|
|
6
|
+
Author-email: Varphi <support@varphi-lang.com>
|
|
7
|
+
Requires-Dist: typer
|
|
8
|
+
Requires-Dist: varphi-devkit>=3.0.0
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# The Varphi-to-Python Compiler
|
|
13
|
+
This is the source code repository for a compiler that converts the [Varphi programming language](https://varphi-lang.com/) into Python.
|
vp2py-3.0.0/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vp2py"
|
|
3
|
+
version = "3.0.0"
|
|
4
|
+
description = "An implementation of a Varphi-to-Python compiler."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"typer",
|
|
9
|
+
"varphi-devkit>=3.0.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[project.authors]]
|
|
13
|
+
name = "Varphi"
|
|
14
|
+
email = "support@varphi-lang.com"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
vp2py = "vp2py.cli:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"black",
|
|
26
|
+
"pytest",
|
|
27
|
+
"vermin",
|
|
28
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vp2py"
|
|
3
|
+
version = "3.0.0"
|
|
4
|
+
description = "An implementation of a Varphi-to-Python compiler."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Varphi", email = "support@varphi-lang.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"typer",
|
|
12
|
+
"varphi-devkit>=3.0.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
vp2py = "vp2py.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"black",
|
|
25
|
+
"pytest",
|
|
26
|
+
"vermin"
|
|
27
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def vp2py(
|
|
6
|
+
input_file: Path = typer.Argument(
|
|
7
|
+
...,
|
|
8
|
+
exists=True,
|
|
9
|
+
file_okay=True,
|
|
10
|
+
dir_okay=False,
|
|
11
|
+
readable=True,
|
|
12
|
+
resolve_path=True,
|
|
13
|
+
help="Path to input Varphi source file",
|
|
14
|
+
),
|
|
15
|
+
debug: bool = typer.Option(
|
|
16
|
+
False,
|
|
17
|
+
"--debug/--no-debug",
|
|
18
|
+
help="Compile with debugging capabilities",
|
|
19
|
+
),
|
|
20
|
+
):
|
|
21
|
+
"""Compile a Varphi source code file to Python"""
|
|
22
|
+
from .compiler import VarphiToPythonCompiler
|
|
23
|
+
|
|
24
|
+
compiler = VarphiToPythonCompiler()
|
|
25
|
+
if debug:
|
|
26
|
+
compiler.toggle_debug()
|
|
27
|
+
|
|
28
|
+
# Read file, compile, and print result to stdout
|
|
29
|
+
result = compiler.compile(input_file.read_text(encoding="utf-8"))
|
|
30
|
+
typer.echo(result)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
typer.run(vp2py)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from varphi_devkit import (
|
|
2
|
+
VarphiCompiler,
|
|
3
|
+
BuiltinSymbol,
|
|
4
|
+
Variable,
|
|
5
|
+
Direction,
|
|
6
|
+
Character,
|
|
7
|
+
ReadWriteTupleElement,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
TEMPLATE = """\
|
|
11
|
+
from vp2py.lib import State, main
|
|
12
|
+
from varphi_devkit import BuiltinSymbol, Direction, Variable, Character, VarphiTransition
|
|
13
|
+
|
|
14
|
+
# --- State Registry ---
|
|
15
|
+
state_registry = {{
|
|
16
|
+
{state_registry_entries}
|
|
17
|
+
}}
|
|
18
|
+
|
|
19
|
+
# --- Transition Definitions ---
|
|
20
|
+
{instruction_definitions}
|
|
21
|
+
|
|
22
|
+
# --- Runtime Setup ---
|
|
23
|
+
initial_state = state_registry['{initial_state}']
|
|
24
|
+
k = {num_tapes}
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
import argparse
|
|
28
|
+
parser = argparse.ArgumentParser(description="Varphi Turing Machine Runtime")
|
|
29
|
+
parser.add_argument("--debug", action="store_true", default={debug_mode}, help="Enable step-by-step debug mode")
|
|
30
|
+
parser.add_argument("-b", "--blank-char", type=str, default="_", help="Character representing a BLANK cell in input/output (Default: _)")
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
# Pass the state_registry to main!
|
|
34
|
+
main(k, initial_state, state_registry, args.debug, args.blank_char)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VarphiToPythonCompiler(VarphiCompiler):
|
|
39
|
+
debug_mode: bool
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.debug_mode = False
|
|
44
|
+
|
|
45
|
+
def toggle_debug(self):
|
|
46
|
+
self.debug_mode = not self.debug_mode
|
|
47
|
+
|
|
48
|
+
def _format_symbol(self, s: ReadWriteTupleElement) -> str:
|
|
49
|
+
"""Converts devkit symbol objects to valid strings to inject into the compiled code."""
|
|
50
|
+
if s == BuiltinSymbol.BLANK:
|
|
51
|
+
return "BuiltinSymbol.BLANK"
|
|
52
|
+
if isinstance(s, Variable):
|
|
53
|
+
return f"Variable({s.id})"
|
|
54
|
+
if isinstance(s, Character):
|
|
55
|
+
return f"Character({repr(s.value)})"
|
|
56
|
+
raise ValueError(f"Unknown symbol type: {type(s)}")
|
|
57
|
+
|
|
58
|
+
def _format_direction(self, d: Direction) -> str:
|
|
59
|
+
"""Converts devkit direction enums to valid Python code strings."""
|
|
60
|
+
return f"Direction.{d.name}"
|
|
61
|
+
|
|
62
|
+
def _generate_compiled_program(self) -> str:
|
|
63
|
+
# Use the devkit's pre-discovered states directly
|
|
64
|
+
all_states = self.states
|
|
65
|
+
|
|
66
|
+
# Build the dictionary mapping strings to the State objects directly to avoid keyword collisions
|
|
67
|
+
registry_entries = "\n".join(
|
|
68
|
+
f" '{name}': State('{name}')," for name in all_states
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Map the pre-sorted IR transitions to VarphiTransition instantiations
|
|
72
|
+
instructions_code = []
|
|
73
|
+
for transitions in self.ir.values():
|
|
74
|
+
for t in transitions:
|
|
75
|
+
read_str = "(" + ", ".join(
|
|
76
|
+
self._format_symbol(s) for s in t.read_symbols
|
|
77
|
+
)
|
|
78
|
+
read_str += ",)" if len(t.read_symbols) == 1 else ")"
|
|
79
|
+
|
|
80
|
+
write_str = "(" + ", ".join(
|
|
81
|
+
self._format_symbol(s) for s in t.write_symbols
|
|
82
|
+
)
|
|
83
|
+
write_str += ",)" if len(t.write_symbols) == 1 else ")"
|
|
84
|
+
|
|
85
|
+
shift_str = "(" + ", ".join(
|
|
86
|
+
self._format_direction(d) for d in t.shift_directions
|
|
87
|
+
)
|
|
88
|
+
shift_str += ",)" if len(t.shift_directions) == 1 else ")"
|
|
89
|
+
|
|
90
|
+
code = (
|
|
91
|
+
f"state_registry['{t.current_state}'].add_transition(\n"
|
|
92
|
+
f" VarphiTransition(\n"
|
|
93
|
+
f" current_state='{t.current_state}',\n"
|
|
94
|
+
f" read_symbols={read_str},\n"
|
|
95
|
+
f" next_state='{t.next_state}',\n"
|
|
96
|
+
f" write_symbols={write_str},\n"
|
|
97
|
+
f" shift_directions={shift_str},\n"
|
|
98
|
+
f" line_number={t.line_number}\n"
|
|
99
|
+
f" )\n"
|
|
100
|
+
f")"
|
|
101
|
+
)
|
|
102
|
+
instructions_code.append(code)
|
|
103
|
+
|
|
104
|
+
return TEMPLATE.format(
|
|
105
|
+
state_registry_entries=registry_entries,
|
|
106
|
+
instruction_definitions="\n\n".join(instructions_code),
|
|
107
|
+
initial_state=self.initial_state,
|
|
108
|
+
num_tapes=self._tape_count,
|
|
109
|
+
debug_mode=self.debug_mode,
|
|
110
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .io import VarphiIO, DebugView
|
|
3
|
+
from .model import State, TuringMachine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(
|
|
7
|
+
k: int,
|
|
8
|
+
initial_state: State,
|
|
9
|
+
state_registry: dict,
|
|
10
|
+
debug: bool,
|
|
11
|
+
blank_char: str = "_",
|
|
12
|
+
) -> None:
|
|
13
|
+
io = VarphiIO.from_stdin(blank_char)
|
|
14
|
+
tm = TuringMachine(k, io.tapes, initial_state, state_registry)
|
|
15
|
+
time_complexity = 0
|
|
16
|
+
SEPARATOR = "—" * 60
|
|
17
|
+
|
|
18
|
+
while tm.peek():
|
|
19
|
+
if debug:
|
|
20
|
+
print(f"\n{SEPARATOR}", file=sys.stderr)
|
|
21
|
+
print(
|
|
22
|
+
f"STEP {time_complexity + 1} [State: {tm.state.name}]", file=sys.stderr
|
|
23
|
+
)
|
|
24
|
+
print(f"{SEPARATOR}", file=sys.stderr)
|
|
25
|
+
print(DebugView(tm, blank_char), file=sys.stderr)
|
|
26
|
+
try:
|
|
27
|
+
input("\n>> Press ENTER to step forward...")
|
|
28
|
+
except (KeyboardInterrupt, EOFError):
|
|
29
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
tm.step()
|
|
33
|
+
time_complexity += 1
|
|
34
|
+
|
|
35
|
+
if sys.stdout.isatty():
|
|
36
|
+
print(f"\n{SEPARATOR}", file=sys.stderr)
|
|
37
|
+
print(f"HALTED at state '{tm.state.name}'", file=sys.stderr)
|
|
38
|
+
print(f"Time taken: {time_complexity} steps", file=sys.stderr)
|
|
39
|
+
print(
|
|
40
|
+
f"Space used: {sum(head.space_complexity() for head in tm.heads)} cells",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
VarphiIO(tm.tapes).print(blank_char)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sys
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
from .model import Tape, TuringMachine
|
|
6
|
+
from .exceptions import VarphiRuntimeError
|
|
7
|
+
from varphi_devkit import BuiltinSymbol, Character
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class VarphiIO:
|
|
12
|
+
tapes: Tuple[Tape, ...]
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def from_stdin(blank_char: str) -> VarphiIO:
|
|
16
|
+
if sys.stdin.isatty():
|
|
17
|
+
print("Number of input tapes: ", end="", file=sys.stderr, flush=True)
|
|
18
|
+
|
|
19
|
+
header = sys.stdin.readline()
|
|
20
|
+
if not header:
|
|
21
|
+
return VarphiIO(tuple())
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
num_tapes = int(header.strip())
|
|
25
|
+
except ValueError:
|
|
26
|
+
raise VarphiRuntimeError(
|
|
27
|
+
f'Runtime Error: Expected an integer for number of tapes, got "{header.strip()}".'
|
|
28
|
+
)
|
|
29
|
+
if num_tapes <= 0:
|
|
30
|
+
raise VarphiRuntimeError(
|
|
31
|
+
f'Runtime Error: Expected a positive number of tapes, got "{num_tapes}".'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
tapes = []
|
|
35
|
+
for i in range(num_tapes):
|
|
36
|
+
if sys.stdin.isatty():
|
|
37
|
+
print(f"Tape {i + 1}: ", end="", file=sys.stderr, flush=True)
|
|
38
|
+
|
|
39
|
+
line = sys.stdin.readline().strip("\r\n")
|
|
40
|
+
|
|
41
|
+
parsed_tape = []
|
|
42
|
+
for char in line:
|
|
43
|
+
if char == blank_char:
|
|
44
|
+
parsed_tape.append(BuiltinSymbol.BLANK)
|
|
45
|
+
else:
|
|
46
|
+
parsed_tape.append(Character(char))
|
|
47
|
+
|
|
48
|
+
tapes.append(Tape(parsed_tape))
|
|
49
|
+
|
|
50
|
+
return VarphiIO(tuple(tapes))
|
|
51
|
+
|
|
52
|
+
def print(self, blank_char: str) -> None:
|
|
53
|
+
show_labels = sys.stdout.isatty()
|
|
54
|
+
if show_labels:
|
|
55
|
+
print("Number of tapes: ", end="", file=sys.stderr, flush=True)
|
|
56
|
+
|
|
57
|
+
print(len(self.tapes))
|
|
58
|
+
|
|
59
|
+
for i, tape in enumerate(self.tapes):
|
|
60
|
+
if show_labels:
|
|
61
|
+
print(f"Tape {i + 1}: ", end="", file=sys.stderr, flush=True)
|
|
62
|
+
print(tape.to_string(blank_char))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class DebugView:
|
|
67
|
+
machine: TuringMachine
|
|
68
|
+
blank_char: str
|
|
69
|
+
|
|
70
|
+
def __str__(self) -> str:
|
|
71
|
+
line_number = (
|
|
72
|
+
self.machine._next_transition.line_number
|
|
73
|
+
if self.machine._next_transition
|
|
74
|
+
else "?"
|
|
75
|
+
)
|
|
76
|
+
lines = [f"State: {self.machine.state.name} (Line {line_number})"]
|
|
77
|
+
|
|
78
|
+
max_radius = 0
|
|
79
|
+
for head in self.machine.heads:
|
|
80
|
+
if not head.tape._tape:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
dist_left = (
|
|
84
|
+
abs(head.index - head.tape._min_idx)
|
|
85
|
+
if head.tape._min_idx is not None
|
|
86
|
+
else 0
|
|
87
|
+
)
|
|
88
|
+
dist_right = (
|
|
89
|
+
abs(head.index - head.tape._max_idx)
|
|
90
|
+
if head.tape._max_idx is not None
|
|
91
|
+
else 0
|
|
92
|
+
)
|
|
93
|
+
max_radius = max(max_radius, dist_left, dist_right)
|
|
94
|
+
|
|
95
|
+
for i, head in enumerate(self.machine.heads):
|
|
96
|
+
start = head.index - max_radius
|
|
97
|
+
end = head.index + max_radius
|
|
98
|
+
|
|
99
|
+
chars = []
|
|
100
|
+
for idx in range(start, end + 1):
|
|
101
|
+
val = head.tape._tape.get(idx, BuiltinSymbol.BLANK)
|
|
102
|
+
val_str = self.blank_char if val == BuiltinSymbol.BLANK else val.value
|
|
103
|
+
|
|
104
|
+
if idx == head.index:
|
|
105
|
+
chars.append(f"[{val_str}]")
|
|
106
|
+
else:
|
|
107
|
+
chars.append(val_str)
|
|
108
|
+
|
|
109
|
+
lines.append(f"Tape {i + 1}: {''.join(chars)}")
|
|
110
|
+
|
|
111
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Tuple, Optional, Set
|
|
3
|
+
import random
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from varphi_devkit import (
|
|
7
|
+
BuiltinSymbol,
|
|
8
|
+
Direction,
|
|
9
|
+
Variable,
|
|
10
|
+
Character,
|
|
11
|
+
ReadWriteTupleElement,
|
|
12
|
+
VarphiTransition,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class State:
|
|
17
|
+
"""A state in the Turing machine."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
transitions: list[VarphiTransition]
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str) -> None:
|
|
23
|
+
"""Initialize a new state."""
|
|
24
|
+
self.name = name
|
|
25
|
+
self.transitions = []
|
|
26
|
+
|
|
27
|
+
def add_transition(self, transition: VarphiTransition) -> None:
|
|
28
|
+
"""Append a VarphiTransition to the state."""
|
|
29
|
+
self.transitions.append(transition)
|
|
30
|
+
|
|
31
|
+
def get_transition(
|
|
32
|
+
self, tape_readings: tuple[Character | BuiltinSymbol, ...]
|
|
33
|
+
) -> Optional[Tuple[VarphiTransition, Dict[Variable, Character | BuiltinSymbol]]]:
|
|
34
|
+
"""
|
|
35
|
+
Evaluate current tape readings against the state's transitions to find the highest-specificity match.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
tape_readings: A tuple of the actual characters currently read by the machine's heads.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A tuple containing the winning VarphiTransition and its variable bindings, or None if the machine halts as a result of this situation.
|
|
42
|
+
"""
|
|
43
|
+
candidates = []
|
|
44
|
+
best_specificity = None
|
|
45
|
+
|
|
46
|
+
for transition in self.transitions:
|
|
47
|
+
bindings = self._check_match(transition.read_symbols, tape_readings)
|
|
48
|
+
|
|
49
|
+
if bindings is not None:
|
|
50
|
+
# The transition rule applies
|
|
51
|
+
current_specificity = transition.specificity
|
|
52
|
+
|
|
53
|
+
if best_specificity is None:
|
|
54
|
+
best_specificity = current_specificity
|
|
55
|
+
|
|
56
|
+
# Once the best (lowest) specificity is set, we will only consider
|
|
57
|
+
# scores that are the same as that scores
|
|
58
|
+
# Since the transitions have been sorted by the devkit by specificity, we can
|
|
59
|
+
# stop collecting transitions once we see one that has a higher specificity than the
|
|
60
|
+
# best specificity
|
|
61
|
+
elif current_specificity > best_specificity:
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
candidates.append((transition, bindings))
|
|
65
|
+
|
|
66
|
+
if not candidates:
|
|
67
|
+
# No transition rules apply, so the machine will halt
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# Resolve nondeterministic ties
|
|
71
|
+
return random.choice(candidates)
|
|
72
|
+
|
|
73
|
+
def _check_match(
|
|
74
|
+
self,
|
|
75
|
+
pattern: tuple[ReadWriteTupleElement, ...],
|
|
76
|
+
readings: tuple[Character | BuiltinSymbol, ...],
|
|
77
|
+
) -> Optional[Dict[Variable, Character | BuiltinSymbol]]:
|
|
78
|
+
"""
|
|
79
|
+
Verify if a given pattern matches the current tape readings and extract variable bindings.
|
|
80
|
+
|
|
81
|
+
Returns the variable bindings (or an empty dictionary if there aren't variables) if the pattern applies,
|
|
82
|
+
otherwise returns None
|
|
83
|
+
"""
|
|
84
|
+
bindings = {}
|
|
85
|
+
|
|
86
|
+
for pattern_symbol, read_symbol in zip(pattern, readings):
|
|
87
|
+
if isinstance(pattern_symbol, Variable):
|
|
88
|
+
if pattern_symbol not in bindings:
|
|
89
|
+
# Bind the variable to the read symbol
|
|
90
|
+
bindings[pattern_symbol] = read_symbol
|
|
91
|
+
elif bindings[pattern_symbol] != read_symbol:
|
|
92
|
+
# We've bound the variable previously, but there's a mismatch
|
|
93
|
+
# This rule cannot possibly apply
|
|
94
|
+
return None
|
|
95
|
+
elif pattern_symbol != read_symbol:
|
|
96
|
+
# The pattern and read symbols don't match up, so the rule doesn't apply
|
|
97
|
+
return None
|
|
98
|
+
# If we're here, the rule applies. Return the variable bindings
|
|
99
|
+
return bindings
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
return f"State({self.name})"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Tape:
|
|
106
|
+
"""Represents a two-way infinite tape of the Turing machine."""
|
|
107
|
+
|
|
108
|
+
_tape: defaultdict[int, Character | BuiltinSymbol]
|
|
109
|
+
_min_idx: Optional[int]
|
|
110
|
+
_max_idx: Optional[int]
|
|
111
|
+
|
|
112
|
+
def __init__(self, initial_values: list[Character | BuiltinSymbol]) -> None:
|
|
113
|
+
self._tape = defaultdict(lambda: BuiltinSymbol.BLANK)
|
|
114
|
+
self._min_idx = None
|
|
115
|
+
self._max_idx = None
|
|
116
|
+
|
|
117
|
+
for i, val in enumerate(initial_values):
|
|
118
|
+
if val != BuiltinSymbol.BLANK:
|
|
119
|
+
self[i] = val
|
|
120
|
+
|
|
121
|
+
def __getitem__(self, index: int) -> Character | BuiltinSymbol:
|
|
122
|
+
self._update_bounds(index)
|
|
123
|
+
return self._tape[index]
|
|
124
|
+
|
|
125
|
+
def __setitem__(self, index: int, value: Character | BuiltinSymbol) -> None:
|
|
126
|
+
self._update_bounds(index)
|
|
127
|
+
self._tape[index] = value
|
|
128
|
+
|
|
129
|
+
def _update_bounds(self, index: int) -> None:
|
|
130
|
+
if self._min_idx is None or self._max_idx is None:
|
|
131
|
+
self._min_idx = index
|
|
132
|
+
self._max_idx = index
|
|
133
|
+
else:
|
|
134
|
+
self._max_idx = max(self._max_idx, index)
|
|
135
|
+
self._min_idx = min(self._min_idx, index)
|
|
136
|
+
|
|
137
|
+
def to_string(self, blank_char: str) -> str:
|
|
138
|
+
if self._min_idx is None or self._max_idx is None:
|
|
139
|
+
return ""
|
|
140
|
+
|
|
141
|
+
min_i, max_i = self._min_idx, self._max_idx
|
|
142
|
+
|
|
143
|
+
while max_i >= min_i and self._tape[max_i] == BuiltinSymbol.BLANK:
|
|
144
|
+
max_i -= 1
|
|
145
|
+
while min_i <= max_i and self._tape[min_i] == BuiltinSymbol.BLANK:
|
|
146
|
+
min_i += 1
|
|
147
|
+
|
|
148
|
+
if min_i > max_i:
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
return "".join(
|
|
152
|
+
blank_char if self._tape[i] == BuiltinSymbol.BLANK else self._tape[i].value
|
|
153
|
+
for i in range(min_i, max_i + 1)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def is_empty(self) -> bool:
|
|
158
|
+
return self._min_idx is None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Head:
|
|
162
|
+
"""Represents a read/write head positioned on a specific tape."""
|
|
163
|
+
|
|
164
|
+
tape: Tape
|
|
165
|
+
index: int
|
|
166
|
+
user_input_cell_range: Optional[tuple[int, int]]
|
|
167
|
+
new_accessed_cells: Set[int]
|
|
168
|
+
|
|
169
|
+
def __init__(self, tape: Tape) -> None:
|
|
170
|
+
self.tape = tape
|
|
171
|
+
self.index = 0
|
|
172
|
+
self.new_accessed_cells = set()
|
|
173
|
+
|
|
174
|
+
if tape.is_empty:
|
|
175
|
+
self.user_input_cell_range = None
|
|
176
|
+
else:
|
|
177
|
+
self.user_input_cell_range = (tape._min_idx, tape._max_idx)
|
|
178
|
+
|
|
179
|
+
def move(self, direction: Direction) -> None:
|
|
180
|
+
if direction == Direction.LEFT:
|
|
181
|
+
self.index -= 1
|
|
182
|
+
elif direction == Direction.RIGHT:
|
|
183
|
+
self.index += 1
|
|
184
|
+
|
|
185
|
+
def read(self) -> Character | BuiltinSymbol:
|
|
186
|
+
self._check_access()
|
|
187
|
+
return self.tape[self.index]
|
|
188
|
+
|
|
189
|
+
def write(self, value: Character | BuiltinSymbol) -> None:
|
|
190
|
+
self._check_access()
|
|
191
|
+
self.tape[self.index] = value
|
|
192
|
+
|
|
193
|
+
def _check_access(self):
|
|
194
|
+
if self.user_input_cell_range is None:
|
|
195
|
+
self.new_accessed_cells.add(self.index)
|
|
196
|
+
else:
|
|
197
|
+
start, end = self.user_input_cell_range
|
|
198
|
+
if self.index < start or self.index > end:
|
|
199
|
+
self.new_accessed_cells.add(self.index)
|
|
200
|
+
|
|
201
|
+
def space_complexity(self) -> int:
|
|
202
|
+
return len(self.new_accessed_cells)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TuringMachine:
|
|
206
|
+
tapes: tuple[Tape, ...]
|
|
207
|
+
heads: tuple[Head, ...]
|
|
208
|
+
state: State
|
|
209
|
+
state_registry: dict[str, State]
|
|
210
|
+
_next_transition: Optional[VarphiTransition]
|
|
211
|
+
_current_bindings: dict[Variable, Character | BuiltinSymbol]
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
k: int,
|
|
216
|
+
tapes: tuple[Tape, ...],
|
|
217
|
+
initial_state: State,
|
|
218
|
+
state_registry: dict[str, State],
|
|
219
|
+
) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Initialize the machine.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
k: The required number of tapes for this machine configuration.
|
|
225
|
+
tapes: A tuple of initial tapes provided by user input. Missing tapes are generated as blanks.
|
|
226
|
+
initial_state: The entry state of the machine.
|
|
227
|
+
state_registry: A dictionary mapping state names to live State objects.
|
|
228
|
+
"""
|
|
229
|
+
self.tapes = tapes # Keep all tapes so they are printed at the end
|
|
230
|
+
missing_count = k - len(self.tapes)
|
|
231
|
+
if missing_count > 0:
|
|
232
|
+
self.tapes += tuple(Tape([]) for _ in range(missing_count))
|
|
233
|
+
|
|
234
|
+
# We only make heads for the first k tapes (we safely ignore excess tapes during execution)
|
|
235
|
+
self.heads = tuple(Head(t) for t in self.tapes[:k])
|
|
236
|
+
self.state = initial_state
|
|
237
|
+
self.state_registry = state_registry
|
|
238
|
+
self._next_transition = None
|
|
239
|
+
self._current_bindings = {}
|
|
240
|
+
|
|
241
|
+
def peek(self) -> bool:
|
|
242
|
+
"""
|
|
243
|
+
Preview the next transition by evaluating current tape readings against the state.
|
|
244
|
+
This will select and "arm" the machine with the next transition, but won't execute it
|
|
245
|
+
Return True if a next transition has been selected, False if the machine will halt
|
|
246
|
+
"""
|
|
247
|
+
reads = tuple(h.read() for h in self.heads)
|
|
248
|
+
result = self.state.get_transition(reads)
|
|
249
|
+
|
|
250
|
+
if result is None:
|
|
251
|
+
# No transition applies; halt
|
|
252
|
+
self._next_transition = None
|
|
253
|
+
self._current_bindings = {}
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
self._next_transition, self._current_bindings = result
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
def step(self) -> None:
|
|
260
|
+
"""Execute the transition locked in by the previous call to peek()."""
|
|
261
|
+
transition = self._next_transition
|
|
262
|
+
bindings = self._current_bindings
|
|
263
|
+
|
|
264
|
+
if transition is None:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# Perform the string-to-object dictionary lookup here
|
|
268
|
+
self.state = self.state_registry[transition.next_state]
|
|
269
|
+
|
|
270
|
+
for i, head in enumerate(self.heads):
|
|
271
|
+
sym = transition.write_symbols[i]
|
|
272
|
+
val_to_write = bindings[sym] if isinstance(sym, Variable) else sym
|
|
273
|
+
head.write(val_to_write)
|
|
274
|
+
head.move(transition.shift_directions[i])
|