Skip to content

Commit b98baab

Browse files
authored
Merge pull request #1092 from boriel-basic/refact/lark_port
refact: port to lark
2 parents 96a95fe + 61450a3 commit b98baab

30 files changed

Lines changed: 17985 additions & 7892 deletions

‎pyproject.toml‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ shell = """
9292
branch = true
9393
omit = [
9494
"tests/*",
95-
"src/ply/*"
95+
"src/**/*_standalone.py",
9696
]
9797

9898
[tool.pytest.ini_options]
@@ -116,15 +116,18 @@ disable_error_code = [
116116
"var-annotated",
117117
]
118118
exclude = [
119-
'src/ply/.*\.py$',
120-
'scratch/*',
121-
'.venv/*',
122-
'venv/*',
119+
'scratch/.*',
120+
'.venv/.*',
121+
'venv/.*',
122+
'src/.*_standalone\.py$',
123123
]
124124

125125
[[tool.mypy.overrides]]
126126
module = [
127-
"src.ply.*",
127+
"src.zxbasm.asmparse_standalone",
128+
"src.zxbasm.asmparse_zxnext_standalone",
129+
"src.zxbc.zxbparser_standalone",
130+
"src.zxbpp.zxbpp_standalone",
128131
]
129132
follow_imports = "skip"
130133

@@ -134,7 +137,7 @@ target-version = "py314"
134137
exclude = [
135138
".venv/",
136139
"venv/",
137-
"src/ply" # PLY, external 3rd party tool
140+
"src/**/*standalone.py",
138141
]
139142

140143
[tool.ruff.lint]

‎src/api/errmsg.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def info(msg: str) -> None:
4343

4444
def error(lineno: int, msg: str, fname: str | None = None) -> None:
4545
"""Generic syntax error routine"""
46+
if getattr(global_, "syntax_error_occurred", False) and not getattr(global_, "reporting_syntax_error", False):
47+
return
48+
4649
if fname is None:
4750
fname = global_.FILENAME
4851

@@ -60,6 +63,8 @@ def error(lineno: int, msg: str, fname: str | None = None) -> None:
6063

6164
def warning(lineno: int, msg: str, fname: str | None = None) -> None:
6265
"""Generic warning error routine"""
66+
if getattr(global_, "syntax_error_occurred", False):
67+
return
6368
global_.has_warnings += 1
6469
if global_.has_warnings <= config.OPTIONS.expected_warnings:
6570
return

‎src/api/global_.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ class LoopInfo(NamedTuple):
5959
# ----------------------------------------------------------------------
6060
has_errors = 0 # Number of errors
6161
has_warnings = 0 # Number of warnings
62+
syntax_error_occurred = False # True if a syntax error occurred during parse
63+
reporting_syntax_error = False # True if we are actively reporting a syntax error
64+
tokens_yielded = 0
65+
tokens_rejected = 0
66+
shifted_at_last_error = 0
6267

6368
# ----------------------------------------------------------------------
6469
# Default var type when not specified (implicit) an can't be guessed

‎src/api/lex.py‎

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# --------------------------------------------------------------------
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
4+
# See the file CONTRIBUTORS.md for copyright details.
5+
# See https://www.gnu.org/licenses/agpl-3.0.html for details.
6+
# --------------------------------------------------------------------
7+
8+
import re
9+
import sys
10+
from collections.abc import Callable
11+
from typing import Any
12+
13+
14+
class LexError(Exception):
15+
pass
16+
17+
18+
class LexToken:
19+
def __init__(self) -> None:
20+
self.type: str | None = None
21+
self.value: Any = None
22+
self.lineno: int = 1
23+
self.lexpos: int = 0
24+
self.lexer: Lexer | None = None
25+
26+
def __repr__(self) -> str:
27+
return f"LexToken({self.type},{self.value!r},{self.lineno},{self.lexpos})"
28+
29+
30+
class Lexer:
31+
def __init__(self, obj: Any) -> None:
32+
self._object = obj
33+
self.lexdata: str = ""
34+
self.lexpos: int = 0
35+
self.lineno: int = 1
36+
self.statestack: list[str] = ["INITIAL"]
37+
self.next_token: LexToken | None = None
38+
39+
# Parse states
40+
states = get_attr(obj, "states", ())
41+
state_names = {"INITIAL"}
42+
for s in states:
43+
state_names.add(s[0])
44+
45+
# Parse tokens
46+
self.tokens = get_attr(obj, "tokens", ())
47+
48+
# Collect rules
49+
functions: list[tuple[str, list[str], str, str, Callable[[LexToken], Any]]] = []
50+
strings: list[tuple[str, list[str], str, str]] = []
51+
self.error_handlers: dict[str, Callable[[LexToken], Any]] = {}
52+
53+
# dir(obj) preserves definition order in modern Python,
54+
# but we want to be safe and sort functions by co_firstlineno
55+
for name in get_dir(obj):
56+
if name.startswith("t_"):
57+
val = get_attr(obj, name)
58+
if name.endswith("_error"):
59+
parts = name[2:-6].split("_")
60+
target_states = [p for p in parts if p in state_names]
61+
if not target_states:
62+
target_states = ["INITIAL"]
63+
for s in target_states:
64+
self.error_handlers[s] = val
65+
else:
66+
parts = name[2:].split("_")
67+
target_states = []
68+
i = 0
69+
# Consume state names from the left, leaving at least one part for the rule name
70+
while i < len(parts) - 1 and parts[i] in state_names:
71+
target_states.append(parts[i])
72+
i += 1
73+
74+
if not target_states:
75+
target_states = ["INITIAL"]
76+
rule_name = "_".join(parts)
77+
else:
78+
rule_name = "_".join(parts[i:])
79+
80+
if callable(val):
81+
pattern = val.__doc__
82+
if pattern:
83+
functions.append((name, target_states, rule_name, pattern, val))
84+
elif isinstance(val, str):
85+
strings.append((name, target_states, rule_name, val))
86+
87+
# Sort functions by co_firstlineno to match definition order in file
88+
def get_line_no(item: tuple[str, list[str], str, str, Callable[[LexToken], Any]]) -> int:
89+
func = item[4]
90+
try:
91+
if hasattr(func, "__code__"):
92+
return func.__code__.co_firstlineno
93+
if hasattr(func, "__func__") and hasattr(func.__func__, "__code__"):
94+
return func.__func__.__code__.co_firstlineno
95+
except Exception:
96+
pass
97+
return 0
98+
99+
functions.sort(key=get_line_no)
100+
101+
# Sort strings by pattern length descending
102+
strings.sort(key=lambda x: len(x[3]), reverse=True)
103+
104+
# Build rules per state
105+
state_rules: dict[str, list[tuple[str, re.Pattern[str], Callable[[LexToken], Any] | None]]] = {
106+
s: [] for s in state_names
107+
}
108+
109+
# Add functions first
110+
for name, target_states, rule_name, pattern, func in functions:
111+
try:
112+
rx = re.compile(pattern)
113+
for s in target_states:
114+
state_rules[s].append((rule_name, rx, func))
115+
except Exception as e:
116+
print(f"Error compiling pattern {pattern!r} for rule {name}: {e}", file=sys.stderr)
117+
raise e
118+
119+
# Add strings second
120+
for name, target_states, rule_name, pattern in strings:
121+
try:
122+
rx = re.compile(pattern)
123+
for s in target_states:
124+
state_rules[s].append((rule_name, rx, None))
125+
except Exception as e:
126+
print(f"Error compiling pattern {pattern!r} for rule {name}: {e}", file=sys.stderr)
127+
raise e
128+
129+
self.compiled_rules = state_rules
130+
131+
def input(self, data: str) -> None:
132+
self.lexdata = data
133+
self.lexpos = 0
134+
self.lineno = 1
135+
self.statestack = ["INITIAL"]
136+
self.next_token = None
137+
138+
def begin(self, state: str) -> None:
139+
if state not in self.compiled_rules:
140+
raise LexError(f"Undefined state: {state}")
141+
self.statestack[-1] = state
142+
143+
def push_state(self, state: str) -> None:
144+
if state not in self.compiled_rules:
145+
raise LexError(f"Undefined state: {state}")
146+
self.statestack.append(state)
147+
148+
def pop_state(self) -> None:
149+
if len(self.statestack) > 1:
150+
self.statestack.pop()
151+
else:
152+
self.statestack = ["INITIAL"]
153+
154+
def skip(self, n: int) -> None:
155+
self.lexpos += n
156+
157+
def clone(self) -> Lexer:
158+
c = Lexer(self._object)
159+
c.lexdata = self.lexdata
160+
c.lexpos = self.lexpos
161+
c.lineno = self.lineno
162+
c.statestack = list(self.statestack)
163+
c.next_token = self.next_token
164+
return c
165+
166+
def token(self) -> LexToken | None:
167+
if self.next_token is not None:
168+
t = self.next_token
169+
self.next_token = None
170+
return t
171+
172+
while self.lexpos < len(self.lexdata):
173+
state = self.statestack[-1]
174+
rules = self.compiled_rules.get(state, [])
175+
176+
matched = False
177+
for rule_name, rx, val in rules:
178+
m = rx.match(self.lexdata, self.lexpos)
179+
if m:
180+
matched = True
181+
matched_text = m.group(0)
182+
183+
t = LexToken()
184+
t.type = rule_name
185+
t.value = matched_text
186+
t.lineno = self.lineno
187+
t.lexpos = self.lexpos
188+
t.lexer = self
189+
190+
# Advance position BEFORE calling function
191+
self.lexpos += len(matched_text)
192+
193+
if val:
194+
res = val(t)
195+
if res is None:
196+
# Ignored token
197+
break
198+
return res
199+
# Convert to token type (which is rule_name)
200+
return t
201+
202+
if matched:
203+
continue
204+
205+
# No rule matched
206+
err_handler = self.error_handlers.get(state)
207+
if err_handler:
208+
t = LexToken()
209+
t.type = "error"
210+
t.value = self.lexdata[self.lexpos :]
211+
t.lineno = self.lineno
212+
t.lexpos = self.lexpos
213+
t.lexer = self
214+
215+
old_pos = self.lexpos
216+
res = err_handler(t)
217+
if self.lexpos == old_pos:
218+
self.lexpos += 1
219+
if res is not None:
220+
return res
221+
else:
222+
raise LexError(
223+
f"Lexical error: Illegal character {self.lexdata[self.lexpos]!r} at position {self.lexpos}"
224+
)
225+
226+
return None
227+
228+
229+
def get_attr(obj: Any, name: str, default: Any = None) -> Any:
230+
if isinstance(obj, dict):
231+
return obj.get(name, default)
232+
return getattr(obj, name, default)
233+
234+
235+
def get_dir(obj: Any) -> list[str]:
236+
if isinstance(obj, dict):
237+
return list(obj.keys())
238+
return dir(obj)
239+
240+
241+
def lex(object: Any = None) -> Lexer:
242+
if object is None:
243+
frame = sys._getframe(1)
244+
object = frame.f_globals
245+
return Lexer(object)

‎src/api/utils.py‎

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77

88
import errno
99
import os
10-
import shelve
1110
import signal
1211
from collections.abc import Callable, Iterable
1312
from contextlib import contextmanager
1413
from functools import wraps
1514
from typing import IO, Any, TypeVar
1615

17-
from src.api import constants, errmsg, global_
16+
from src.api import errmsg, global_
1817

1918
__all__ = (
2019
"chdir",
@@ -30,9 +29,6 @@
3029
like reading files or path management"""
3130

3231

33-
SHELVE_PATH = os.path.join(constants.ZXBASIC_ROOT, "parsetab", "tabs.dbm")
34-
SHELVE = shelve.open(SHELVE_PATH, protocol=5, flag="c")
35-
3632
T = TypeVar("T")
3733

3834

@@ -184,20 +180,6 @@ def eval_to_num(expr: str) -> int | float | None:
184180
return None
185181

186182

187-
def load_object(key: str) -> Any:
188-
return SHELVE[key] if key in SHELVE else None
189-
190-
191-
def save_object(key: str, obj: Any) -> Any:
192-
SHELVE[key] = obj
193-
SHELVE.sync()
194-
return obj
195-
196-
197-
def get_or_create(key: str, fn: Callable[[], Any]) -> Any:
198-
return load_object(key) or save_object(key, fn())
199-
200-
201183
def timeout(seconds: Callable[[], int] | int = 10, error_message=os.strerror(errno.ETIME)):
202184
def decorator(func):
203185
def _handle_timeout(signum, frame):

‎src/parsetab/__init__.py‎

Lines changed: 0 additions & 6 deletions
This file was deleted.

‎src/parsetab/tabs.dbm.bak‎

Lines changed: 0 additions & 4 deletions
This file was deleted.

‎src/parsetab/tabs.dbm.dat‎

-1.15 MB
Binary file not shown.

‎src/parsetab/tabs.dbm.dir‎

Lines changed: 0 additions & 4 deletions
This file was deleted.

‎src/ply/__init__.py‎

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)