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.
Files changed (117) hide show
  1. opencrbasic/__init__.py +3 -0
  2. opencrbasic/__main__.py +5 -0
  3. opencrbasic/analysis.py +184 -0
  4. opencrbasic/bindings.py +1034 -0
  5. opencrbasic/catalog.py +173 -0
  6. opencrbasic/checkconfig.py +144 -0
  7. opencrbasic/checker/__init__.py +977 -0
  8. opencrbasic/checker/cr3xx.py +3118 -0
  9. opencrbasic/checker/ess.py +81 -0
  10. opencrbasic/checker/expressions.py +3581 -0
  11. opencrbasic/checker/instr_data.py +1802 -0
  12. opencrbasic/checker/instr_measure.py +1547 -0
  13. opencrbasic/checker/instr_rules.py +3540 -0
  14. opencrbasic/checker/instructions.py +17 -0
  15. opencrbasic/checker/intervals.py +678 -0
  16. opencrbasic/checker/longconv.py +133 -0
  17. opencrbasic/checker/scan_time.py +580 -0
  18. opencrbasic/checker/storage.py +456 -0
  19. opencrbasic/checker/structs.py +240 -0
  20. opencrbasic/checker/structure.py +6213 -0
  21. opencrbasic/checker/symbols.py +609 -0
  22. opencrbasic/checker/terminals.py +249 -0
  23. opencrbasic/checker/unused.py +186 -0
  24. opencrbasic/checker/var_types.py +564 -0
  25. opencrbasic/checker/warnings.py +661 -0
  26. opencrbasic/checks.py +491 -0
  27. opencrbasic/cli.py +391 -0
  28. opencrbasic/commands.py +128 -0
  29. opencrbasic/compiler.py +320 -0
  30. opencrbasic/consteval.py +327 -0
  31. opencrbasic/data/background_calibration.json +4 -0
  32. opencrbasic/data/before_beginprog.json +99 -0
  33. opencrbasic/data/cr1000x/background_calibration.json +4 -0
  34. opencrbasic/data/cr1000x/before_beginprog.json +138 -0
  35. opencrbasic/data/cr1000x/ess_variables.json +1087 -0
  36. opencrbasic/data/cr1000x/fnotch.json +29 -0
  37. opencrbasic/data/cr1000x/functions.json +1337 -0
  38. opencrbasic/data/cr1000x/instructions.json +4836 -0
  39. opencrbasic/data/cr1000x/measure.json +121 -0
  40. opencrbasic/data/cr1000x/mode.json +111 -0
  41. opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
  42. opencrbasic/data/cr1000x/settings_fields.json +321 -0
  43. opencrbasic/data/cr1000x/status_members.json +318 -0
  44. opencrbasic/data/cr1000x/symbols.json +872 -0
  45. opencrbasic/data/cr1000x/terminals.json +225 -0
  46. opencrbasic/data/cr300/before_beginprog.json +42 -0
  47. opencrbasic/data/cr300/ess_variables.json +1087 -0
  48. opencrbasic/data/cr300/fnotch.json +22 -0
  49. opencrbasic/data/cr300/functions.json +1261 -0
  50. opencrbasic/data/cr300/instructions.json +2837 -0
  51. opencrbasic/data/cr300/measure.json +33 -0
  52. opencrbasic/data/cr300/mode.json +121 -0
  53. opencrbasic/data/cr300/settings_fields.json +259 -0
  54. opencrbasic/data/cr300/status_members.json +303 -0
  55. opencrbasic/data/cr300/symbols.json +584 -0
  56. opencrbasic/data/cr300/terminals.json +193 -0
  57. opencrbasic/data/cr350/before_beginprog.json +44 -0
  58. opencrbasic/data/cr350/ess_variables.json +1087 -0
  59. opencrbasic/data/cr350/fnotch.json +22 -0
  60. opencrbasic/data/cr350/functions.json +1262 -0
  61. opencrbasic/data/cr350/instructions.json +2848 -0
  62. opencrbasic/data/cr350/measure.json +35 -0
  63. opencrbasic/data/cr350/mode.json +121 -0
  64. opencrbasic/data/cr350/settings_fields.json +292 -0
  65. opencrbasic/data/cr350/status_members.json +303 -0
  66. opencrbasic/data/cr350/symbols.json +572 -0
  67. opencrbasic/data/cr350/terminals.json +190 -0
  68. opencrbasic/data/ess_variables.json +1087 -0
  69. opencrbasic/data/fnotch.json +34 -0
  70. opencrbasic/data/functions.json +1337 -0
  71. opencrbasic/data/instructions.json +4965 -0
  72. opencrbasic/data/measure.json +123 -0
  73. opencrbasic/data/mode.json +121 -0
  74. opencrbasic/data/notices_cr6.json +890 -0
  75. opencrbasic/data/pipeline_tasks.json +84 -0
  76. opencrbasic/data/settings_fields.json +322 -0
  77. opencrbasic/data/status_members.json +319 -0
  78. opencrbasic/data/status_table.json +684 -0
  79. opencrbasic/data/symbols.json +902 -0
  80. opencrbasic/data/terminals.json +195 -0
  81. opencrbasic/data/type_codes.json +249 -0
  82. opencrbasic/diagnostics.py +196 -0
  83. opencrbasic/editing.py +827 -0
  84. opencrbasic/formatting.py +310 -0
  85. opencrbasic/helpdocs.py +677 -0
  86. opencrbasic/hover.py +667 -0
  87. opencrbasic/inlay.py +169 -0
  88. opencrbasic/lexer.py +183 -0
  89. opencrbasic/lsp/__init__.py +5 -0
  90. opencrbasic/lsp/__main__.py +7 -0
  91. opencrbasic/lsp/config.py +202 -0
  92. opencrbasic/lsp/features.py +602 -0
  93. opencrbasic/lsp/server.py +741 -0
  94. opencrbasic/lsp/session.py +1189 -0
  95. opencrbasic/lsp/worker.py +128 -0
  96. opencrbasic/mode.py +143 -0
  97. opencrbasic/navigation.py +401 -0
  98. opencrbasic/parser.py +3264 -0
  99. opencrbasic/positions.py +274 -0
  100. opencrbasic/preprocess.py +269 -0
  101. opencrbasic/profiles.py +266 -0
  102. opencrbasic/provenance.py +17 -0
  103. opencrbasic/quickfix.py +331 -0
  104. opencrbasic/rename.py +662 -0
  105. opencrbasic/signature.py +33 -0
  106. opencrbasic/source.py +600 -0
  107. opencrbasic/sources.py +125 -0
  108. opencrbasic/syntax.py +337 -0
  109. opencrbasic/tables.py +2173 -0
  110. opencrbasic/tables_hook.py +14 -0
  111. opencrbasic/tdf.py +142 -0
  112. opencrbasic/timing.py +565 -0
  113. opencrbasic-1.0.0.dist-info/METADATA +223 -0
  114. opencrbasic-1.0.0.dist-info/RECORD +117 -0
  115. opencrbasic-1.0.0.dist-info/WHEEL +4 -0
  116. opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
  117. 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