opencrbasic 1.0.0__py3-none-any.whl
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.
- opencrbasic/__init__.py +3 -0
- opencrbasic/__main__.py +5 -0
- opencrbasic/analysis.py +184 -0
- opencrbasic/bindings.py +1034 -0
- opencrbasic/catalog.py +173 -0
- opencrbasic/checkconfig.py +144 -0
- opencrbasic/checker/__init__.py +977 -0
- opencrbasic/checker/cr3xx.py +3118 -0
- opencrbasic/checker/ess.py +81 -0
- opencrbasic/checker/expressions.py +3581 -0
- opencrbasic/checker/instr_data.py +1802 -0
- opencrbasic/checker/instr_measure.py +1547 -0
- opencrbasic/checker/instr_rules.py +3540 -0
- opencrbasic/checker/instructions.py +17 -0
- opencrbasic/checker/intervals.py +678 -0
- opencrbasic/checker/longconv.py +133 -0
- opencrbasic/checker/scan_time.py +580 -0
- opencrbasic/checker/storage.py +456 -0
- opencrbasic/checker/structs.py +240 -0
- opencrbasic/checker/structure.py +6213 -0
- opencrbasic/checker/symbols.py +609 -0
- opencrbasic/checker/terminals.py +249 -0
- opencrbasic/checker/unused.py +186 -0
- opencrbasic/checker/var_types.py +564 -0
- opencrbasic/checker/warnings.py +661 -0
- opencrbasic/checks.py +491 -0
- opencrbasic/cli.py +391 -0
- opencrbasic/commands.py +128 -0
- opencrbasic/compiler.py +320 -0
- opencrbasic/consteval.py +327 -0
- opencrbasic/data/background_calibration.json +4 -0
- opencrbasic/data/before_beginprog.json +99 -0
- opencrbasic/data/cr1000x/background_calibration.json +4 -0
- opencrbasic/data/cr1000x/before_beginprog.json +138 -0
- opencrbasic/data/cr1000x/ess_variables.json +1087 -0
- opencrbasic/data/cr1000x/fnotch.json +29 -0
- opencrbasic/data/cr1000x/functions.json +1337 -0
- opencrbasic/data/cr1000x/instructions.json +4836 -0
- opencrbasic/data/cr1000x/measure.json +121 -0
- opencrbasic/data/cr1000x/mode.json +111 -0
- opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
- opencrbasic/data/cr1000x/settings_fields.json +321 -0
- opencrbasic/data/cr1000x/status_members.json +318 -0
- opencrbasic/data/cr1000x/symbols.json +872 -0
- opencrbasic/data/cr1000x/terminals.json +225 -0
- opencrbasic/data/cr300/before_beginprog.json +42 -0
- opencrbasic/data/cr300/ess_variables.json +1087 -0
- opencrbasic/data/cr300/fnotch.json +22 -0
- opencrbasic/data/cr300/functions.json +1261 -0
- opencrbasic/data/cr300/instructions.json +2837 -0
- opencrbasic/data/cr300/measure.json +33 -0
- opencrbasic/data/cr300/mode.json +121 -0
- opencrbasic/data/cr300/settings_fields.json +259 -0
- opencrbasic/data/cr300/status_members.json +303 -0
- opencrbasic/data/cr300/symbols.json +584 -0
- opencrbasic/data/cr300/terminals.json +193 -0
- opencrbasic/data/cr350/before_beginprog.json +44 -0
- opencrbasic/data/cr350/ess_variables.json +1087 -0
- opencrbasic/data/cr350/fnotch.json +22 -0
- opencrbasic/data/cr350/functions.json +1262 -0
- opencrbasic/data/cr350/instructions.json +2848 -0
- opencrbasic/data/cr350/measure.json +35 -0
- opencrbasic/data/cr350/mode.json +121 -0
- opencrbasic/data/cr350/settings_fields.json +292 -0
- opencrbasic/data/cr350/status_members.json +303 -0
- opencrbasic/data/cr350/symbols.json +572 -0
- opencrbasic/data/cr350/terminals.json +190 -0
- opencrbasic/data/ess_variables.json +1087 -0
- opencrbasic/data/fnotch.json +34 -0
- opencrbasic/data/functions.json +1337 -0
- opencrbasic/data/instructions.json +4965 -0
- opencrbasic/data/measure.json +123 -0
- opencrbasic/data/mode.json +121 -0
- opencrbasic/data/notices_cr6.json +890 -0
- opencrbasic/data/pipeline_tasks.json +84 -0
- opencrbasic/data/settings_fields.json +322 -0
- opencrbasic/data/status_members.json +319 -0
- opencrbasic/data/status_table.json +684 -0
- opencrbasic/data/symbols.json +902 -0
- opencrbasic/data/terminals.json +195 -0
- opencrbasic/data/type_codes.json +249 -0
- opencrbasic/diagnostics.py +196 -0
- opencrbasic/editing.py +827 -0
- opencrbasic/formatting.py +310 -0
- opencrbasic/helpdocs.py +677 -0
- opencrbasic/hover.py +667 -0
- opencrbasic/inlay.py +169 -0
- opencrbasic/lexer.py +183 -0
- opencrbasic/lsp/__init__.py +5 -0
- opencrbasic/lsp/__main__.py +7 -0
- opencrbasic/lsp/config.py +202 -0
- opencrbasic/lsp/features.py +602 -0
- opencrbasic/lsp/server.py +741 -0
- opencrbasic/lsp/session.py +1189 -0
- opencrbasic/lsp/worker.py +128 -0
- opencrbasic/mode.py +143 -0
- opencrbasic/navigation.py +401 -0
- opencrbasic/parser.py +3264 -0
- opencrbasic/positions.py +274 -0
- opencrbasic/preprocess.py +269 -0
- opencrbasic/profiles.py +266 -0
- opencrbasic/provenance.py +17 -0
- opencrbasic/quickfix.py +331 -0
- opencrbasic/rename.py +662 -0
- opencrbasic/signature.py +33 -0
- opencrbasic/source.py +600 -0
- opencrbasic/sources.py +125 -0
- opencrbasic/syntax.py +337 -0
- opencrbasic/tables.py +2173 -0
- opencrbasic/tables_hook.py +14 -0
- opencrbasic/tdf.py +142 -0
- opencrbasic/timing.py +565 -0
- opencrbasic-1.0.0.dist-info/METADATA +223 -0
- opencrbasic-1.0.0.dist-info/RECORD +117 -0
- opencrbasic-1.0.0.dist-info/WHEEL +4 -0
- opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
- opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
opencrbasic/sources.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Where program text comes from: the disk, or the disk plus unsaved editor buffers.
|
|
2
|
+
|
|
3
|
+
The loader (`source.py`) asks a `SourceProvider` for file bytes and directory listings instead of
|
|
4
|
+
touching the filesystem itself, so the language-server facade (`analysis.py`) can analyse a
|
|
5
|
+
program whose files have unsaved edits through the same compile driver the CLI uses. Keys are
|
|
6
|
+
absolute, non-lowercased, non-symlink-resolved paths (`Path.absolute()`). `DiskSource` is exactly
|
|
7
|
+
the loader's historical behaviour. See `docs/lsp-api.md`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import stat
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SourceProvider(Protocol):
|
|
18
|
+
def read(self, path: Path) -> bytes | None:
|
|
19
|
+
"""The file's bytes; None when it is missing or unreadable."""
|
|
20
|
+
|
|
21
|
+
def list_dir(self, directory: Path) -> list[str] | None:
|
|
22
|
+
"""Names of the files (not subdirectories) in `directory`; None when it is not a directory."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DiskSource:
|
|
26
|
+
def read(self, path: Path) -> bytes | None:
|
|
27
|
+
try:
|
|
28
|
+
return Path(path).read_bytes()
|
|
29
|
+
except OSError:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def list_dir(self, directory: Path) -> list[str] | None:
|
|
33
|
+
directory = Path(directory)
|
|
34
|
+
if not directory.is_dir():
|
|
35
|
+
return None
|
|
36
|
+
return [entry.name for entry in directory.iterdir() if entry.is_file()]
|
|
37
|
+
|
|
38
|
+
def list_subdirs(self, directory: Path) -> list[str] | None:
|
|
39
|
+
directory = Path(directory)
|
|
40
|
+
if not directory.is_dir():
|
|
41
|
+
return None
|
|
42
|
+
return [entry.name for entry in directory.iterdir() if entry.is_dir()]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OverlaySource:
|
|
46
|
+
"""`base` with `overlays` (path -> bytes) on top. An overlay file also appears in its
|
|
47
|
+
directory's listing, even when `base` has no such file or no such directory."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, base: SourceProvider, overlays: Mapping[Path, bytes] | None = None):
|
|
50
|
+
self.base = base
|
|
51
|
+
self.overlays: dict[Path, bytes] = {Path(p).absolute(): bytes(b) for p, b in (overlays or {}).items()}
|
|
52
|
+
|
|
53
|
+
def read(self, path: Path) -> bytes | None:
|
|
54
|
+
data = self.overlays.get(Path(path).absolute())
|
|
55
|
+
return data if data is not None else self.base.read(path)
|
|
56
|
+
|
|
57
|
+
def list_dir(self, directory: Path) -> list[str] | None:
|
|
58
|
+
directory = Path(directory).absolute()
|
|
59
|
+
names = self.base.list_dir(directory)
|
|
60
|
+
extra = sorted(p.name for p in self.overlays if p.parent == directory)
|
|
61
|
+
if names is None:
|
|
62
|
+
return extra or None
|
|
63
|
+
have = set(names)
|
|
64
|
+
return names + [n for n in extra if n not in have]
|
|
65
|
+
|
|
66
|
+
def list_subdirs(self, directory: Path) -> list[str] | None:
|
|
67
|
+
subdirs = getattr(self.base, "list_subdirs", None)
|
|
68
|
+
return subdirs(directory) if subdirs else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LimitedSource:
|
|
72
|
+
"""`inner` without the files larger than `limit` bytes: such a file reads as missing (None)
|
|
73
|
+
and is recorded in `oversized` (path -> size). The size of an overlay is its length; a disk
|
|
74
|
+
file is checked with `stat` before it is read, so an oversized file is never read."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, inner: OverlaySource, limit: int):
|
|
77
|
+
self.inner = inner
|
|
78
|
+
self.limit = limit
|
|
79
|
+
self.oversized: dict[Path, int] = {}
|
|
80
|
+
|
|
81
|
+
def read(self, path: Path) -> bytes | None:
|
|
82
|
+
data = self.inner.overlays.get(Path(path).absolute())
|
|
83
|
+
if data is None:
|
|
84
|
+
try:
|
|
85
|
+
info = Path(path).stat()
|
|
86
|
+
except OSError:
|
|
87
|
+
info = None
|
|
88
|
+
if info is not None and stat.S_ISREG(info.st_mode) and info.st_size > self.limit:
|
|
89
|
+
self.oversized[Path(path)] = info.st_size
|
|
90
|
+
return None
|
|
91
|
+
data = self.inner.read(path)
|
|
92
|
+
if data is not None and len(data) > self.limit: # an overlay, or a file that grew
|
|
93
|
+
self.oversized[Path(path)] = len(data)
|
|
94
|
+
return None
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
def list_dir(self, directory: Path) -> list[str] | None:
|
|
98
|
+
return self.inner.list_dir(directory)
|
|
99
|
+
|
|
100
|
+
def list_subdirs(self, directory: Path) -> list[str] | None:
|
|
101
|
+
return self.inner.list_subdirs(directory)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RecordingSource:
|
|
105
|
+
"""Wraps `inner` and records what was asked: `reads` maps path -> sha256 hex of the bytes (None
|
|
106
|
+
for a miss), `listings` maps directory -> tuple of sorted names (None for a non-directory)."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, inner: SourceProvider):
|
|
109
|
+
self.inner = inner
|
|
110
|
+
self.reads: dict[Path, str | None] = {}
|
|
111
|
+
self.listings: dict[Path, tuple[str, ...] | None] = {}
|
|
112
|
+
|
|
113
|
+
def read(self, path: Path) -> bytes | None:
|
|
114
|
+
data = self.inner.read(path)
|
|
115
|
+
self.reads[Path(path)] = None if data is None else hashlib.sha256(data).hexdigest()
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
def list_dir(self, directory: Path) -> list[str] | None:
|
|
119
|
+
names = self.inner.list_dir(directory)
|
|
120
|
+
self.listings[Path(directory)] = None if names is None else tuple(sorted(names))
|
|
121
|
+
return names
|
|
122
|
+
|
|
123
|
+
def list_subdirs(self, directory: Path) -> list[str] | None:
|
|
124
|
+
subdirs = getattr(self.inner, "list_subdirs", None)
|
|
125
|
+
return subdirs(directory) if subdirs else None
|
opencrbasic/syntax.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Syntax tree for CRBasic.
|
|
2
|
+
|
|
3
|
+
A program is a flat list of statements, one per logical source line (or per `:`-separated
|
|
4
|
+
part). Block structure (If/EndIf, Scan/NextScan, DataTable/EndTable, ...) is kept flat, as the
|
|
5
|
+
original compiler does, and is checked by `opencrbasic.structure`. Every node carries the
|
|
6
|
+
1-based source line it came from and, for diagnostics, the file it came from.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING, Literal
|
|
12
|
+
|
|
13
|
+
from .provenance import LoadProvenance
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .lexer import Token
|
|
17
|
+
from .source import LogicalLine
|
|
18
|
+
|
|
19
|
+
type ConstValue = int | float | str # a folded constant (see `consteval.value_of`)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Node:
|
|
24
|
+
line: int
|
|
25
|
+
file: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------- expressions
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Expr(Node):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Number(Expr):
|
|
38
|
+
value: float | int = 0
|
|
39
|
+
text: str = "" # source spelling, e.g. "&H1F", "1.5E3"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class String(Expr):
|
|
44
|
+
value: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Name(Expr):
|
|
49
|
+
"""A bare identifier: variable, constant, predefined symbol, table name, ..."""
|
|
50
|
+
|
|
51
|
+
name: str = ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Index(Expr):
|
|
56
|
+
"""`name(i, j, k)`: array element, or a call when `name` is a builtin function / Function."""
|
|
57
|
+
|
|
58
|
+
name: str = ""
|
|
59
|
+
args: list[Expr | None] = field(default_factory=list) # None marks an empty argument slot
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Member(Expr):
|
|
64
|
+
"""`Table.Field`, `Status.Battery(1)`, `Struct.member`."""
|
|
65
|
+
|
|
66
|
+
base: Expr | None = None
|
|
67
|
+
member: str = ""
|
|
68
|
+
args: list[Expr | None] | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Unary(Expr):
|
|
73
|
+
op: str = "" # "-", "+", "NOT"
|
|
74
|
+
operand: Expr | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ArrayLiteral(Expr):
|
|
79
|
+
"""`{a, b, c}`: a `Const` array value, or an array initialiser in a `Public`/`Dim`."""
|
|
80
|
+
|
|
81
|
+
items: list[Expr | None] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Juxtaposed(ArrayLiteral):
|
|
86
|
+
"""Operands side by side in a declaration dimension (`Dim G(1 8)`), which the compiler reports
|
|
87
|
+
as `Incomplete expression`; `items` are the operands, so name walkers see all of them."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class Binary(Expr):
|
|
92
|
+
op: str = "" # upper-case operator text: + - * / \ ^ & MOD AND OR XOR IMP EQV = <> < > <= >= << >>
|
|
93
|
+
left: Expr | None = None
|
|
94
|
+
right: Expr | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------- statements
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _StatementMetadata:
|
|
101
|
+
"""Parser-attached details; annotations preserve the absence of unset attributes."""
|
|
102
|
+
|
|
103
|
+
origin: tuple[LogicalLine, int]
|
|
104
|
+
dropped_tail: list[Token]
|
|
105
|
+
grouped: bool
|
|
106
|
+
hoisted_last: bool
|
|
107
|
+
arg_spans: list[tuple[int, int] | None]
|
|
108
|
+
alias_junk: bool
|
|
109
|
+
alias_item_junk: list[bool]
|
|
110
|
+
extra_targets: list[Expr]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class Stmt(Node, _StatementMetadata):
|
|
115
|
+
text: str = "" # the logical line as written (for diagnostics that echo it)
|
|
116
|
+
at_eol: bool = True # `text` (this statement's own `:`-separated segment) reaches the
|
|
117
|
+
# true end of its physical source line -- no `'` comment and no further `:`-segment cut it
|
|
118
|
+
# short. Set by `parser.py`'s `_parse_line`; used only by `checker/structure.py`'s
|
|
119
|
+
# `_out_of_place` to decide whether CR300/CR350 print the fallback's closing `.` on its own
|
|
120
|
+
# output line (`docs/loggers.md` "Generic fallback echo on CR300/CR350").
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class _DeclarationMetadata:
|
|
124
|
+
size_missing: int | None
|
|
125
|
+
size_junk: tuple[int, str] | None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class VarDecl(_DeclarationMetadata):
|
|
130
|
+
name: str
|
|
131
|
+
dims: list[Expr] = field(default_factory=list) # up to 3
|
|
132
|
+
type: str | None = (
|
|
133
|
+
None # upper-case: FLOAT LONG BOOLEAN STRING DOUBLE UINT1 UINT2 UINT4 BOOL8 NSEC or a StructureType name
|
|
134
|
+
)
|
|
135
|
+
string_size: Expr | None = None # `As String * n`
|
|
136
|
+
init: list[Expr | None] | None = None # `= value` or `= {a, b, c}`
|
|
137
|
+
units: str | None = None
|
|
138
|
+
dim0_span: tuple[int, int] | None = None # (start, end) columns of the first
|
|
139
|
+
# dimension expression's own source text within the enclosing `Declaration.text` -- lets
|
|
140
|
+
# `checker/structure.py` echo it verbatim for `Variable %s dimension exceeds max of %d.`
|
|
141
|
+
# (oracle-confirmed to always name the *first* dimension, `docs/checker.md`), without the
|
|
142
|
+
# parser needing to reconstruct an expression's source spelling from its AST.
|
|
143
|
+
paren_dim: bool = False # a `(` inside the dimension list makes the variable Float whatever its `As` type
|
|
144
|
+
junk_dims: list[Expr] = field(default_factory=list)
|
|
145
|
+
empty_type_init: bool = False # `As Float() = ...`: `Invalid, or out of place expression: =.` (round 54) # the dimensions as written when one is a `Juxtaposed`; `dims` then holds 1 in its place
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class Declaration(Stmt):
|
|
150
|
+
"""Public / Dim / Static (keyword upper-case in `kind`)."""
|
|
151
|
+
|
|
152
|
+
kind: str = ""
|
|
153
|
+
vars: list[VarDecl] = field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class ConstDecl(Stmt):
|
|
158
|
+
name: str = ""
|
|
159
|
+
value: Expr | None = None
|
|
160
|
+
type: str | None = None
|
|
161
|
+
value_span: tuple[int, int] | None = None # (start, end) columns of `value`'s own source
|
|
162
|
+
# text within `Stmt.text` -- lets `checker/expressions.py` echo the initializer verbatim for
|
|
163
|
+
# `check_consts`'s undefined-symbol message, the same way `Instruction.arg_spans` already
|
|
164
|
+
# lets `_arg_text` echo an argument (`docs/checker.md` "Round 4"'s whole-expression echo).
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class Assignment(Stmt):
|
|
169
|
+
target: Expr | None = None # Name, Index or Member
|
|
170
|
+
value: Expr | None = None
|
|
171
|
+
compound: bool = False # `X += 1`, stored as `X = X + 1`
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class Instruction(Stmt):
|
|
176
|
+
"""Any statement of the form `Keyword [(] args [)]`, including block keywords.
|
|
177
|
+
|
|
178
|
+
`keyword` is the instruction-table spelling in upper case (`SCAN`, `NEXTSCAN`, `DATATABLE`,
|
|
179
|
+
`SAMPLE`, `ENDIF`, `CALLTABLE`, ...). `args` are the comma-separated arguments, None for an
|
|
180
|
+
empty slot. Statements that need extra structure use the dedicated classes below.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
keyword: str = ""
|
|
184
|
+
args: list[Expr | None] = field(default_factory=list)
|
|
185
|
+
arg_spans: list[tuple[int, int] | None] = field(default_factory=list) # (start, end) columns
|
|
186
|
+
# of each argument's own raw source text within this statement's `text` (`None` for an empty
|
|
187
|
+
# slot); lets a checker echo an argument's *verbatim* spelling (e.g. `1+2`, not a re-derived
|
|
188
|
+
# `3` or a spacing-normalised `1 + 2`) without reconstructing it from the parsed `Expr` tree.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class If(Stmt):
|
|
193
|
+
"""`If cond Then` (block form, `inline` None) or `If cond Then stmt [Else stmt]`."""
|
|
194
|
+
|
|
195
|
+
cond: Expr | None = None
|
|
196
|
+
inline: list[Stmt] | None = None
|
|
197
|
+
inline_else: list[Stmt] | None = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class ElseIf(Stmt):
|
|
202
|
+
cond: Expr | None = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class For(Stmt):
|
|
207
|
+
var: Expr | None = None
|
|
208
|
+
start: Expr | None = None
|
|
209
|
+
end: Expr | None = None
|
|
210
|
+
step: Expr | None = None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class DoLoop(Stmt):
|
|
215
|
+
"""`Do [While|Until cond]` or `Loop [While|Until cond]`; `keyword` is DO or LOOP."""
|
|
216
|
+
|
|
217
|
+
keyword: str = ""
|
|
218
|
+
cond_kind: str | None = None # WHILE / UNTIL
|
|
219
|
+
cond: Expr | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
type CaseItem = tuple[Literal["value"], Expr] | tuple[Literal["range"], Expr, Expr] | tuple[Literal["is"], str, Expr]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class Case(Stmt):
|
|
227
|
+
"""`Case a, b To c, Is > d, Else`; each item is ("value", e) / ("range", lo, hi) / ("is", op, e)."""
|
|
228
|
+
|
|
229
|
+
items: list[CaseItem] = field(default_factory=list)
|
|
230
|
+
is_else: bool = False
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class Param:
|
|
235
|
+
name: str
|
|
236
|
+
by_ref: bool | None = None # None when not written
|
|
237
|
+
type: str | None = None
|
|
238
|
+
string_size: Expr | None = None
|
|
239
|
+
dims: int = 0 # `x()` style array parameter
|
|
240
|
+
default: Expr | None = None # `Optional x = 1`
|
|
241
|
+
# Parsed expressions inside `x(...)`, in source order; alias declaration validation and
|
|
242
|
+
# editor bindings use these. They take no part in equality or repr.
|
|
243
|
+
dim_exprs: list[Expr] = field(default_factory=list, compare=False, repr=False)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class SubDef(Stmt):
|
|
248
|
+
"""`Sub name(params)` or `Function name(params) [As type]`; `keyword` SUB or FUNCTION."""
|
|
249
|
+
|
|
250
|
+
keyword: str = ""
|
|
251
|
+
name: str = ""
|
|
252
|
+
params: list[Param] = field(default_factory=list)
|
|
253
|
+
return_type: str | None = None
|
|
254
|
+
# pass-1 messages of a malformed header (`parser._parse_subdef`), in source order
|
|
255
|
+
header_errors: list[str] = field(default_factory=list, compare=False, repr=False)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass
|
|
259
|
+
class Call(Stmt):
|
|
260
|
+
"""`Call name(args)` or a bare `name(args)` / `name args` invocation of a Sub."""
|
|
261
|
+
|
|
262
|
+
name: str = ""
|
|
263
|
+
args: list[Expr | None] = field(default_factory=list)
|
|
264
|
+
explicit: bool = False # written with `Call`
|
|
265
|
+
tail_ignored: bool = False # `S()` followed by ignored trailing text
|
|
266
|
+
tail_expr: Expr | None = None # a `Function` call statement continued as one expression
|
|
267
|
+
juxtaposed: list[Expr] | None = None # operands of a juxtaposed tail: `Fn(1) 1`
|
|
268
|
+
juxt_opaque: bool = False # tail starts with `,` or `(`: `Fn(1),Zork`
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class Label(Stmt):
|
|
273
|
+
name: str = ""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class Unparsed(Stmt):
|
|
278
|
+
"""A line the parser could not handle; carries the reason for diagnostics."""
|
|
279
|
+
|
|
280
|
+
reason: str = ""
|
|
281
|
+
expr: Expr | None = None # the expression of a statement the parser took apart but cannot compile
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def ends_routine(stmt: Stmt) -> bool:
|
|
285
|
+
"""`EndSub`/`EndFunction`, or the two-word `End Sub`/`End Function` (parsed as `END` with the
|
|
286
|
+
word as its argument); the real compiler closes the routine with either spelling (oracle,
|
|
287
|
+
2026-10-12, all four loggers, `docs/checker.md` "Two-word routine closers")."""
|
|
288
|
+
if not isinstance(stmt, Instruction):
|
|
289
|
+
return False
|
|
290
|
+
kw = stmt.keyword.upper()
|
|
291
|
+
if kw in ("ENDSUB", "ENDFUNCTION"):
|
|
292
|
+
return True
|
|
293
|
+
return (
|
|
294
|
+
kw == "END"
|
|
295
|
+
and bool(stmt.args)
|
|
296
|
+
and isinstance(stmt.args[0], Name)
|
|
297
|
+
and stmt.args[0].name.upper() in ("SUB", "FUNCTION")
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def hash_statement(st: Stmt) -> Unparsed | None:
|
|
302
|
+
"""The `#...` statement that ends the compile (`Unknown instruction`): `st` itself or one in
|
|
303
|
+
an inline `If` body."""
|
|
304
|
+
if isinstance(st, Unparsed):
|
|
305
|
+
return st if st.text.lstrip().startswith("#") else None
|
|
306
|
+
for sub in (getattr(st, "inline", None) or []) + (getattr(st, "inline_else", None) or []):
|
|
307
|
+
if (h := hash_statement(sub)) is not None:
|
|
308
|
+
return h
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def cut_after_hash(st: Stmt) -> Stmt:
|
|
313
|
+
"""`st` without the inline statements that follow its `#...` statement (and without the `Else`
|
|
314
|
+
body when the `#...` is in the `Then` body): the compile stops there."""
|
|
315
|
+
then, other = getattr(st, "inline", None) or [], getattr(st, "inline_else", None) or []
|
|
316
|
+
for body, name in ((then, "inline"), (other, "inline_else")):
|
|
317
|
+
k = next((i for i, sub in enumerate(body) if hash_statement(sub)), None)
|
|
318
|
+
if k is not None and isinstance(st, If):
|
|
319
|
+
cut = body[:k] + [cut_after_hash(body[k])]
|
|
320
|
+
if name == "inline":
|
|
321
|
+
return dataclasses.replace(st, inline=cut, inline_else=[])
|
|
322
|
+
return dataclasses.replace(st, inline_else=cut)
|
|
323
|
+
return st
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@dataclass
|
|
327
|
+
class Program(LoadProvenance):
|
|
328
|
+
name: str
|
|
329
|
+
statements: list[Stmt] = field(default_factory=list)
|
|
330
|
+
last_line: int = 0 # `EndProg`'s line, else the main file's last physical line
|
|
331
|
+
hash_cut: int | None = None # parser diagnostics kept: those before the first `#...` statement ends
|
|
332
|
+
unclosed_if: bool = False # a `#If` still open at the end of the file (before any `EndProg`)
|
|
333
|
+
include_error_at: list[int] = field(default_factory=list) # statement count when each reported
|
|
334
|
+
# include error (extension, nested include) was read: where the CR300/CR350 cascade starts
|
|
335
|
+
consts: dict[str, ConstValue] = field(default_factory=dict) # name (upper) -> resolved value,
|
|
336
|
+
# for every `Const` the parser could constant-fold in an active branch; `checker.py` reuses
|
|
337
|
+
# this (via `consteval.Env`) instead of re-walking the program to rebuild it.
|