varphi-devkit 1.5.0__tar.gz → 2.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.
- varphi_devkit-2.0.0/.github/workflows/pipeline.yml +58 -0
- varphi_devkit-2.0.0/.gitignore +13 -0
- varphi_devkit-2.0.0/.python-version +1 -0
- varphi_devkit-2.0.0/PKG-INFO +7 -0
- varphi_devkit-2.0.0/README.md +0 -0
- varphi_devkit-2.0.0/grammar/Varphi.g4 +54 -0
- varphi_devkit-2.0.0/pyproject.toml +33 -0
- varphi_devkit-2.0.0/scripts/build.py +69 -0
- varphi_devkit-2.0.0/src/varphi_devkit/__init__.py +38 -0
- varphi_devkit-2.0.0/src/varphi_devkit/compiler.py +185 -0
- varphi_devkit-2.0.0/src/varphi_devkit/exceptions.py +130 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/Varphi.interp +47 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/Varphi.tokens +21 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/VarphiLexer.interp +59 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/VarphiLexer.py +97 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/VarphiLexer.tokens +21 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/VarphiListener.py +84 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/VarphiParser.py +655 -0
- varphi_devkit-2.0.0/src/varphi_devkit/parser/__init__.py +5 -0
- varphi_devkit-2.0.0/src/varphi_devkit/py.typed +0 -0
- varphi_devkit-2.0.0/tests/test_devkit.py +155 -0
- varphi_devkit-2.0.0/uv.lock +92 -0
- varphi_devkit-1.5.0/LICENSE +0 -28
- varphi_devkit-1.5.0/PKG-INFO +0 -200
- varphi_devkit-1.5.0/README.md +0 -178
- varphi_devkit-1.5.0/pyproject.toml +0 -110
- varphi_devkit-1.5.0/src/varphi_devkit/__init__.py +0 -33
- varphi_devkit-1.5.0/src/varphi_devkit/compilation.py +0 -113
- varphi_devkit-1.5.0/src/varphi_devkit/model.py +0 -55
- varphi_devkit-1.5.0/src/varphi_devkit/syntax/Varphi.g4 +0 -24
- varphi_devkit-1.5.0/src/varphi_devkit/syntax/__init__.py +0 -28
- varphi_devkit-1.5.0/src/varphi_devkit/syntax/antlr/VarphiLexer.py +0 -76
- varphi_devkit-1.5.0/src/varphi_devkit/syntax/antlr/VarphiListener.py +0 -30
- varphi_devkit-1.5.0/src/varphi_devkit/syntax/antlr/VarphiParser.py +0 -180
- varphi_devkit-1.5.0/src/varphi_devkit/syntax/antlr/__init__.py +0 -21
- varphi_devkit-1.5.0/src/varphi_devkit/syntax/error_listener.py +0 -79
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: CI/CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: Test
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-java@v4
|
|
18
|
+
with:
|
|
19
|
+
distribution: 'temurin'
|
|
20
|
+
java-version: '17'
|
|
21
|
+
|
|
22
|
+
- uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Generate ANTLR Parser
|
|
27
|
+
run: uv run python scripts/build.py
|
|
28
|
+
|
|
29
|
+
- name: Run Pytest
|
|
30
|
+
run: uv run pytest
|
|
31
|
+
|
|
32
|
+
release:
|
|
33
|
+
name: Build & Publish
|
|
34
|
+
needs: test
|
|
35
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment: pypi
|
|
38
|
+
permissions:
|
|
39
|
+
id-token: write
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- uses: actions/setup-java@v4
|
|
45
|
+
with:
|
|
46
|
+
distribution: 'temurin'
|
|
47
|
+
java-version: '17'
|
|
48
|
+
|
|
49
|
+
- uses: astral-sh/setup-uv@v5
|
|
50
|
+
|
|
51
|
+
- name: Generate ANTLR Parser
|
|
52
|
+
run: uv run python scripts/build.py
|
|
53
|
+
|
|
54
|
+
- name: Build Package
|
|
55
|
+
run: uv build
|
|
56
|
+
|
|
57
|
+
- name: Publish to PyPI
|
|
58
|
+
run: uv publish
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
grammar Varphi;
|
|
2
|
+
|
|
3
|
+
// ======================================================
|
|
4
|
+
// PARSER RULES
|
|
5
|
+
// ======================================================
|
|
6
|
+
|
|
7
|
+
program : NEWLINE* transition (NEWLINE+ transition)* NEWLINE* EOF;
|
|
8
|
+
|
|
9
|
+
transition : current_state=state_id read_symbols next_state=state_id write_symbols shift_directions;
|
|
10
|
+
|
|
11
|
+
read_symbols : LPAREN symbol (COMMA symbol)* RPAREN;
|
|
12
|
+
|
|
13
|
+
write_symbols : LPAREN symbol (COMMA symbol)* RPAREN;
|
|
14
|
+
|
|
15
|
+
shift_directions : LPAREN direction (COMMA direction)* RPAREN;
|
|
16
|
+
|
|
17
|
+
state_id : ID | ALPHANUM | LEFT_KW | RIGHT_KW | STAY_KW | BLANK_KW;
|
|
18
|
+
|
|
19
|
+
// Updated: Symbol can now be a Variable instead of STAR
|
|
20
|
+
symbol : ALPHANUM | BLANK_KW | VARIABLE;
|
|
21
|
+
|
|
22
|
+
direction : LEFT_KW | RIGHT_KW | STAY_KW;
|
|
23
|
+
|
|
24
|
+
// ======================================================
|
|
25
|
+
// LEXER RULES
|
|
26
|
+
// ======================================================
|
|
27
|
+
|
|
28
|
+
LPAREN : '(';
|
|
29
|
+
RPAREN : ')';
|
|
30
|
+
COMMA : ',';
|
|
31
|
+
// STAR rule removed
|
|
32
|
+
|
|
33
|
+
// New Lexer Rule for Variables (e.g., $x, $val_1)
|
|
34
|
+
VARIABLE : '$' [a-zA-Z0-9_]+;
|
|
35
|
+
|
|
36
|
+
// Keywords
|
|
37
|
+
LEFT_KW : 'LEFT';
|
|
38
|
+
RIGHT_KW : 'RIGHT';
|
|
39
|
+
STAY_KW : 'STAY';
|
|
40
|
+
BLANK_KW : 'BLANK';
|
|
41
|
+
|
|
42
|
+
// ------------------------------------------------------
|
|
43
|
+
// PRIORITY RULES
|
|
44
|
+
// ------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
ALPHANUM : [a-zA-Z0-9];
|
|
47
|
+
ID : [a-zA-Z0-9_]+;
|
|
48
|
+
|
|
49
|
+
// ------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
COMMENT : '//' ~[\r\n]* -> skip;
|
|
52
|
+
MULTI_COMMENT : '/*' .*? '*/' -> skip;
|
|
53
|
+
WS : [ \t]+ -> skip;
|
|
54
|
+
NEWLINE : '\r'? '\n';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "varphi-devkit"
|
|
3
|
+
version = "2.0.0"
|
|
4
|
+
description = "Development kit for the Varphi language"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"antlr4-python3-runtime>=4.13.2",
|
|
9
|
+
]
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Hassan El-Sheikha", email = "hassan.elsheikha@utoronto.ca" }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
20
|
+
"src/varphi_devkit/parser" = "varphi_devkit/parser"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.sdist.force-include]
|
|
23
|
+
"src/varphi_devkit/parser" = "src/varphi_devkit/parser"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
pythonpath = ["src"]
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.0.0",
|
|
33
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import urllib.request
|
|
5
|
+
import tempfile
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# Configuration
|
|
10
|
+
ANTLR_DOWNLOAD_URL = "https://www.antlr.org/download/antlr-4.13.2-complete.jar"
|
|
11
|
+
|
|
12
|
+
# Paths
|
|
13
|
+
ROOT = Path(__file__).parent.parent
|
|
14
|
+
GRAMMAR_FILE = ROOT / "grammar" / "Varphi.g4"
|
|
15
|
+
# Using the path from your snippet:
|
|
16
|
+
OUTPUT_DIR = ROOT / "src" / "varphi_devkit" / "parser"
|
|
17
|
+
|
|
18
|
+
def check_java_installed():
|
|
19
|
+
if not shutil.which("java"):
|
|
20
|
+
print("Error: 'java' executable not found in PATH.")
|
|
21
|
+
print("Please install Java and ensure it is added to your environment variables.")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
def generate_parser():
|
|
25
|
+
check_java_installed()
|
|
26
|
+
|
|
27
|
+
if not GRAMMAR_FILE.exists():
|
|
28
|
+
print(f"Error: Grammar file not found at {GRAMMAR_FILE}")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
(OUTPUT_DIR / "__init__.py").touch()
|
|
33
|
+
|
|
34
|
+
print(f"Downloading ANTLR jar to tempfile...")
|
|
35
|
+
|
|
36
|
+
# Create a temp file. delete=False is required for Windows compatibility
|
|
37
|
+
# so we can close the file before Java tries to open it.
|
|
38
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".jar") as tmp_jar:
|
|
39
|
+
try:
|
|
40
|
+
# 1. Download
|
|
41
|
+
with urllib.request.urlopen(ANTLR_DOWNLOAD_URL) as response:
|
|
42
|
+
shutil.copyfileobj(response, tmp_jar)
|
|
43
|
+
|
|
44
|
+
# Close the file handle so Java can read it
|
|
45
|
+
tmp_jar.close()
|
|
46
|
+
|
|
47
|
+
# 2. Run Java
|
|
48
|
+
cmd = [
|
|
49
|
+
"java",
|
|
50
|
+
"-jar", tmp_jar.name,
|
|
51
|
+
"-Dlanguage=Python3",
|
|
52
|
+
"-o", str(OUTPUT_DIR),
|
|
53
|
+
str(GRAMMAR_FILE)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
print(f"Generating parser...")
|
|
57
|
+
subprocess.run(cmd, check=True)
|
|
58
|
+
print("Success. Parser generated in src/varphi_devkit/parser/")
|
|
59
|
+
|
|
60
|
+
except subprocess.CalledProcessError as e:
|
|
61
|
+
print("Error during ANTLR generation.")
|
|
62
|
+
sys.exit(e.returncode)
|
|
63
|
+
finally:
|
|
64
|
+
# 3. Cleanup: Delete the temp file manually
|
|
65
|
+
if os.path.exists(tmp_jar.name):
|
|
66
|
+
os.remove(tmp_jar.name)
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
generate_parser()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Varphi Development Kit.
|
|
3
|
+
|
|
4
|
+
A framework for building compilers, interpreters, and analysis tools for the Varphi language.
|
|
5
|
+
This package handles the complexity of parsing, validation, and variable canonicalization, providing a
|
|
6
|
+
convenient abstraction layer for implementing custom Varphi backends.
|
|
7
|
+
|
|
8
|
+
**Core API:**
|
|
9
|
+
- `VarphiCompiler`: The abstract base class you must subclass. Override `handle_transition` to process logic.
|
|
10
|
+
- `VarphiTransition`: A validated, canonicalized representation of a single transition line.
|
|
11
|
+
|
|
12
|
+
**Constants:**
|
|
13
|
+
- `BLANK`, `LEFT`, `RIGHT`, `STAY`: Primitives for tape operations.
|
|
14
|
+
|
|
15
|
+
**Exceptions:**
|
|
16
|
+
- `VarphiSyntaxError`: Base class for rich error reporting with source code context.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .compiler import VarphiCompiler, VarphiTransition, BLANK, LEFT, RIGHT, STAY
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
VarphiSyntaxError,
|
|
22
|
+
VarphiTransitionInconsistentTapeCountError,
|
|
23
|
+
VarphiGlobalTapeCountError,
|
|
24
|
+
VarphiUndefinedVariableError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"VarphiCompiler",
|
|
29
|
+
"VarphiTransition",
|
|
30
|
+
"BLANK",
|
|
31
|
+
"LEFT",
|
|
32
|
+
"RIGHT",
|
|
33
|
+
"STAY",
|
|
34
|
+
"VarphiSyntaxError",
|
|
35
|
+
"VarphiTransitionInconsistentTapeCountError",
|
|
36
|
+
"VarphiGlobalTapeCountError",
|
|
37
|
+
"VarphiUndefinedVariableError",
|
|
38
|
+
]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from antlr4 import InputStream, CommonTokenStream, ParseTreeWalker
|
|
5
|
+
|
|
6
|
+
from .parser import VarphiLexer
|
|
7
|
+
from .parser import VarphiParser
|
|
8
|
+
from .parser import VarphiListener
|
|
9
|
+
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
VarphiErrorListener,
|
|
12
|
+
VarphiTransitionInconsistentTapeCountError,
|
|
13
|
+
VarphiGlobalTapeCountError,
|
|
14
|
+
VarphiUndefinedVariableError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
BLANK = "_"
|
|
18
|
+
LEFT = "LEFT"
|
|
19
|
+
RIGHT = "RIGHT"
|
|
20
|
+
STAY = "STAY"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class VarphiTransition:
|
|
25
|
+
"""
|
|
26
|
+
Represents a transition (a single line) in a Varphi program.
|
|
27
|
+
Attributes:
|
|
28
|
+
- current_state (str): String containing the name of the state the machine must be on to trigger this transition.
|
|
29
|
+
- read_symbols (tuple[str, ...]): Tuple of strings containing the values the heads must read to trigger this transition. Strings starting with $ are variables and are guaranteed to be ordered ($1, $2, ...). Otherwise, members may be BLANK or alphanumericals.
|
|
30
|
+
- next_state (str): The name of the next state to transition to when this transition is triggered.
|
|
31
|
+
- write_symbols (tuple[str, ...]): Tuple of strings containing the values the heads will write when this transition is triggered. Follows the same format as read_symbols, and variable names appearing here are guaranteed to appear in read_symbols.
|
|
32
|
+
- shift_directions (tuple[str, ...]): A tuple of strings containing the directions the heads will move in when this transition is triggered. Each element is guaranteed to be one of LEFT, RIGHT, or STAY
|
|
33
|
+
- line_number (int): The line number in the source code this transition corresponds to.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
current_state: str
|
|
37
|
+
read_symbols: tuple[str, ...]
|
|
38
|
+
next_state: str
|
|
39
|
+
write_symbols: tuple[str, ...]
|
|
40
|
+
shift_directions: tuple[str, ...]
|
|
41
|
+
line_number: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class VarphiCompiler(VarphiListener, ABC):
|
|
45
|
+
"""
|
|
46
|
+
An abstract Varphi compiler.
|
|
47
|
+
|
|
48
|
+
Concrete Varphi compiler implementations must subclass this class and implement the following methods:
|
|
49
|
+
- handle_transition(self, transition: VarphiTransition) -> None: Automatically called at compile-time on each transition in the Varphi program
|
|
50
|
+
- generate_compiled_program(self) -> str: Returns the compiled Varphi program after all transitions have been handled via handle_transition()
|
|
51
|
+
If __init__() is overridden to add additional attributes (e.g., the compiled program so far), then
|
|
52
|
+
- super().__init__() must be called
|
|
53
|
+
- compile(self, program: str) -> str must be overridden to reset the state, followed by a call to super().__init__()
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
_expected_tape_count: Optional[int]
|
|
57
|
+
|
|
58
|
+
def __init__(self):
|
|
59
|
+
"""Initialize this compiler."""
|
|
60
|
+
self._expected_tape_count = None
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def handle_transition(self, transition: VarphiTransition) -> None:
|
|
64
|
+
"""Handle a single transition in a Varphi program."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def generate_compiled_program(self) -> str:
|
|
69
|
+
"""Generate the compiled program after all transitions have been handled."""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def compile(self, program: str) -> str:
|
|
73
|
+
"""Compile a Varphi program."""
|
|
74
|
+
# Reset state (subclasses must do so for their own state too)
|
|
75
|
+
self._expected_tape_count = None
|
|
76
|
+
|
|
77
|
+
input_stream = InputStream(program)
|
|
78
|
+
error_listener = VarphiErrorListener()
|
|
79
|
+
|
|
80
|
+
lexer = VarphiLexer(input_stream)
|
|
81
|
+
lexer.removeErrorListeners()
|
|
82
|
+
lexer.addErrorListener(error_listener)
|
|
83
|
+
|
|
84
|
+
token_stream = CommonTokenStream(lexer)
|
|
85
|
+
parser = VarphiParser(token_stream)
|
|
86
|
+
parser.removeErrorListeners()
|
|
87
|
+
parser.addErrorListener(error_listener)
|
|
88
|
+
|
|
89
|
+
tree = parser.program()
|
|
90
|
+
walker = ParseTreeWalker()
|
|
91
|
+
walker.walk(self, tree)
|
|
92
|
+
|
|
93
|
+
return self.generate_compiled_program()
|
|
94
|
+
|
|
95
|
+
def enterTransition(self, ctx: VarphiParser.TransitionContext) -> None:
|
|
96
|
+
"""Extract information from a raw transition context, and delegate to handle_transition()."""
|
|
97
|
+
current_state = ctx.current_state.getText()
|
|
98
|
+
next_state = ctx.next_state.getText()
|
|
99
|
+
|
|
100
|
+
def extract_symbol(symbol_ctx) -> str:
|
|
101
|
+
"""Given a symbol context, extract the corresponding symbol string."""
|
|
102
|
+
if symbol_ctx.VARIABLE():
|
|
103
|
+
return symbol_ctx.VARIABLE().getText()
|
|
104
|
+
if symbol_ctx.BLANK_KW():
|
|
105
|
+
return BLANK
|
|
106
|
+
if symbol_ctx.ALPHANUM():
|
|
107
|
+
return symbol_ctx.ALPHANUM().getText()
|
|
108
|
+
raise ValueError(f"Unknown symbol type: {symbol_ctx.getText()}")
|
|
109
|
+
|
|
110
|
+
def extract_direction(symbol_ctx) -> str:
|
|
111
|
+
"""Given a direction context, extract the corresponding direction string."""
|
|
112
|
+
if symbol_ctx.LEFT_KW():
|
|
113
|
+
return LEFT
|
|
114
|
+
if symbol_ctx.RIGHT_KW():
|
|
115
|
+
return RIGHT
|
|
116
|
+
if symbol_ctx.STAY_KW():
|
|
117
|
+
return STAY
|
|
118
|
+
raise ValueError(f"Unknown direction: {symbol_ctx.getText()}")
|
|
119
|
+
|
|
120
|
+
read_ctx = ctx.read_symbols()
|
|
121
|
+
reads = tuple(extract_symbol(s) for s in read_ctx.symbol()) if read_ctx else ()
|
|
122
|
+
|
|
123
|
+
write_ctx = ctx.write_symbols()
|
|
124
|
+
writes = (
|
|
125
|
+
tuple(extract_symbol(s) for s in write_ctx.symbol()) if write_ctx else ()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
shift_ctx = ctx.shift_directions()
|
|
129
|
+
shifts = (
|
|
130
|
+
tuple(extract_direction(d) for d in shift_ctx.direction())
|
|
131
|
+
if shift_ctx
|
|
132
|
+
else ()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Check if the tuple-lengths of the transition are all the same
|
|
136
|
+
if len(writes) != len(reads) or len(shifts) != len(reads):
|
|
137
|
+
raise VarphiTransitionInconsistentTapeCountError(
|
|
138
|
+
ctx, len(reads), len(writes), len(shifts)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Use the tuple lengths of the first transition as ground truth and make sure all other transitions are consistent
|
|
142
|
+
current_tape_count = len(reads)
|
|
143
|
+
if self._expected_tape_count is None:
|
|
144
|
+
self._expected_tape_count = current_tape_count
|
|
145
|
+
elif current_tape_count != self._expected_tape_count:
|
|
146
|
+
raise VarphiGlobalTapeCountError(
|
|
147
|
+
ctx, self._expected_tape_count, current_tape_count
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Map user variables to $1, $2, $3... based on appearance in the read tuple
|
|
151
|
+
# NOTE: This is just an optimization so that equivalent patterns ($x, $y) and ($y, $x) are easy call "equivalent"
|
|
152
|
+
variable_map = {}
|
|
153
|
+
next_var_id = 1
|
|
154
|
+
canonical_reads = []
|
|
155
|
+
for sym in reads:
|
|
156
|
+
if sym.startswith("$"):
|
|
157
|
+
# Check if we have already assigned an ID to this variable (e.g. read($x, $x))
|
|
158
|
+
if sym not in variable_map:
|
|
159
|
+
variable_map[sym] = f"${next_var_id}"
|
|
160
|
+
next_var_id += 1
|
|
161
|
+
canonical_reads.append(variable_map[sym])
|
|
162
|
+
else:
|
|
163
|
+
canonical_reads.append(sym)
|
|
164
|
+
|
|
165
|
+
# Map write variables to their canonical versions
|
|
166
|
+
canonical_writes = []
|
|
167
|
+
for i, sym in enumerate(writes):
|
|
168
|
+
if sym.startswith("$"):
|
|
169
|
+
if sym not in variable_map:
|
|
170
|
+
# This variable is not defined in the read tuple
|
|
171
|
+
specific_ctx = write_ctx.symbol(i)
|
|
172
|
+
raise VarphiUndefinedVariableError(specific_ctx, sym)
|
|
173
|
+
canonical_writes.append(variable_map[sym])
|
|
174
|
+
else:
|
|
175
|
+
canonical_writes.append(sym)
|
|
176
|
+
|
|
177
|
+
transition = VarphiTransition(
|
|
178
|
+
current_state=current_state,
|
|
179
|
+
read_symbols=tuple(canonical_reads),
|
|
180
|
+
next_state=next_state,
|
|
181
|
+
write_symbols=tuple(canonical_writes),
|
|
182
|
+
shift_directions=shifts,
|
|
183
|
+
line_number=ctx.start.line,
|
|
184
|
+
)
|
|
185
|
+
self.handle_transition(transition)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from antlr4.error.ErrorListener import ErrorListener
|
|
2
|
+
from antlr4 import Token
|
|
3
|
+
|
|
4
|
+
RESET = "\033[0m"
|
|
5
|
+
BOLD = "\033[1m"
|
|
6
|
+
RED = "\033[91m"
|
|
7
|
+
BLUE = "\033[34m"
|
|
8
|
+
CYAN = "\033[96m"
|
|
9
|
+
WHITE = "\033[97m"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VarphiSyntaxError(Exception):
|
|
13
|
+
"""
|
|
14
|
+
Base exception class for all Varphi compilation errors.
|
|
15
|
+
Captures context (line, column, symbol).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, recognizer, offendingSymbol, line, column, msg):
|
|
19
|
+
self.recognizer = recognizer
|
|
20
|
+
self.offendingSymbol = offendingSymbol
|
|
21
|
+
self.line = line
|
|
22
|
+
self.column = column
|
|
23
|
+
self.msg = msg
|
|
24
|
+
super().__init__(msg)
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
# Start the error message with a bold red "error:" label
|
|
28
|
+
result = [f"\n{BOLD}{RED}error:{RESET} {BOLD}{WHITE}{self.msg}{RESET}"]
|
|
29
|
+
result.append(f"{BLUE} -->{RESET} line {self.line}:{self.column + 1}")
|
|
30
|
+
|
|
31
|
+
# Attempt to retrieve the input stream to show the code snippet
|
|
32
|
+
stream = None
|
|
33
|
+
try:
|
|
34
|
+
# Try getting the stream from the recognizer (parser/lexer)
|
|
35
|
+
if self.recognizer:
|
|
36
|
+
temp_stream = self.recognizer.getInputStream()
|
|
37
|
+
if hasattr(temp_stream, "tokenSource"):
|
|
38
|
+
stream = temp_stream.tokenSource.inputStream
|
|
39
|
+
else:
|
|
40
|
+
stream = temp_stream
|
|
41
|
+
|
|
42
|
+
# If that fails, try getting it from the offending symbol (token)
|
|
43
|
+
elif self.offendingSymbol:
|
|
44
|
+
if hasattr(self.offendingSymbol, "getInputStream"):
|
|
45
|
+
stream = self.offendingSymbol.getInputStream()
|
|
46
|
+
elif hasattr(self.offendingSymbol, "tokenSource"):
|
|
47
|
+
stream = self.offendingSymbol.tokenSource.inputStream
|
|
48
|
+
|
|
49
|
+
# If we successfully found the stream, generate the code preview
|
|
50
|
+
if stream:
|
|
51
|
+
lines = str(stream).splitlines()
|
|
52
|
+
# Ensure the line number is valid within the source
|
|
53
|
+
if 0 <= self.line - 1 < len(lines):
|
|
54
|
+
code_line = lines[self.line - 1]
|
|
55
|
+
|
|
56
|
+
# Formatting constants for the "gutter"
|
|
57
|
+
gutter_width = 4
|
|
58
|
+
line_num_str = str(self.line)
|
|
59
|
+
|
|
60
|
+
# Print the empty pipe above the code line
|
|
61
|
+
result.append(f"{BLUE}{' ' * gutter_width} |{RESET}")
|
|
62
|
+
|
|
63
|
+
# Print the actual line of code with the line number
|
|
64
|
+
result.append(
|
|
65
|
+
f"{BLUE}{line_num_str:>{gutter_width}} |{RESET} {code_line}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Calculate the length of the error squiggle (^)
|
|
69
|
+
token_len = 1
|
|
70
|
+
if self.offendingSymbol and isinstance(self.offendingSymbol, Token):
|
|
71
|
+
start = self.offendingSymbol.start
|
|
72
|
+
stop = self.offendingSymbol.stop
|
|
73
|
+
if start is not None and stop is not None:
|
|
74
|
+
token_len = stop - start + 1
|
|
75
|
+
|
|
76
|
+
# Create the pointer line (e.g., " | ^~~~")
|
|
77
|
+
padding = " " * self.column
|
|
78
|
+
pointer = f"{BOLD}{RED}^{'~' * (token_len - 1)}{RESET}"
|
|
79
|
+
result.append(
|
|
80
|
+
f"{BLUE}{' ' * gutter_width} |{RESET} {padding}{pointer}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except Exception:
|
|
84
|
+
# Fallback: If anything fails during error generation, just return the basic message
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
return "\n".join(result)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class VarphiTransitionInconsistentTapeCountError(VarphiSyntaxError):
|
|
91
|
+
"""Raised when a single transition has mismatched read/write/shift tuple lengths."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, ctx, r_len, w_len, s_len):
|
|
94
|
+
msg = (
|
|
95
|
+
f"local tape count mismatch: read {r_len} symbols, "
|
|
96
|
+
f"but wrote {w_len} and shifted {s_len}"
|
|
97
|
+
)
|
|
98
|
+
super().__init__(None, ctx.start, ctx.start.line, ctx.start.column, msg)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class VarphiGlobalTapeCountError(VarphiSyntaxError):
|
|
102
|
+
"""Raised when a transition's tape count doesn't match the machine's global tape count."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, ctx, expected, actual):
|
|
105
|
+
msg = (
|
|
106
|
+
f"global tape count mismatch: previous transitions used {expected} tapes, "
|
|
107
|
+
f"but this one uses {actual}"
|
|
108
|
+
)
|
|
109
|
+
super().__init__(None, ctx.start, ctx.start.line, ctx.start.column, msg)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class VarphiUndefinedVariableError(VarphiSyntaxError):
|
|
113
|
+
"""Raised when a variable is used in a write tuple without being defined in the read tuple."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, ctx, variable_name):
|
|
116
|
+
msg = (
|
|
117
|
+
f"Undefined variable: '{variable_name}' is used in the write tuple "
|
|
118
|
+
f"but was not defined in the read tuple."
|
|
119
|
+
)
|
|
120
|
+
super().__init__(None, ctx.start, ctx.start.line, ctx.start.column, msg)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class VarphiErrorListener(ErrorListener):
|
|
124
|
+
"""Custom ANTLR ErrorListener that converts syntax errors into VarphiSyntaxErrors."""
|
|
125
|
+
|
|
126
|
+
def __init__(self):
|
|
127
|
+
super().__init__()
|
|
128
|
+
|
|
129
|
+
def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
|
|
130
|
+
raise VarphiSyntaxError(recognizer, offendingSymbol, line, column, msg)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
token literal names:
|
|
2
|
+
null
|
|
3
|
+
'('
|
|
4
|
+
')'
|
|
5
|
+
','
|
|
6
|
+
null
|
|
7
|
+
'LEFT'
|
|
8
|
+
'RIGHT'
|
|
9
|
+
'STAY'
|
|
10
|
+
'BLANK'
|
|
11
|
+
null
|
|
12
|
+
null
|
|
13
|
+
null
|
|
14
|
+
null
|
|
15
|
+
null
|
|
16
|
+
null
|
|
17
|
+
|
|
18
|
+
token symbolic names:
|
|
19
|
+
null
|
|
20
|
+
LPAREN
|
|
21
|
+
RPAREN
|
|
22
|
+
COMMA
|
|
23
|
+
VARIABLE
|
|
24
|
+
LEFT_KW
|
|
25
|
+
RIGHT_KW
|
|
26
|
+
STAY_KW
|
|
27
|
+
BLANK_KW
|
|
28
|
+
ALPHANUM
|
|
29
|
+
ID
|
|
30
|
+
COMMENT
|
|
31
|
+
MULTI_COMMENT
|
|
32
|
+
WS
|
|
33
|
+
NEWLINE
|
|
34
|
+
|
|
35
|
+
rule names:
|
|
36
|
+
program
|
|
37
|
+
transition
|
|
38
|
+
read_symbols
|
|
39
|
+
write_symbols
|
|
40
|
+
shift_directions
|
|
41
|
+
state_id
|
|
42
|
+
symbol
|
|
43
|
+
direction
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
atn:
|
|
47
|
+
[4, 1, 14, 88, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 1, 0, 5, 0, 18, 8, 0, 10, 0, 12, 0, 21, 9, 0, 1, 0, 1, 0, 4, 0, 25, 8, 0, 11, 0, 12, 0, 26, 1, 0, 5, 0, 30, 8, 0, 10, 0, 12, 0, 33, 9, 0, 1, 0, 5, 0, 36, 8, 0, 10, 0, 12, 0, 39, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 53, 8, 2, 10, 2, 12, 2, 56, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 5, 3, 64, 8, 3, 10, 3, 12, 3, 67, 9, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 5, 4, 75, 8, 4, 10, 4, 12, 4, 78, 9, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 0, 0, 8, 0, 2, 4, 6, 8, 10, 12, 14, 0, 3, 1, 0, 5, 10, 2, 0, 4, 4, 8, 9, 1, 0, 5, 7, 86, 0, 19, 1, 0, 0, 0, 2, 42, 1, 0, 0, 0, 4, 48, 1, 0, 0, 0, 6, 59, 1, 0, 0, 0, 8, 70, 1, 0, 0, 0, 10, 81, 1, 0, 0, 0, 12, 83, 1, 0, 0, 0, 14, 85, 1, 0, 0, 0, 16, 18, 5, 14, 0, 0, 17, 16, 1, 0, 0, 0, 18, 21, 1, 0, 0, 0, 19, 17, 1, 0, 0, 0, 19, 20, 1, 0, 0, 0, 20, 22, 1, 0, 0, 0, 21, 19, 1, 0, 0, 0, 22, 31, 3, 2, 1, 0, 23, 25, 5, 14, 0, 0, 24, 23, 1, 0, 0, 0, 25, 26, 1, 0, 0, 0, 26, 24, 1, 0, 0, 0, 26, 27, 1, 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 30, 3, 2, 1, 0, 29, 24, 1, 0, 0, 0, 30, 33, 1, 0, 0, 0, 31, 29, 1, 0, 0, 0, 31, 32, 1, 0, 0, 0, 32, 37, 1, 0, 0, 0, 33, 31, 1, 0, 0, 0, 34, 36, 5, 14, 0, 0, 35, 34, 1, 0, 0, 0, 36, 39, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 40, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 40, 41, 5, 0, 0, 1, 41, 1, 1, 0, 0, 0, 42, 43, 3, 10, 5, 0, 43, 44, 3, 4, 2, 0, 44, 45, 3, 10, 5, 0, 45, 46, 3, 6, 3, 0, 46, 47, 3, 8, 4, 0, 47, 3, 1, 0, 0, 0, 48, 49, 5, 1, 0, 0, 49, 54, 3, 12, 6, 0, 50, 51, 5, 3, 0, 0, 51, 53, 3, 12, 6, 0, 52, 50, 1, 0, 0, 0, 53, 56, 1, 0, 0, 0, 54, 52, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 57, 1, 0, 0, 0, 56, 54, 1, 0, 0, 0, 57, 58, 5, 2, 0, 0, 58, 5, 1, 0, 0, 0, 59, 60, 5, 1, 0, 0, 60, 65, 3, 12, 6, 0, 61, 62, 5, 3, 0, 0, 62, 64, 3, 12, 6, 0, 63, 61, 1, 0, 0, 0, 64, 67, 1, 0, 0, 0, 65, 63, 1, 0, 0, 0, 65, 66, 1, 0, 0, 0, 66, 68, 1, 0, 0, 0, 67, 65, 1, 0, 0, 0, 68, 69, 5, 2, 0, 0, 69, 7, 1, 0, 0, 0, 70, 71, 5, 1, 0, 0, 71, 76, 3, 14, 7, 0, 72, 73, 5, 3, 0, 0, 73, 75, 3, 14, 7, 0, 74, 72, 1, 0, 0, 0, 75, 78, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 76, 77, 1, 0, 0, 0, 77, 79, 1, 0, 0, 0, 78, 76, 1, 0, 0, 0, 79, 80, 5, 2, 0, 0, 80, 9, 1, 0, 0, 0, 81, 82, 7, 0, 0, 0, 82, 11, 1, 0, 0, 0, 83, 84, 7, 1, 0, 0, 84, 13, 1, 0, 0, 0, 85, 86, 7, 2, 0, 0, 86, 15, 1, 0, 0, 0, 7, 19, 26, 31, 37, 54, 65, 76]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LPAREN=1
|
|
2
|
+
RPAREN=2
|
|
3
|
+
COMMA=3
|
|
4
|
+
VARIABLE=4
|
|
5
|
+
LEFT_KW=5
|
|
6
|
+
RIGHT_KW=6
|
|
7
|
+
STAY_KW=7
|
|
8
|
+
BLANK_KW=8
|
|
9
|
+
ALPHANUM=9
|
|
10
|
+
ID=10
|
|
11
|
+
COMMENT=11
|
|
12
|
+
MULTI_COMMENT=12
|
|
13
|
+
WS=13
|
|
14
|
+
NEWLINE=14
|
|
15
|
+
'('=1
|
|
16
|
+
')'=2
|
|
17
|
+
','=3
|
|
18
|
+
'LEFT'=5
|
|
19
|
+
'RIGHT'=6
|
|
20
|
+
'STAY'=7
|
|
21
|
+
'BLANK'=8
|