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
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
"""Declared-name collection: `Sub`/`Function` names, used by `structure.py` to decide whether a
|
|
2
|
+
bare (no-`Call`-keyword) name is a legal statement. Rules and oracle evidence: `docs/checker.md`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
|
|
8
|
+
from .. import consteval
|
|
9
|
+
from .. import syntax as S
|
|
10
|
+
from ..diagnostics import Diagnostics
|
|
11
|
+
from ..source import _split_top_commas
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def declared_subs(program: S.Program) -> set[str]:
|
|
15
|
+
"""Upper-case names of every `Sub`/`Function` declared anywhere in the program.
|
|
16
|
+
|
|
17
|
+
A bare identifier (or `Name(args)` with no `Call` keyword) is only a legal CRBasic statement
|
|
18
|
+
when it names an already-declared `Sub`/`Function` (oracle-confirmed, `docs/frontend.md`
|
|
19
|
+
"Statements"; re-confirmed for the parenthesised-argument form in `docs/checker.md`). Building
|
|
20
|
+
this set from the whole program (not just statements seen so far) is deliberately lenient
|
|
21
|
+
about forward references: we would rather under-report ("verdict", not "exact") than reject a
|
|
22
|
+
program the real compiler accepts (AGENTS.md priority order).
|
|
23
|
+
"""
|
|
24
|
+
return {s.name.upper() for s in program.statements if isinstance(s, S.SubDef)}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Instructions whose first argument is an identifier this checker treats as declaring a name a
|
|
28
|
+
# bare `Name` may later reference: a `DataTable`'s own name (referenced by `CallTable`/`Read`/...),
|
|
29
|
+
# or a `StructureType`/`ObjectType`'s own name (referenced as a `Sample`/`Average`/... `DataType`
|
|
30
|
+
# argument for a structured measurement, oracle-confirmed regression this session found,
|
|
31
|
+
# `structuretype_1.CR6`/`structuretype_2.CR6`: `Sample(3,Piezometer,VWSensor)` where `VWSensor` is
|
|
32
|
+
# the `StructureType` declared just above, not a fixed `data/symbols.json` `DataType` constant).
|
|
33
|
+
_NAME_DECLARING_INSTRUCTIONS = ("DATATABLE", "STRUCTURETYPE", "OBJECTTYPE", "CONSTTABLE")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# System tables always addressable as `Table.Field` (`Status.SerialNumber`, `Public.X`,
|
|
37
|
+
# `Settings.PakBusAddress`), oracle-confirmed accepted on all four loggers (CR6/CR1000X/CR300/
|
|
38
|
+
# CR350) with no program-shape precondition -- unlike `DataTableInfo` below, these three exist in
|
|
39
|
+
# every program regardless of whether any `DataTable`/`ConstTable`/`SlowSequence` is declared
|
|
40
|
+
# (`docs/checker.md`). Not sourced from any `data/<logger>/symbols.json` (that table is the
|
|
41
|
+
# compiler's *named-constant* table, `docs/type-codes.md`; a table name resolves through a
|
|
42
|
+
# different mechanism entirely, oracle-confirmed by `Status`/`Public`/`Settings` never appearing
|
|
43
|
+
# in any of the four extracted `symbols.json` files despite compiling cleanly).
|
|
44
|
+
_ALWAYS_KNOWN_SYSTEM_TABLES = frozenset({"STATUS", "PUBLIC", "SETTINGS"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def system_table_names(program: S.Program) -> set[str]:
|
|
48
|
+
"""System table names a bare `Name`/`Member` base may legally reference, beyond the
|
|
49
|
+
program's own declared `DataTable`s (already covered by `declared_names`).
|
|
50
|
+
|
|
51
|
+
`DataTableInfo` only resolves once the program declares at least one user `DataTable`
|
|
52
|
+
(oracle-confirmed, all four loggers: `DataTableInfo.SkippedRecord(1)` alone in an otherwise
|
|
53
|
+
table-less program fails `Could not find DataTableInfo.`/`Could not find DataTable
|
|
54
|
+
DataTableInfo.`; the same reference compiles once any `DataTable(...)...EndTable` exists
|
|
55
|
+
anywhere in the program, `docs/checker.md`). `Status`/`Public`/`Settings` have no such
|
|
56
|
+
precondition. Checked against `program.statements` (post-`#If`-resolution, so an inactive
|
|
57
|
+
`DataTable` inside a false `#If` branch correctly does not count, matching what the real
|
|
58
|
+
compiler would also not see).
|
|
59
|
+
"""
|
|
60
|
+
names = set(_ALWAYS_KNOWN_SYSTEM_TABLES)
|
|
61
|
+
if any(isinstance(s, S.Instruction) and s.keyword.upper() == "DATATABLE" for s in program.statements):
|
|
62
|
+
names.add("DATATABLEINFO")
|
|
63
|
+
return names
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _walk_names(expr: S.Expr | None):
|
|
67
|
+
"""Yields every `Name`/`Index`/`Member`-base's own bare name, in source order, from anywhere
|
|
68
|
+
inside `expr` -- the same recursion shape `expressions._undefined_names` walks for a different
|
|
69
|
+
purpose (undefined-name detection), duplicated here rather than imported so `symbols.py` does
|
|
70
|
+
not depend on `expressions.py` (the reverse dependency already holds, `expressions.py` imports
|
|
71
|
+
this module)."""
|
|
72
|
+
if expr is None:
|
|
73
|
+
return
|
|
74
|
+
if isinstance(expr, S.Name):
|
|
75
|
+
yield expr.name
|
|
76
|
+
elif isinstance(expr, S.Unary):
|
|
77
|
+
yield from _walk_names(expr.operand)
|
|
78
|
+
elif isinstance(expr, S.Binary):
|
|
79
|
+
yield from _walk_names(expr.left)
|
|
80
|
+
yield from _walk_names(expr.right)
|
|
81
|
+
elif isinstance(expr, S.Index):
|
|
82
|
+
yield expr.name
|
|
83
|
+
for a in expr.args:
|
|
84
|
+
yield from _walk_names(a)
|
|
85
|
+
elif isinstance(expr, S.Member):
|
|
86
|
+
yield from _walk_names(expr.base)
|
|
87
|
+
elif isinstance(expr, S.ArrayLiteral):
|
|
88
|
+
for item in expr.items:
|
|
89
|
+
yield from _walk_names(item)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _stmt_exprs(stmt: S.Stmt):
|
|
93
|
+
"""Every expression a statement holds (declaration dimensions/initialisers, `Const` value,
|
|
94
|
+
assignment, condition, loop bounds, `Case` items, call/instruction arguments, parameter
|
|
95
|
+
defaults, and those of a one-line `If`'s bodies)."""
|
|
96
|
+
if isinstance(stmt, S.Declaration):
|
|
97
|
+
for v in stmt.vars:
|
|
98
|
+
yield from v.dims
|
|
99
|
+
yield from v.init or ()
|
|
100
|
+
yield v.string_size
|
|
101
|
+
elif isinstance(stmt, S.ConstDecl):
|
|
102
|
+
yield stmt.value
|
|
103
|
+
elif isinstance(stmt, S.Assignment):
|
|
104
|
+
yield stmt.target
|
|
105
|
+
yield stmt.value
|
|
106
|
+
elif isinstance(stmt, (S.Instruction, S.Call)):
|
|
107
|
+
yield from stmt.args
|
|
108
|
+
elif isinstance(stmt, S.If):
|
|
109
|
+
yield stmt.cond
|
|
110
|
+
for s2 in [*(stmt.inline or ()), *(stmt.inline_else or ())]:
|
|
111
|
+
yield from _stmt_exprs(s2)
|
|
112
|
+
elif isinstance(stmt, (S.ElseIf, S.DoLoop)):
|
|
113
|
+
yield stmt.cond
|
|
114
|
+
elif isinstance(stmt, S.For):
|
|
115
|
+
yield from (stmt.var, stmt.start, stmt.end, stmt.step)
|
|
116
|
+
elif isinstance(stmt, S.Case):
|
|
117
|
+
for item in stmt.items:
|
|
118
|
+
yield from (part for part in item if isinstance(part, S.Expr))
|
|
119
|
+
elif isinstance(stmt, S.SubDef):
|
|
120
|
+
for p in stmt.params:
|
|
121
|
+
yield p.default
|
|
122
|
+
yield p.string_size
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _name_nodes(expr: S.Expr | None):
|
|
126
|
+
"""The `Name`/`Index` nodes and `Member` bases inside `expr` that `expressions._undefined_names`
|
|
127
|
+
looks a bare name up for."""
|
|
128
|
+
if expr is None:
|
|
129
|
+
return
|
|
130
|
+
if isinstance(expr, (S.Name, S.Index)):
|
|
131
|
+
yield expr
|
|
132
|
+
if isinstance(expr, S.Index):
|
|
133
|
+
for a in expr.args:
|
|
134
|
+
yield from _name_nodes(a)
|
|
135
|
+
elif isinstance(expr, S.Unary):
|
|
136
|
+
yield from _name_nodes(expr.operand)
|
|
137
|
+
elif isinstance(expr, S.Binary):
|
|
138
|
+
yield from _name_nodes(expr.left)
|
|
139
|
+
yield from _name_nodes(expr.right)
|
|
140
|
+
elif isinstance(expr, S.Member):
|
|
141
|
+
yield from _name_nodes(expr.base)
|
|
142
|
+
for a in getattr(expr, "args", None) or ():
|
|
143
|
+
yield from _name_nodes(a)
|
|
144
|
+
elif isinstance(expr, S.ArrayLiteral):
|
|
145
|
+
for item in expr.items:
|
|
146
|
+
yield from _name_nodes(item)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def out_of_scope_nodes(program: S.Program, resolvable: frozenset | set = frozenset()) -> frozenset[int]:
|
|
150
|
+
"""`id`s of the `Name`/`Index` nodes that name a `Dim` or parameter local to some other `Sub`/
|
|
151
|
+
`Function` and nothing visible where they stand: the real compilers report them undefined (oracle, all
|
|
152
|
+
four loggers, 2026-10-12: `Dim Y` in a Sub, `Y = 2` in main code or in another Sub ->
|
|
153
|
+
`Invalid, or out of place expression`, CR300/CR350 `Y is not defined.`). A name that is also
|
|
154
|
+
declared outside a routine, or `Public`/`Const` inside one (both global), stays visible, and so
|
|
155
|
+
does one that resolves without any declaration (`resolvable`: a predefined symbol, function...,
|
|
156
|
+
so a refused `Dim Sec` in a Sub leaves every `Sec` valid)."""
|
|
157
|
+
locals_: dict[int, set[str]] = {}
|
|
158
|
+
owners: list[tuple[S.Stmt, S.SubDef | None]] = []
|
|
159
|
+
cur = None
|
|
160
|
+
for st in program.statements:
|
|
161
|
+
if isinstance(st, S.SubDef):
|
|
162
|
+
cur = st
|
|
163
|
+
locals_[id(cur)] = {p.name.upper() for p in st.params}
|
|
164
|
+
owners.append((st, cur))
|
|
165
|
+
if S.ends_routine(st):
|
|
166
|
+
cur = None
|
|
167
|
+
elif cur is not None and isinstance(st, S.Declaration) and st.kind == "DIM":
|
|
168
|
+
locals_[id(cur)].update(v.name.upper() for v in st.vars)
|
|
169
|
+
every = set().union(*locals_.values()) if locals_ else set()
|
|
170
|
+
if not every:
|
|
171
|
+
return frozenset()
|
|
172
|
+
outer = declared_names(
|
|
173
|
+
S.Program(
|
|
174
|
+
"",
|
|
175
|
+
[
|
|
176
|
+
replace(st, params=[]) if isinstance(st, S.SubDef) else st
|
|
177
|
+
for st, r in owners
|
|
178
|
+
if not (r is not None and isinstance(st, S.Declaration) and st.kind == "DIM")
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
hidden_all = every - outer - set(resolvable)
|
|
183
|
+
if not hidden_all:
|
|
184
|
+
return frozenset()
|
|
185
|
+
out: set[int] = set()
|
|
186
|
+
for st, r in owners:
|
|
187
|
+
hidden = hidden_all - (locals_[id(r)] if r is not None else set())
|
|
188
|
+
if r is st:
|
|
189
|
+
continue # the header itself
|
|
190
|
+
for e in _stmt_exprs(st):
|
|
191
|
+
for node in _name_nodes(e):
|
|
192
|
+
if node.name.upper() in hidden:
|
|
193
|
+
out.add(id(node))
|
|
194
|
+
return frozenset(out)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def ess_variable_names(program: S.Program) -> set[str]:
|
|
198
|
+
"""Names this checker treats as resolved once the program declares `ESSVariables` (`docs/
|
|
199
|
+
checker.md` "Round 5" -- a fuzz-found false reject): `ESSVariables Dim`/`ESSVariables Public`
|
|
200
|
+
implicitly declares a whole predefined family of NTCIP `ess...`-prefixed variables
|
|
201
|
+
(`essNtcipSiteDescription`, `essBatteryStatus`, ...), oracle-confirmed accepted even though none
|
|
202
|
+
of them is a `Public`/`Dim`/`Const` `declared_names` would ever see (`ESSVariables Dim` /
|
|
203
|
+
`essNtcipSiteDescription = "Main Street"` compiles on `CR6Comp.exe`).
|
|
204
|
+
|
|
205
|
+
This project has not extracted the actual, complete ESS-variable name list from the binary (no
|
|
206
|
+
table this project has decoded enumerates it, unlike a `DataTable`'s own fields), so rather than
|
|
207
|
+
guess at it, this returns every name *actually referenced anywhere in this specific program*
|
|
208
|
+
that starts with the documented `ess` prefix (case-insensitive, matching the CHM's own spelling
|
|
209
|
+
convention) when -- and only when -- the program declares `ESSVariables` somewhere. Deliberately
|
|
210
|
+
conservative both ways: a genuinely misspelled `ess`-prefixed name is silently accepted too
|
|
211
|
+
(under-reporting, the safe side of AGENTS.md's priority), and this costs nothing when the
|
|
212
|
+
program never uses `ESSVariables` at all (the overwhelmingly common case -- returns empty
|
|
213
|
+
immediately).
|
|
214
|
+
"""
|
|
215
|
+
if not any(isinstance(s, S.Instruction) and s.keyword.upper() == "ESSVARIABLES" for s in program.statements):
|
|
216
|
+
return set()
|
|
217
|
+
names: set[str] = set()
|
|
218
|
+
for stmt in program.statements:
|
|
219
|
+
exprs: list[S.Expr | None] = []
|
|
220
|
+
if isinstance(stmt, S.Assignment):
|
|
221
|
+
exprs = [stmt.target, stmt.value]
|
|
222
|
+
elif isinstance(stmt, S.Instruction):
|
|
223
|
+
exprs = list(stmt.args)
|
|
224
|
+
for expr in exprs:
|
|
225
|
+
for name in _walk_names(expr):
|
|
226
|
+
if name.upper().startswith("ESS"):
|
|
227
|
+
names.add(name.upper())
|
|
228
|
+
return names
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def declared_variable_types(program: S.Program) -> dict[str, str]:
|
|
232
|
+
"""Upper-case name -> declared type (`FLOAT` when none) of every Public/Dim/Static variable;
|
|
233
|
+
a name declared with two different types is left out."""
|
|
234
|
+
types: dict[str, str] = {}
|
|
235
|
+
clash: set[str] = set()
|
|
236
|
+
for st in program.statements:
|
|
237
|
+
if isinstance(st, S.Declaration) and st.kind in ("PUBLIC", "DIM", "STATIC"):
|
|
238
|
+
for v in st.vars:
|
|
239
|
+
name, kind = v.name.upper(), (v.type or "FLOAT").upper()
|
|
240
|
+
if v.paren_dim: # `Public A((1+2)) As Long` is a Float array (oracle, docs/checker.md)
|
|
241
|
+
kind = "FLOAT"
|
|
242
|
+
if types.setdefault(name, kind) != kind:
|
|
243
|
+
clash.add(name)
|
|
244
|
+
for name in clash:
|
|
245
|
+
del types[name]
|
|
246
|
+
return types
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def const_table_fields(program: S.Program) -> dict[str, set[str]]:
|
|
250
|
+
"""Closed ConstTable name -> Const names declared in its body."""
|
|
251
|
+
fields: dict[str, set[str]] = {}
|
|
252
|
+
current: str | None = None
|
|
253
|
+
for stmt in program.statements:
|
|
254
|
+
if isinstance(stmt, S.Instruction) and stmt.keyword.upper() == "CONSTTABLE":
|
|
255
|
+
current = stmt.args[0].name.upper() if stmt.args and isinstance(stmt.args[0], S.Name) else None
|
|
256
|
+
if current is not None:
|
|
257
|
+
fields.setdefault(current, set())
|
|
258
|
+
elif isinstance(stmt, S.Instruction) and stmt.keyword.upper() == "ENDCONSTTABLE":
|
|
259
|
+
current = None
|
|
260
|
+
elif current is not None and isinstance(stmt, S.ConstDecl):
|
|
261
|
+
fields[current].add(stmt.name.upper())
|
|
262
|
+
return fields
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def declared_variable_names(program: S.Program) -> set[str]:
|
|
266
|
+
"""Names that are genuine runtime storage: `Public`/`Dim`/`Static` variables and `Sub`/
|
|
267
|
+
`Function` parameters -- deliberately excluding `Const`/`ConstTable`, `DataTable`/
|
|
268
|
+
`StructureType`/`ObjectType` names, `Alias` targets, and `Label`s, none of which are a live
|
|
269
|
+
variable a compile-time-constant-only argument could illegally reference.
|
|
270
|
+
|
|
271
|
+
Used only by the constant-only-family check below (`_CONST_ONLY_FAMILY_KINDS`): a narrower
|
|
272
|
+
purpose than `declared_names`' whole-program symbol-exists check, so this is its own function
|
|
273
|
+
rather than a filter over that one's result (`declared_names` does not tag *which kind* of
|
|
274
|
+
name each entry is).
|
|
275
|
+
"""
|
|
276
|
+
names: set[str] = set()
|
|
277
|
+
for stmt in program.statements:
|
|
278
|
+
if isinstance(stmt, S.Declaration):
|
|
279
|
+
for v in stmt.vars:
|
|
280
|
+
names.add(v.name.upper())
|
|
281
|
+
elif isinstance(stmt, S.SubDef):
|
|
282
|
+
for p in stmt.params:
|
|
283
|
+
if p.name.upper() not in program.consts:
|
|
284
|
+
names.add(p.name.upper()) # (a parameter named like a later `Const` is local)
|
|
285
|
+
return names
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def statement_scopes(program: S.Program, profile) -> dict[int, tuple[frozenset[str], frozenset[str]]]:
|
|
289
|
+
"""`id(stmt)` -> (the parameters of the routine whose body holds it, the `Const` names not yet
|
|
290
|
+
declared there). Inside its own body a parameter is a variable even when a later `Const` has
|
|
291
|
+
its name, and a pass-1 constant position sees only earlier `Const`s (oracle, all four loggers,
|
|
292
|
+
round 71, `docs/checker.md` "Constants declared after their use"). A predefined name stays
|
|
293
|
+
predefined before its own `Const` (`Public A(IEEE4)` / `Const IEEE4=1`)."""
|
|
294
|
+
consts = {st.name.upper() for st in program.statements if isinstance(st, S.ConstDecl)} - set(profile.symbols)
|
|
295
|
+
pending = set(consts)
|
|
296
|
+
params: frozenset[str] = frozenset()
|
|
297
|
+
out = {}
|
|
298
|
+
for st in program.statements:
|
|
299
|
+
if isinstance(st, S.SubDef):
|
|
300
|
+
# (a parameter named like an earlier `Const` is `already declared as a CONST`)
|
|
301
|
+
params = frozenset(p.name.upper() for p in st.params) - (consts - pending)
|
|
302
|
+
elif S.ends_routine(st):
|
|
303
|
+
params = frozenset()
|
|
304
|
+
elif isinstance(st, S.ConstDecl):
|
|
305
|
+
pending.discard(st.name.upper())
|
|
306
|
+
out[id(st)] = (params, frozenset(pending))
|
|
307
|
+
return out
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def declared_dim0_sizes(program: S.Program, env: consteval.Env) -> dict[str, int]:
|
|
311
|
+
"""Every `Public`/`Dim`/`Static` variable's own upper-case name -> its *first* dimension's own
|
|
312
|
+
declared constant size, for `structure.py`'s `Illegally dimensioned aliased name` check
|
|
313
|
+
(round 6, `docs/checker.md`). A dimension that does not fold to an `int`/`float` constant is
|
|
314
|
+
skipped (no entry -- the caller must then leave that name unchecked, `structure.py`'s own
|
|
315
|
+
`check_array_dims` makes the same conservative choice for the same reason, AGENTS.md: never
|
|
316
|
+
guess into a false reject).
|
|
317
|
+
|
|
318
|
+
A variable with **no** declared dimensions at all (a plain scalar) maps to `1`, not omitted --
|
|
319
|
+
oracle-confirmed (`docs/checker.md`): `Alias y(2) = Ts` on a scalar `Public y` fails the exact
|
|
320
|
+
same way an out-of-range array index does, while `Alias y(1) = Ts` compiles clean, matching a
|
|
321
|
+
size-1 array rather than "no array at all, so anything goes".
|
|
322
|
+
"""
|
|
323
|
+
sizes: dict[str, int] = {}
|
|
324
|
+
for stmt in program.statements:
|
|
325
|
+
if not (isinstance(stmt, S.Declaration) and stmt.kind in ("PUBLIC", "DIM", "STATIC")):
|
|
326
|
+
continue
|
|
327
|
+
for v in stmt.vars:
|
|
328
|
+
if not v.dims:
|
|
329
|
+
sizes[v.name.upper()] = 1
|
|
330
|
+
continue
|
|
331
|
+
try:
|
|
332
|
+
val = consteval.value_of(v.dims[0], env)
|
|
333
|
+
except consteval.NotConstant:
|
|
334
|
+
continue
|
|
335
|
+
if isinstance(val, (int, float)) and not isinstance(val, bool):
|
|
336
|
+
sizes[v.name.upper()] = int(val)
|
|
337
|
+
return sizes
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def declared_dim_counts(program: S.Program, strings: bool = False) -> dict[str, int]:
|
|
341
|
+
"""Every `Public`/`Dim`/`Static` variable's upper-case name -> its number of declared
|
|
342
|
+
dimensions (0 for a scalar). A name declared twice with different counts is left out, and so
|
|
343
|
+
is a String unless `strings` (CR300/CR350 have no character subscript: `S(1,2)` for
|
|
344
|
+
`Public S(5) As String` is `Dimensions not declared`, oracle 2026-10-01)."""
|
|
345
|
+
counts: dict[str, int] = {}
|
|
346
|
+
ambiguous: set[str] = set()
|
|
347
|
+
strings_seen: set[str] = set()
|
|
348
|
+
for stmt in program.statements:
|
|
349
|
+
if isinstance(stmt, S.Declaration) and stmt.kind in ("PUBLIC", "DIM", "STATIC"):
|
|
350
|
+
for v in stmt.vars:
|
|
351
|
+
name = v.name.upper()
|
|
352
|
+
if (v.type or "").upper() == "STRING":
|
|
353
|
+
strings_seen.add(name)
|
|
354
|
+
if ((v.type or "").upper() == "STRING" and not strings) or re.search(
|
|
355
|
+
r"(?i)\b" + re.escape(v.name) + r"\s*\(\s*\)" + (r"\s*As\b" if strings else ""), stmt.text
|
|
356
|
+
):
|
|
357
|
+
# a String takes a character subscript (`Buf(1,1,I)`); `A()` is its own error
|
|
358
|
+
# on CR6/CR1000X, a scalar to CR300/CR350 (`Public A()`, `X = A(1)` ->
|
|
359
|
+
# `Dimensions not declared`, oracle 2026-10-10) unless a failed `A() As Long`
|
|
360
|
+
ambiguous.add(name)
|
|
361
|
+
elif counts.setdefault(name, len(v.dims)) != len(v.dims):
|
|
362
|
+
ambiguous.add(name)
|
|
363
|
+
out = DimCounts({k: v for k, v in counts.items() if k not in ambiguous})
|
|
364
|
+
out.strings = frozenset(strings_seen)
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class DimCounts(dict):
|
|
369
|
+
"""`declared_dim_counts`'s result; `.strings` names the String variables it includes."""
|
|
370
|
+
|
|
371
|
+
strings: frozenset = frozenset()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def scoped_view(program: S.Program, stmt: S.Stmt, publics: bool = False) -> S.Program:
|
|
375
|
+
"""`program` restricted to the declarations visible at `stmt`: everything outside `Sub`/`Function`
|
|
376
|
+
bodies plus `stmt`'s own routine's body. Other routines' local `Dim`s are dropped, and the
|
|
377
|
+
routine's own sit later in the list so a local shadows a global (last declaration wins). With
|
|
378
|
+
`publics`, another routine's `Public` stays (it is global, `public.htm`), first in the list."""
|
|
379
|
+
tagged, owner, cur = [], None, None
|
|
380
|
+
for st in program.statements:
|
|
381
|
+
if isinstance(st, S.SubDef):
|
|
382
|
+
cur = st
|
|
383
|
+
elif S.ends_routine(st):
|
|
384
|
+
cur = None
|
|
385
|
+
if st is stmt:
|
|
386
|
+
owner = cur
|
|
387
|
+
tagged.append((st, cur))
|
|
388
|
+
stmts: list[S.Stmt] = [st for st, c in tagged if c is None] + [
|
|
389
|
+
st for st, c in tagged if c is not None and c is owner
|
|
390
|
+
]
|
|
391
|
+
if publics:
|
|
392
|
+
stmts = [
|
|
393
|
+
st
|
|
394
|
+
for st, c in tagged
|
|
395
|
+
if c is not None and c is not owner and isinstance(st, S.Declaration) and st.kind == "PUBLIC"
|
|
396
|
+
] + stmts
|
|
397
|
+
return S.Program("", stmts)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class ScopeTables:
|
|
401
|
+
"""`dims` (upper-case name -> full dimension tuple, as `declared_all_dims`), `types` (name -> type,
|
|
402
|
+
arrays always, scalars only when `scalars`) and `arrays` (names declared with dimensions), for the
|
|
403
|
+
declarations visible at one statement."""
|
|
404
|
+
|
|
405
|
+
__slots__ = ("dims", "types", "arrays")
|
|
406
|
+
|
|
407
|
+
def __init__(self, dims, types, arrays):
|
|
408
|
+
self.dims, self.types, self.arrays = dims, types, arrays
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def scope_tables(program: S.Program, env: consteval.Env, scalars: bool = False, parameters: bool = False):
|
|
412
|
+
"""`stmt -> ScopeTables` looking array shapes up by the scope of the use site: a `Sub`/`Function`
|
|
413
|
+
body sees its own local `Dim`s over the globals (a local shadows a global of the same name, oracle
|
|
414
|
+
2026-10-12: `Dim P(5)` in one Sub, `S1(P(15))` against `Public P(20)` in main code compiles, and
|
|
415
|
+
`P(6)` inside that Sub is out of bounds), main code sees only the globals. A routine's own
|
|
416
|
+
parameters hide the name (no entry: the caller skips it), or with `parameters` stand as their
|
|
417
|
+
own storage: the header shape, `(1,)` when none (round 31). Cached per routine."""
|
|
418
|
+
owner: dict[int, S.SubDef | None] = {}
|
|
419
|
+
cur = None
|
|
420
|
+
for st in program.statements:
|
|
421
|
+
if isinstance(st, S.SubDef):
|
|
422
|
+
cur = st
|
|
423
|
+
elif S.ends_routine(st):
|
|
424
|
+
cur = None
|
|
425
|
+
owner[id(st)] = cur
|
|
426
|
+
cache: dict[int, ScopeTables] = {}
|
|
427
|
+
|
|
428
|
+
def build(routine, probe) -> ScopeTables:
|
|
429
|
+
view = scoped_view(program, probe)
|
|
430
|
+
dims = declared_all_dims(view, env)
|
|
431
|
+
types: dict[str, str] = {}
|
|
432
|
+
arrays: set[str] = set()
|
|
433
|
+
for st in view.statements:
|
|
434
|
+
if isinstance(st, S.Declaration) and st.kind in ("PUBLIC", "DIM"):
|
|
435
|
+
for v in st.vars:
|
|
436
|
+
if v.dims or scalars:
|
|
437
|
+
types[v.name.upper()] = (v.type or "FLOAT").upper()
|
|
438
|
+
if v.dims:
|
|
439
|
+
arrays.add(v.name.upper())
|
|
440
|
+
if routine is not None:
|
|
441
|
+
for prm in routine.params:
|
|
442
|
+
n = prm.name.upper()
|
|
443
|
+
dims.pop(n, None)
|
|
444
|
+
types.pop(n, None)
|
|
445
|
+
arrays.discard(n)
|
|
446
|
+
if parameters: # (`Q()` holds one element too, round 31 review)
|
|
447
|
+
try:
|
|
448
|
+
shape = tuple(consteval.value_of(d, env) for d in prm.dim_exprs) or (1,)
|
|
449
|
+
except consteval.NotConstant:
|
|
450
|
+
continue
|
|
451
|
+
if all(
|
|
452
|
+
isinstance(d, (int, float)) and not isinstance(d, bool) and 1 <= d < float("inf") for d in shape
|
|
453
|
+
):
|
|
454
|
+
dims[n], types[n] = tuple(int(d) for d in shape), (prm.type or "FLOAT").upper()
|
|
455
|
+
return ScopeTables(dims, types, arrays)
|
|
456
|
+
|
|
457
|
+
def lookup(stmt) -> ScopeTables:
|
|
458
|
+
routine = owner.get(id(stmt))
|
|
459
|
+
key = id(routine)
|
|
460
|
+
if key not in cache:
|
|
461
|
+
cache[key] = build(routine, stmt)
|
|
462
|
+
return cache[key]
|
|
463
|
+
|
|
464
|
+
return lookup
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def declared_all_dims(program: S.Program, env: consteval.Env) -> dict[str, tuple[int, ...]]:
|
|
468
|
+
"""Every `Public`/`Dim`/`Static` variable's own upper-case name -> its **full** tuple of
|
|
469
|
+
declared dimension sizes, `(1,)` for a scalar (matching `declared_dim0_sizes`'s own "behaves
|
|
470
|
+
like a size-1 array" convention) -- for `structure.py`'s `Illegally dimensioned aliased name`
|
|
471
|
+
check's own row-major flat-offset computation (round 7, `docs/checker.md`). Unlike
|
|
472
|
+
`declared_dim0_sizes` (which only needs the *first* dimension to fold), this one needs *every*
|
|
473
|
+
declared dimension to fold to a positive int/float constant, or the variable is omitted
|
|
474
|
+
entirely (no entry) -- the caller must then leave that name's own flat-offset check unrun
|
|
475
|
+
(AGENTS.md: never guess into a false reject; `structure.py`'s own `_check_array_dims` already
|
|
476
|
+
reports a real error for a declaration whose own dims don't fold this way, so a well-formed
|
|
477
|
+
declaration this checker reaches at all should already have every dim foldable in practice).
|
|
478
|
+
"""
|
|
479
|
+
dims: dict[str, tuple[int, ...]] = {}
|
|
480
|
+
for stmt in program.statements:
|
|
481
|
+
if not (isinstance(stmt, S.Declaration) and stmt.kind in ("PUBLIC", "DIM", "STATIC")):
|
|
482
|
+
continue
|
|
483
|
+
for v in stmt.vars:
|
|
484
|
+
if not v.dims:
|
|
485
|
+
dims[v.name.upper()] = (1,)
|
|
486
|
+
continue
|
|
487
|
+
sizes: list[int] = []
|
|
488
|
+
for d in v.dims:
|
|
489
|
+
try:
|
|
490
|
+
val = consteval.value_of(d, env)
|
|
491
|
+
except consteval.NotConstant:
|
|
492
|
+
break
|
|
493
|
+
if (
|
|
494
|
+
not isinstance(val, (int, float))
|
|
495
|
+
or isinstance(val, bool)
|
|
496
|
+
or val != val
|
|
497
|
+
or val in (float("inf"), float("-inf"))
|
|
498
|
+
):
|
|
499
|
+
break
|
|
500
|
+
sizes.append(int(val))
|
|
501
|
+
else:
|
|
502
|
+
dims[v.name.upper()] = tuple(sizes)
|
|
503
|
+
return dims
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def declared_names(program: S.Program) -> set[str]:
|
|
507
|
+
"""Every name a bare `Name` expression may legally reference: declared `Public`/`Dim`/`Const`/
|
|
508
|
+
`ConstTable` variables, `Sub`/`Function` names and their parameters, `DataTable` and
|
|
509
|
+
`StructureType`/`ObjectType` names, `Alias` targets, and `Label`s (goto targets). A `For`
|
|
510
|
+
loop does not declare its variable (oracle, 2026-10-01: `For j = 1 To 2` -> `j is not
|
|
511
|
+
defined.`). Deliberately global (not scope- or order-checked): a name declared anywhere in the
|
|
512
|
+
program, including inside a `Sub` it is not used in, is accepted everywhere, matching this
|
|
513
|
+
checker's general stance of preferring under-reporting (AGENTS.md's "never reject an accepted
|
|
514
|
+
program" priority) over a full scope analysis this project has not verified against the real
|
|
515
|
+
compiler.
|
|
516
|
+
"""
|
|
517
|
+
names: set[str] = set()
|
|
518
|
+
for stmt in program.statements:
|
|
519
|
+
if isinstance(stmt, S.Declaration):
|
|
520
|
+
for v in stmt.vars:
|
|
521
|
+
names.add(v.name.upper())
|
|
522
|
+
elif isinstance(stmt, S.ConstDecl):
|
|
523
|
+
names.add(stmt.name.upper())
|
|
524
|
+
elif isinstance(stmt, S.SubDef):
|
|
525
|
+
names.add(stmt.name.upper())
|
|
526
|
+
for p in stmt.params:
|
|
527
|
+
names.add(p.name.upper())
|
|
528
|
+
elif isinstance(stmt, S.Label):
|
|
529
|
+
names.add(stmt.name.upper())
|
|
530
|
+
elif isinstance(stmt, S.Instruction):
|
|
531
|
+
kw = stmt.keyword.upper()
|
|
532
|
+
if kw in _NAME_DECLARING_INSTRUCTIONS and stmt.args and isinstance(stmt.args[0], S.Name):
|
|
533
|
+
names.add(stmt.args[0].name.upper())
|
|
534
|
+
elif kw == "ALIAS":
|
|
535
|
+
for arg in stmt.args[1:]:
|
|
536
|
+
# `Alias X = Y(2)` registers `Y` (round 30)
|
|
537
|
+
if isinstance(arg, S.Name) or (isinstance(arg, S.Index) and len(arg.args) == 1):
|
|
538
|
+
names.add(arg.name.upper())
|
|
539
|
+
# `SNMPVariable(MyTemp, 1.3.6.1..., Float, RONLY)` declares `MyTemp` (oracle, all four
|
|
540
|
+
# loggers, 2026-09-28; a CR350 fuzz false reject). Its dotted OID does not parse here.
|
|
541
|
+
m = _SNMP_VARIABLE_RE.match(getattr(stmt, "text", "") or "")
|
|
542
|
+
if m:
|
|
543
|
+
names.add(m.group(1).upper())
|
|
544
|
+
if isinstance(stmt, S.Unparsed) and stmt.reason.startswith("Invalid As clause: "):
|
|
545
|
+
names.update(_names_through_bad_as_clause(stmt))
|
|
546
|
+
return names
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _names_through_bad_as_clause(stmt: S.Unparsed) -> set[str]:
|
|
550
|
+
"""`Public Q, Flag A s Boolean`: the variables up to and including the one with the invalid
|
|
551
|
+
`As` clause are registered, later ones are not (oracle, CR300, 2026-10-08)."""
|
|
552
|
+
m = re.match(r"(?i)\s*(?:Public|Dim|Static)\s+(.*)$", stmt.text or "", re.S)
|
|
553
|
+
if not m:
|
|
554
|
+
return set()
|
|
555
|
+
bad = re.escape(stmt.reason[len("Invalid As clause: ") :].strip())
|
|
556
|
+
out: set[str] = set()
|
|
557
|
+
for item in _split_top_commas(m.group(1)): # not a comma inside an array's dimensions
|
|
558
|
+
name = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)", item)
|
|
559
|
+
if not name:
|
|
560
|
+
break
|
|
561
|
+
out.add(name.group(1).upper())
|
|
562
|
+
if bad and re.search(rf"\b{bad}\b", item[name.end() :]):
|
|
563
|
+
break
|
|
564
|
+
return out
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
_SNMP_VARIABLE_RE = re.compile(r"(?i)\s*SNMPVariable\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)")
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def is_reserved_predefined_name(name: str, profile) -> bool:
|
|
571
|
+
"""Declaration namespace, docs/checker.md#predefined-constant-name-collisions."""
|
|
572
|
+
key = name.upper()
|
|
573
|
+
return key in profile.symbols and key not in profile.declarable_predefined_names
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def report_predefined_name_collision(stmt: S.Stmt, name: str, profile, diags: Diagnostics) -> bool:
|
|
577
|
+
if not is_reserved_predefined_name(name, profile):
|
|
578
|
+
return False
|
|
579
|
+
diags.error(stmt.line, f"{name} is a predefined CONST.", stmt.file)
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def report_function_name_collision(stmt: S.Stmt, name: str, profile, diags: Diagnostics) -> bool:
|
|
584
|
+
if name.upper() not in profile.functions:
|
|
585
|
+
return False
|
|
586
|
+
diags.error(stmt.line, f"{name} already defined as an internal function.", stmt.file)
|
|
587
|
+
return True
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def check_local_name_collision(stmt: S.Stmt, profile, diags: Diagnostics) -> None:
|
|
591
|
+
"""Routine namespace checks in declaration order; local Dim/parameters shadow functions."""
|
|
592
|
+
if isinstance(stmt, S.ConstDecl):
|
|
593
|
+
names = [stmt.name]
|
|
594
|
+
shadows_functions = False
|
|
595
|
+
elif isinstance(stmt, S.Declaration) and stmt.kind in ("PUBLIC", "DIM"):
|
|
596
|
+
names = [v.name for v in stmt.vars]
|
|
597
|
+
shadows_functions = stmt.kind == "DIM"
|
|
598
|
+
elif isinstance(stmt, S.SubDef):
|
|
599
|
+
names = [p.name for p in stmt.params]
|
|
600
|
+
shadows_functions = True
|
|
601
|
+
else:
|
|
602
|
+
return
|
|
603
|
+
for index, name in enumerate(names):
|
|
604
|
+
collision = not shadows_functions and report_function_name_collision(stmt, name, profile, diags)
|
|
605
|
+
collision = collision or report_predefined_name_collision(stmt, name, profile, diags)
|
|
606
|
+
if collision and profile.single_pass:
|
|
607
|
+
if index + 2 < len(names):
|
|
608
|
+
diags.error(stmt.line, f"Too many parameters: {names[index + 2]}.", stmt.file)
|
|
609
|
+
break
|