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,977 @@
|
|
|
1
|
+
"""Semantic checker: `check(program, profile, diags, result)`.
|
|
2
|
+
|
|
3
|
+
Two passes, mirroring the real compiler's own architecture (`docs/checker.md` "Pass model",
|
|
4
|
+
oracle-confirmed): **pass 1** parses every statement into a record and is lenient about *shape* --
|
|
5
|
+
an unrecognised line becomes an `Expr`/`Call` record without error at this stage. Pass-1 errors are
|
|
6
|
+
parsing and declaration errors (parameter-count/shape by type code, `Variable not named.`,
|
|
7
|
+
`Units`/`Const`/`Alias` truncation, array-dimension limits, block matching); every one in the file
|
|
8
|
+
is reported regardless of what else is wrong. **Pass 2** runs only when pass 1 reported nothing at
|
|
9
|
+
all, and compiles the records in order: this is where the generic "Expr"/implicit-`Call` fallback
|
|
10
|
+
(`Invalid, or out of place expression: %s.`, `Sub %s not defined.`) and every instruction's own
|
|
11
|
+
semantic handler (`expressions.py`'s undefined-symbol/constant-only checks, `instructions.py`'s
|
|
12
|
+
channel/port checks, `instr_data.py`'s table-structure checks, `instr_measure.py`'s value checks)
|
|
13
|
+
live. Pass 2 stops entirely at the first **placement** error (a statement occurring somewhere it
|
|
14
|
+
structurally cannot, e.g. before `BeginProg`) -- `structure.check_pass2` is both the "Expr" fallback
|
|
15
|
+
*and* the only place that currently detects a placement error (the before-`BeginProg` case; two
|
|
16
|
+
other real placement shapes are not yet implemented, `docs/checker.md`), so its return value (the
|
|
17
|
+
placement error's own line, or `None`) is what `check` below uses to truncate every other pass-2
|
|
18
|
+
module's diagnostics to the same cutoff, without those modules needing to coordinate it themselves.
|
|
19
|
+
|
|
20
|
+
Runs, in order:
|
|
21
|
+
|
|
22
|
+
1. `structure.check_pass1` -- `BeginProg`/`EndProg` presence, `Scan`/`NextScan` and `If`/`Select`
|
|
23
|
+
block matching, declaration/array-dimension errors, `Units`/`Const`/`Alias` truncation.
|
|
24
|
+
2. `expressions.check_consts` -- an undefined name inside a `Const`'s own value expression
|
|
25
|
+
(`<name> is not defined.`/`Invalid symbol in expression: %s.`, per-logger). Also pass 1 (oracle
|
|
26
|
+
evidence, `docs/loggers.md`: `Const` folding happens eagerly at declaration time, so this
|
|
27
|
+
accumulates alongside an unrelated, earlier `Variable not named.` rather than being suppressed
|
|
28
|
+
by it the way the *rest* of `expressions.py`'s pass-2 checks would be). `expressions.
|
|
29
|
+
check_data_params` -- `Data`/`DataLong`'s own parameters, each required to be a constant
|
|
30
|
+
expression with no name reference at all, defined or not (`Invalid symbol in expression: %s.`/
|
|
31
|
+
`Illegal Data parameter %s.`, `docs/checker.md`) -- runs right after it, also ungated pass 1,
|
|
32
|
+
the same way.
|
|
33
|
+
3. `cr3xx.check` -- CR300/CR350 only (a no-op on every other logger): `SerialOpen`'s own `ComPort`
|
|
34
|
+
argument and every other `Serial*` function's own first argument (`docs/loggers.md`). Also
|
|
35
|
+
ungated, oracle-confirmed the same way as step 2.
|
|
36
|
+
4. `instr_rules.check` -- a required-parameter position with no value at all ("Invalid or missing
|
|
37
|
+
parameter.", `checker/instr_rules.py`, owned alongside this package). Also pass 1 (oracle
|
|
38
|
+
evidence, `docs/checker.md`: a file with several earlier "Variable not named." errors still
|
|
39
|
+
gets its own, later "Invalid or missing parameter." reported). `structure.
|
|
40
|
+
check_missing_param_followup` runs right after it, also ungated: the physical line the
|
|
41
|
+
shortfall's own wrapped argument list spills onto gets its own "Invalid, or out of place
|
|
42
|
+
expression: %s." when every name it references resolves (`docs/checker.md` "Missing-parameter
|
|
43
|
+
followup"). `instr_rules.check_too_many` --
|
|
44
|
+
the mirror image, an argument list running past the declared count ("Too many parameters: %s.",
|
|
45
|
+
round 5, `docs/checker.md`) -- runs alongside it, also pass 1 (oracle-confirmed the same way).
|
|
46
|
+
`expressions.check_function_arity` -- the same shortfall/overflow shape, but for a *built-in
|
|
47
|
+
function* call inside an expression rather than an instruction's own argument list ("Expression
|
|
48
|
+
expects , : %s."/"No Matching, or out of place ) in expression: %s.", `docs/checker.md`); also
|
|
49
|
+
pass 1, oracle-confirmed the same way.
|
|
50
|
+
5. `warnings.check_early` -- reachability/fnotch; ungated, fires even when the file has other
|
|
51
|
+
errors elsewhere (`docs/instructions.md`). `warnings.check_background_calibration` is a
|
|
52
|
+
related but separately-gated check, run at step 11 below instead (round 6, `docs/checker.md`).
|
|
53
|
+
|
|
54
|
+
If, and only if, steps 1-5 found nothing:
|
|
55
|
+
|
|
56
|
+
6. `structure.check_pass2` -- the generic "Expr"/implicit-`Call` fallback and the before-`BeginProg`
|
|
57
|
+
placement check (returns the stop, if any).
|
|
58
|
+
7. `expressions.check` -- a bare identifier or compound expression used at an instruction argument
|
|
59
|
+
position that resolves nowhere, and the constant-only-family "rejects a variable" check
|
|
60
|
+
(deliberately narrow; see that module's docstring). `instr_data.check_unclosed_calltable` runs
|
|
61
|
+
alongside it, then `cr3xx.check_unknown_line_words` (CR300/CR350 only, one leading-word `%s is
|
|
62
|
+
not defined.` line per `S.Unparsed` statement's own generic
|
|
63
|
+
fallback, `docs/loggers.md` "Round 8").
|
|
64
|
+
8. `instructions.check` -- per-instruction argument checks layered on top of the generic type-code
|
|
65
|
+
parsing (the analog-channel/port terminal range checks, `terminals.py`); `instr_data.
|
|
66
|
+
check_table_structure` (table-structure placement) and `instr_measure.check` (`Range`/`SW12`/
|
|
67
|
+
`Delay`/`CS616`/`TimerInput`/`ExciteV`/`ExciteI` value checks, `docs/instructions.md`) run
|
|
68
|
+
alongside it.
|
|
69
|
+
9. Every error step 6-8 added is truncated to the earliest stop (expanded-source position) of steps 6-8.
|
|
70
|
+
|
|
71
|
+
Then, regardless of errors so far:
|
|
72
|
+
|
|
73
|
+
9b. `cr3xx.check_too_many_cascade` (CR300/CR350 only, a no-op on every other logger): once any
|
|
74
|
+
undefined-name-class or genuine `Too many parameters:` finding has fired anywhere above, in
|
|
75
|
+
any tier, every following two-plus-argument recognised instruction to end of file gets its own
|
|
76
|
+
spurious `Too many parameters: %s.` in place of whatever it would otherwise have found
|
|
77
|
+
(`checker/cr3xx.py`'s own module comment for the oracle evidence). Run here, unconditionally,
|
|
78
|
+
rather than inside step 7's own tier-B block, because the taint can originate from a tier-A
|
|
79
|
+
finding that skips tier B/C entirely (`docs/loggers.md`).
|
|
80
|
+
10. `mode.decide` -- Sequential/PipelineMode (`mode.py`, reusing the parser's own folded `Const`
|
|
81
|
+
values via `consteval.Env`, as the previous placeholder checker did).
|
|
82
|
+
11. When the compile has no errors so far: `warnings.check_background_calibration` (round 6, see
|
|
83
|
+
that function's own docstring for why it is not part of step 5 above), then `tables_hook.
|
|
84
|
+
attach`, when the module is available (it is written by a different concurrent workstream,
|
|
85
|
+
`PLAN.md`; guarded so this checker still runs standalone while that module is in progress),
|
|
86
|
+
then `warnings.check` ("table declared but never called") and
|
|
87
|
+
`instr_data.check_duplicate_fields`.
|
|
88
|
+
|
|
89
|
+
Full instruction-signature checks (the rest of `instructions.py`) and a general expression-tree
|
|
90
|
+
name/type resolution (the rest of `expressions.py`) are future work; see `docs/checker.md` and
|
|
91
|
+
`PROGRESS.md`'s phase-3 row for current coverage and the oracle evidence behind each rule above.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
import dataclasses
|
|
95
|
+
import os
|
|
96
|
+
from contextvars import ContextVar
|
|
97
|
+
|
|
98
|
+
from .. import consteval, mode
|
|
99
|
+
from .. import syntax as S
|
|
100
|
+
from ..diagnostics import Diagnostic, Diagnostics
|
|
101
|
+
from ..profiles import Profile
|
|
102
|
+
from . import (
|
|
103
|
+
cr3xx,
|
|
104
|
+
ess,
|
|
105
|
+
expressions,
|
|
106
|
+
instr_data,
|
|
107
|
+
instr_measure,
|
|
108
|
+
instr_rules,
|
|
109
|
+
instructions,
|
|
110
|
+
intervals,
|
|
111
|
+
storage,
|
|
112
|
+
structs,
|
|
113
|
+
structure,
|
|
114
|
+
var_types,
|
|
115
|
+
warnings,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
_current_result: ContextVar = ContextVar("opencrbasic_checker_result", default=None)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _is_unreached(d) -> bool:
|
|
122
|
+
return d.severity == "warning" and d.message.endswith(" never reached at run time.")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run_pass(fn, *args) -> None:
|
|
126
|
+
"""Never let one checker pass's bug take down the whole compile (AGENTS.md: a crash is worse
|
|
127
|
+
than a wrong-but-returned answer; `docs/fuzz-findings.md` "Hang"/"Crash" -- both found by
|
|
128
|
+
fuzzing, both in code this session does not exhaustively re-verify). A pass that raises is
|
|
129
|
+
treated as having found nothing further this run: whatever diagnostics it already recorded
|
|
130
|
+
(via `diags`, mutated in place) stand, and every other pass still runs. No oracle text can ever
|
|
131
|
+
exist for "opencrbasic itself broke", so this never invents a diagnostic to paper over it --
|
|
132
|
+
the caller is responsible for still returning a usable `CompileResult` either way.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
fn(*args)
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
_record_exception(fn, exc)
|
|
138
|
+
if os.environ.get("OPENCRBASIC_STRICT") == "1":
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Instructions whose compile function rejects the placement (`TimerInput must be used within a
|
|
143
|
+
# Scan...NextScan.`, `MenuItem must be used within DisplayMenu..EndMenu.`) before resolving their own
|
|
144
|
+
# arguments: an undeclared name in them is not reported (oracle, CR6/CR1000X, 2026-10-12, `docs/checker.md`
|
|
145
|
+
# "Round 14 placement before arguments"). `PulseCount` resolves its arguments first and is not listed.
|
|
146
|
+
_PLACEMENT_FIRST = {
|
|
147
|
+
"TIMERINPUT": "a Scan...NextScan.",
|
|
148
|
+
"MENUITEM": "DisplayMenu..EndMenu.",
|
|
149
|
+
"MENURECOMPILE": "DisplayMenu..EndMenu.",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _placement_rejected(program: S.Program, profile, diags: Diagnostics) -> frozenset:
|
|
154
|
+
"""`id()`s of the `_PLACEMENT_FIRST` statements that already carry their placement error."""
|
|
155
|
+
if profile.single_pass:
|
|
156
|
+
return frozenset()
|
|
157
|
+
found = set()
|
|
158
|
+
for stmt in program.statements:
|
|
159
|
+
if not isinstance(stmt, S.Instruction) or stmt.keyword.upper() not in _PLACEMENT_FIRST:
|
|
160
|
+
continue
|
|
161
|
+
kw = stmt.keyword.upper()
|
|
162
|
+
text = f"{profile.instructions[kw]['name']} must be used within {_PLACEMENT_FIRST[kw]}"
|
|
163
|
+
if any(
|
|
164
|
+
d.severity == "error" and d.line == stmt.line and d.file == stmt.file and d.message == text
|
|
165
|
+
for d in diags.items
|
|
166
|
+
):
|
|
167
|
+
found.add(id(stmt))
|
|
168
|
+
return frozenset(found)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _with_inline(program: S.Program) -> S.Program:
|
|
172
|
+
"""A copy of `program` whose statement list also holds each one-line `If`'s statements, right
|
|
173
|
+
after the `If`."""
|
|
174
|
+
statements: list[S.Stmt] = []
|
|
175
|
+
flat_at: list[int] = [] # raw statement index -> index in the flattened list
|
|
176
|
+
for stmt in program.statements:
|
|
177
|
+
flat_at.append(len(statements))
|
|
178
|
+
statements.append(stmt)
|
|
179
|
+
if isinstance(stmt, S.If):
|
|
180
|
+
statements.extend((stmt.inline or []) + (stmt.inline_else or []))
|
|
181
|
+
flat_at.append(len(statements))
|
|
182
|
+
# (`include_error_at` indexes the raw list: re-index it, or each earlier one-line `If` body
|
|
183
|
+
# would shift the CR300/CR350 cascade start too early)
|
|
184
|
+
return dataclasses.replace(
|
|
185
|
+
program,
|
|
186
|
+
statements=statements,
|
|
187
|
+
include_error_at=[flat_at[min(i, len(flat_at) - 1)] for i in program.include_error_at],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _run_pass_capture(fn, *args):
|
|
192
|
+
"""Like `_run_pass`, but returns `fn`'s own return value (or `None` on an exception) -- for
|
|
193
|
+
`structure.check_pass2`, whose return value (the placement-error cutoff line) the caller needs.
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
return fn(*args)
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
_record_exception(fn, exc)
|
|
199
|
+
if os.environ.get("OPENCRBASIC_STRICT") == "1":
|
|
200
|
+
raise
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _record_exception(fn, exc: Exception) -> None:
|
|
205
|
+
result = _current_result.get()
|
|
206
|
+
if result is not None:
|
|
207
|
+
result.record_implementation_failure(f"{fn.__module__}.{fn.__name__}", exc)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def check(program: S.Program, profile: Profile, diags: Diagnostics, result) -> None:
|
|
211
|
+
token = _current_result.set(result)
|
|
212
|
+
try:
|
|
213
|
+
_check(program, profile, diags, result)
|
|
214
|
+
finally:
|
|
215
|
+
_current_result.reset(token)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _place_index_bounds(bounds: Diagnostics, diags: Diagnostics, program: S.Program, single_pass: bool = False) -> None:
|
|
219
|
+
"""CR6/CR1000X `Variable %s out of bounds.` from the index check (`bounds`), which is appended
|
|
220
|
+
after tier C, with two corrections (oracle, round 9, docs/checker.md "Index bounds placement"):
|
|
221
|
+
a message the destination check already printed on the line is not printed twice
|
|
222
|
+
(`VoltSe(A(11),1,..,13,..)`), and for `PortGet` the destination (argument 1) comes before the
|
|
223
|
+
port (argument 2), so it is inserted ahead of `Invalid port ...` (`PortSet(1,A(11))` keeps the
|
|
224
|
+
port first). CR300/CR350 (`single_pass`): only the first correction (round 31, `Therm109(A(6),
|
|
225
|
+
..)`, `VoltSe(A(6),..)`; docs/checker.md "Measurement destination and channel spans")."""
|
|
226
|
+
portget = (
|
|
227
|
+
set()
|
|
228
|
+
if single_pass
|
|
229
|
+
else {
|
|
230
|
+
(st.file, st.line)
|
|
231
|
+
for st in program.statements
|
|
232
|
+
if isinstance(st, S.Instruction) and st.keyword.upper() == "PORTGET"
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
kept = []
|
|
236
|
+
printed = list(diags.items) # before any insertion below
|
|
237
|
+
for n, d in enumerate(bounds.items):
|
|
238
|
+
same = lambda o: (o.file, o.line, o.message) == (d.file, d.line, d.message) # noqa: E731
|
|
239
|
+
if sum(map(same, bounds.items[:n])) < sum(map(same, printed)):
|
|
240
|
+
continue # the destination check printed it
|
|
241
|
+
at = next(
|
|
242
|
+
(
|
|
243
|
+
i
|
|
244
|
+
for i, o in enumerate(diags.items)
|
|
245
|
+
if (o.file, o.line) == (d.file, d.line) and o.message.startswith("Invalid port")
|
|
246
|
+
),
|
|
247
|
+
None,
|
|
248
|
+
)
|
|
249
|
+
if at is not None and (d.file, d.line) in portget:
|
|
250
|
+
diags.items.insert(at, d)
|
|
251
|
+
else:
|
|
252
|
+
kept.append(d)
|
|
253
|
+
bounds.items = kept
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _check(program: S.Program, profile: Profile, diags: Diagnostics, result) -> None:
|
|
257
|
+
# `Unknown instruction: #...` ends the compile on all four loggers: nothing after that
|
|
258
|
+
# statement is reported (oracle, 2026-10-10: `#a` then `#b` -> only `#a`)
|
|
259
|
+
stop = next((i for i, st in enumerate(program.statements) if S.hash_statement(st)), None)
|
|
260
|
+
if stop is not None:
|
|
261
|
+
program = dataclasses.replace(
|
|
262
|
+
program, statements=[*program.statements[:stop], S.cut_after_hash(program.statements[stop])]
|
|
263
|
+
)
|
|
264
|
+
if program.hash_cut is not None:
|
|
265
|
+
del diags.items[program.hash_cut :] # parser findings of later lines
|
|
266
|
+
program = structure.open_program_at_slowsequence(program, profile)
|
|
267
|
+
env = consteval.Env(profile, dict(program.consts))
|
|
268
|
+
|
|
269
|
+
_run_pass(structure.check_pass1, program, profile, diags, env)
|
|
270
|
+
# Also pass 1 (unconditional -- oracle-confirmed, `docs/loggers.md`): a `Const`'s own value
|
|
271
|
+
# expression is folded eagerly at declaration time, so an undefined name inside it is
|
|
272
|
+
# reported alongside pass-1's other declaration errors, not deferred to pass 2's gated
|
|
273
|
+
# `expressions.check` below.
|
|
274
|
+
_run_pass(expressions.check_consts, program, profile, diags)
|
|
275
|
+
_run_pass(instr_data.check_constant_destinations, program, profile, diags, env)
|
|
276
|
+
# CR6/CR1000X: constant-family instruction arguments resolve in pass 1 too.
|
|
277
|
+
_run_pass(expressions.check_const_family_args, _with_inline(program), profile, diags)
|
|
278
|
+
# CR6/CR1000X: `Range` code and `fN1` value checks, same pass (`docs/instructions.md`).
|
|
279
|
+
_run_pass(instr_measure.check_pass1_values, program, profile, diags, env)
|
|
280
|
+
# `TimeIntoInterval`/`IfTime` constant arguments, a parser check (`checker/intervals.py`).
|
|
281
|
+
_run_pass(var_types.check_string_operands, program, profile, diags)
|
|
282
|
+
_run_pass(intervals.check, program, profile, diags, env)
|
|
283
|
+
_run_pass(instr_rules.check_const_int_args, program, profile, diags, env)
|
|
284
|
+
_run_pass(expressions.check_function_call_counts, program, profile, diags)
|
|
285
|
+
_run_pass(expressions.check_recursive_functions, program, profile, diags)
|
|
286
|
+
# `Data`/`DataLong` parameter constant-only checking (`expressions.py`'s own docstring for the
|
|
287
|
+
# oracle evidence). Also ungated pass 1, same reasoning as `check_consts` just above.
|
|
288
|
+
_run_pass(expressions.check_data_params, program, profile, diags)
|
|
289
|
+
# CR300/CR350 only (a no-op on every other logger, `checker/cr3xx.py`): `SerialOpen`'s own
|
|
290
|
+
# `ComPort` argument, and every other `Serial*` function's own first argument. Also ungated
|
|
291
|
+
# (oracle-confirmed, `docs/loggers.md`).
|
|
292
|
+
_run_pass(cr3xx.check, program, profile, diags)
|
|
293
|
+
_run_pass(cr3xx.check_dropped_tail, program, profile, diags)
|
|
294
|
+
_run_pass(instr_rules.check, _with_inline(program), profile, diags) # (a one-line `If` body too)
|
|
295
|
+
_run_pass(instr_rules.check_return_placement, program, profile, diags)
|
|
296
|
+
# The leftover line a required-parameter shortfall's own wrapped argument list spills onto
|
|
297
|
+
# (`checker/structure.py`'s `check_missing_param_followup` docstring for the oracle evidence):
|
|
298
|
+
# also ungated, since it fires precisely because the call just above already found something.
|
|
299
|
+
_run_pass(structure.check_missing_param_followup, program, profile, diags)
|
|
300
|
+
_run_pass(instr_rules.check_too_many, _with_inline(program), profile, diags)
|
|
301
|
+
_run_pass(instr_rules.check_essinitialize_too_many, program, profile, diags)
|
|
302
|
+
_run_pass(instr_rules.check_snmpvariable_too_many, program, profile, diags)
|
|
303
|
+
_run_pass(instr_rules.check_snmp_oid, program, profile, diags, env)
|
|
304
|
+
_run_pass(instr_rules.check_modemcallback_baud, program, profile, diags)
|
|
305
|
+
_run_pass(instr_rules.check_serialin_destination, program, profile, diags)
|
|
306
|
+
_run_pass(instr_rules.check_serialin_comport, _with_inline(program), profile, diags, env)
|
|
307
|
+
_run_pass(instr_rules.check_emailrecv_destination, program, profile, diags)
|
|
308
|
+
_run_pass(instr_rules.check_tcpopen_connections, program, profile, diags, env)
|
|
309
|
+
_run_pass(instr_rules.check_tcpopen_handle_type, program, profile, diags)
|
|
310
|
+
# `AddPrecise`/`MovePrecise`'s own `PrecisionVariable` argument (`instr_rules.py`'s own
|
|
311
|
+
# `_PRECISION_VARIABLE_POSITIONS` docstring for the oracle evidence). Also pass 1,
|
|
312
|
+
# unconditional -- oracle-confirmed the same way as the two calls just above.
|
|
313
|
+
_run_pass(instr_rules.check_precision_variable, program, profile, diags)
|
|
314
|
+
_run_pass(instr_rules.check_vibrating_wire_diagnostics, program, profile, diags)
|
|
315
|
+
_run_pass(instr_rules.check_iif_types, _with_inline(program), profile, diags)
|
|
316
|
+
# Built-in function call arity (`Expression expects , : %s.` / `No Matching, or out of place )
|
|
317
|
+
# in expression: %s.`, `checker/expressions.py`'s `check_function_arity`, own module docstring
|
|
318
|
+
# for the oracle evidence and the safe allow-lists). Also pass 1, unconditional -- oracle-
|
|
319
|
+
# confirmed the same way as the two calls just above (`docs/checker.md`).
|
|
320
|
+
_run_pass(var_types.check_ctype, program, profile, diags)
|
|
321
|
+
_run_pass(var_types.check_pointer_deref, program, profile, diags)
|
|
322
|
+
held_member: list = [] # compile-stage `Member` index errors, see `pass1_errors` below
|
|
323
|
+
_run_pass(expressions.check_function_arity, _with_inline(program), profile, diags, held_member)
|
|
324
|
+
# Ungated on CR6/CR1000X (unlike step 2 below): reachability/background-calibration/fnotch all
|
|
325
|
+
# oracle-confirmed to fire even when the file has other errors elsewhere (`checker/warnings.py`,
|
|
326
|
+
# `docs/instructions.md`). **Not** shared by CR300/CR350 (`profile.early_warnings_gated`,
|
|
327
|
+
# `docs/loggers.md`): there, this same call waits until tier B and the table `Reps` checks are
|
|
328
|
+
# clean.
|
|
329
|
+
if not profile.single_pass and not diags.errors:
|
|
330
|
+
# CR6/CR1000X: an unknown structure member is a pass-1 error (`checker/structs.py`).
|
|
331
|
+
_run_pass(structs.check_members, program, profile, diags, _with_inline(program))
|
|
332
|
+
early = Diagnostics() # CR6/CR1000X `Scan`/`DataInterval` value errors, see below
|
|
333
|
+
if not profile.early_warnings_gated:
|
|
334
|
+
early_start = len(diags.items)
|
|
335
|
+
errors_before_early = len(diags.errors)
|
|
336
|
+
_run_pass(warnings.check_early, program, profile, diags, env)
|
|
337
|
+
if not profile.single_pass and not errors_before_early:
|
|
338
|
+
_run_pass(instr_measure.check_scan_delay_budget, program, profile, diags, env, True)
|
|
339
|
+
# Pass-2 findings that neither gate pass 2 nor tier C (oracle, CR6/CR1000X, 2026-10-03:
|
|
340
|
+
# `Scan(0,Sec,1,0)` plus a later `z = Q`, a bad channel or an orphan `Sample` reports
|
|
341
|
+
# both) but end it past a placement stop, and hide the unclosed-block checks.
|
|
342
|
+
early.items = [d for d in diags.items[early_start:] if d.severity == "error"]
|
|
343
|
+
diags.items[early_start:] = [d for d in diags.items[early_start:] if d.severity != "error"]
|
|
344
|
+
|
|
345
|
+
bounds = Diagnostics()
|
|
346
|
+
pass2_start, flat = 0, program # (both set before pass 2 reports anything they bound)
|
|
347
|
+
fields = Diagnostics() # `SetSetting`/`SetStatus` field-name findings, see below
|
|
348
|
+
# CR6/CR1000X `X not allowed outside of Scan..NextScan in PipeLine Mode.` and `DO .. LOOP
|
|
349
|
+
# cannot span across sequences.`: pass 2 errors that do not hide tier C (oracle: printed
|
|
350
|
+
# together with the instruction's own `Invalid channel`/`Invalid port` and `Scan interval too
|
|
351
|
+
# short` errors), merged ahead of the tier-C findings below.
|
|
352
|
+
pipe = Diagnostics()
|
|
353
|
+
|
|
354
|
+
ranges = Diagnostics() # `measure.json` `const_ranges` findings, see below
|
|
355
|
+
leading = Diagnostics() # pass-2 findings that print ahead of their line's other findings
|
|
356
|
+
claims = Diagnostics() # terminal-ownership conflicts, see below
|
|
357
|
+
links = structure.CallLinks()
|
|
358
|
+
|
|
359
|
+
def before_cutoff(diagnostic) -> bool: # tier B (below) redefines it once a stop is known
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
held_items: list = []
|
|
363
|
+
if held_member:
|
|
364
|
+
# A table/status field with 3+ indices fails in the statement's compile (pass 2), not in
|
|
365
|
+
# pass 1: it neither stops pass 2 nor hides the placement error, but any pass-1 error hides
|
|
366
|
+
# it, and it is only reached up to the first placement stop (oracle, all four loggers,
|
|
367
|
+
# 2026-10-10; message tracer: pass-1 frames for `Sin(1,2)`, compile frames for these).
|
|
368
|
+
held_items = [d for d in diags.items if any(d is h for h in held_member)]
|
|
369
|
+
diags.items[:] = [d for d in diags.items if not any(d is h for h in held_member)]
|
|
370
|
+
if diags.errors:
|
|
371
|
+
held_items = []
|
|
372
|
+
_run_pass(storage.check_pass1, program, profile, diags, env) # CR6/CR1000X `Out of memory.`
|
|
373
|
+
_run_pass(instr_data.drop_unreached_destinations, program, profile, diags, env)
|
|
374
|
+
pass1_errors = bool(diags.errors)
|
|
375
|
+
result.pass1_errors = pass1_errors
|
|
376
|
+
pass1_items = [d for d in diags.items if d.severity == "error"]
|
|
377
|
+
if pass1_errors and not profile.single_pass:
|
|
378
|
+
# CR6/CR1000X: any pass-1 error hides the reachability warning (oracle, 2026-10-12)
|
|
379
|
+
diags.items[:] = [d for d in diags.items if not _is_unreached(d)]
|
|
380
|
+
generic_errors: list = [] # pass-2 argument/expression findings (CR6/CR1000X: no tier-C gate)
|
|
381
|
+
unknown_table_diags: list = [] # `Could not find DataTable T.`: ends the walk, gates nothing above it
|
|
382
|
+
if pass1_errors:
|
|
383
|
+
early.items = [] # hidden by any pass-1 error (oracle, CR6/CR1000X, 2026-10-03)
|
|
384
|
+
soft: list = [] # stage-2a findings that gate nothing (`instr_data.WIND_SUBINTERVAL`)
|
|
385
|
+
if not diags.errors:
|
|
386
|
+
# CR6/CR1000X instruction checks between pass 1 and pass 2's expression tier; an error
|
|
387
|
+
# here skips tier B (`instr_data.check_stage2a`).
|
|
388
|
+
_run_pass(instr_data.check_stage2a, program, profile, diags, env)
|
|
389
|
+
soft = [d for d in diags.items if d.message == instr_data.WIND_SUBINTERVAL]
|
|
390
|
+
diags.items[:] = [d for d in diags.items if d.message != instr_data.WIND_SUBINTERVAL]
|
|
391
|
+
if diags.errors:
|
|
392
|
+
diags.items[:] = [d for d in diags.items if not _is_unreached(d)] # (oracle, 2026-10-12)
|
|
393
|
+
# hidden by a stage-2a error too; only a negative `DataInterval` ahead of a table
|
|
394
|
+
# `Reps`/bounds error survives (round 52,
|
|
395
|
+
# docs/checker.md#datainterval-before-a-table-reps-error-round-52)
|
|
396
|
+
order = {(st.file, st.line): i for i, st in enumerate(program.statements)}
|
|
397
|
+
first = min(order.get((d.file, d.line), 0) for d in diags.errors)
|
|
398
|
+
early.items = [
|
|
399
|
+
d
|
|
400
|
+
for d in early.items
|
|
401
|
+
if d.message.startswith("DataInterval TintoInt") and order.get((d.file, d.line), 0) < first
|
|
402
|
+
]
|
|
403
|
+
if not diags.errors or profile.single_pass:
|
|
404
|
+
# Pass 2, tier B (see this module's docstring): the generic "Expr"/implicit-`Call`
|
|
405
|
+
# fallback (`structure.check_pass2`) and the undefined-symbol/constant-only-family checks
|
|
406
|
+
# on a *recognised* instruction's own arguments (`expressions.check`) freely accumulate
|
|
407
|
+
# with each other -- oracle-confirmed, `docs/checker.md` "Pass model": `Randomize(FooBar)`
|
|
408
|
+
# (an `expressions.check` finding) followed by a separate bare `qux quux` (a
|
|
409
|
+
# `structure.check_pass2` finding) reports *both*. `structure.check_pass2` also reports
|
|
410
|
+
# where (if anywhere) tier B should be considered cut off, matching the real compiler's own
|
|
411
|
+
# "stop entirely at the first placement error" behaviour.
|
|
412
|
+
pass2_start = len(diags.items)
|
|
413
|
+
_run_pass(instr_rules.check_spi_open_order, _with_inline(program), profile, leading)
|
|
414
|
+
# Block-closer messages come before a same-line placement error (`EndIf illegally
|
|
415
|
+
# occurs before BeginProg.` follows `END IF without IF...`).
|
|
416
|
+
# CR300/CR350: collected aside in `leading` (printed first on their line, but they do not
|
|
417
|
+
# count as errors for the placement stop; `cr3xx.drop_block_closer_errors_after_errors`).
|
|
418
|
+
_run_pass(structure.check_block_closers, program, profile, leading if profile.single_pass else diags)
|
|
419
|
+
# Tier B also checks the statements of a one-line `If` (oracle: `If X Then Y = 1` ->
|
|
420
|
+
# `Invalid, or out of place expression: Y = 1.`).
|
|
421
|
+
flat = _with_inline(program)
|
|
422
|
+
n_generic = len(diags.items)
|
|
423
|
+
stop_line = _run_pass_capture(structure.check_pass2, flat, profile, diags, env, links)
|
|
424
|
+
generic_errors += diags.items[n_generic:]
|
|
425
|
+
# Every stop is one expanded-source position (`links.positions`), the earliest wins; an
|
|
426
|
+
# included file has its own line numbers, so a physical line cannot order them.
|
|
427
|
+
# Like the `Scan` value errors, a nested `Scan` neither gates tier C nor survives past a
|
|
428
|
+
# placement stop (oracle, CR6/CR1000X, 2026-10-03: a later bad channel is still reported).
|
|
429
|
+
if not profile.single_pass:
|
|
430
|
+
early.items[:0] = [d for d in diags.items[pass2_start:] if d.message == structure.NESTED_SCAN]
|
|
431
|
+
diags.items[pass2_start:] = [d for d in diags.items[pass2_start:] if d.message != structure.NESTED_SCAN]
|
|
432
|
+
stops = [links.cutoff] if stop_line is not None and links.cutoff else []
|
|
433
|
+
unreached_off: set = set() # statements whose own failure replaces their reachability warning
|
|
434
|
+
stray_stop = _run_pass_capture(structure.check_stray_controls, program, profile, diags)
|
|
435
|
+
if stray_stop is not None:
|
|
436
|
+
stops.append(stray_stop)
|
|
437
|
+
_run_pass(structure.check_pipeline_task_placement, program, profile, pipe)
|
|
438
|
+
_run_pass(structure.check_slow_sequence_blocks, program, profile, pipe)
|
|
439
|
+
menu_stop = _run_pass_capture(instr_data.check_table_and_menu_placement, program, profile, diags, env)
|
|
440
|
+
if menu_stop is not None:
|
|
441
|
+
stops.append(menu_stop)
|
|
442
|
+
# An unknown `CallTable` target ends pass 2 there like a placement error (oracle, all four
|
|
443
|
+
# loggers: a later `NextScan with no Scan.`/`x = 1` is not reported).
|
|
444
|
+
# (CR300/CR350 order these differently, `check_scan_placement_late`: not modelled.)
|
|
445
|
+
if not profile.single_pass:
|
|
446
|
+
for stmt, _name in instr_data.unknown_calltables(program):
|
|
447
|
+
stops.append((stmt.file, stmt.line))
|
|
448
|
+
unreached_off.add((stmt.file, stmt.line))
|
|
449
|
+
for stmt, _message, ends in instr_data.unknown_table_references(program, profile):
|
|
450
|
+
if ends: # (`GetRecord`/`ResetTable`/`FileMark` of a missing table, round 14)
|
|
451
|
+
stops.append((stmt.file, stmt.line))
|
|
452
|
+
# An orphan `EndTable`/empty table ends pass 2 too (reported in tier C, `check_table_
|
|
453
|
+
# structure`), cutting a later placement error.
|
|
454
|
+
if not profile.single_pass:
|
|
455
|
+
early_table_stop = _run_pass_capture(
|
|
456
|
+
instr_data.check_table_structure, _with_inline(program), profile, Diagnostics()
|
|
457
|
+
)
|
|
458
|
+
if early_table_stop is not None:
|
|
459
|
+
stops.append(early_table_stop)
|
|
460
|
+
stop_at = min((links.positions[k] for k in stops if k in links.positions), default=None)
|
|
461
|
+
|
|
462
|
+
def before_cutoff(diagnostic) -> bool:
|
|
463
|
+
if stop_at is None:
|
|
464
|
+
return True
|
|
465
|
+
return links.positions.get((diagnostic.file, diagnostic.line), stop_at) <= stop_at
|
|
466
|
+
|
|
467
|
+
# The reachability warning is printed by the walk that stops at these (oracle, 2026-10-12)
|
|
468
|
+
kept = [
|
|
469
|
+
d
|
|
470
|
+
for d in diags.items[:pass2_start]
|
|
471
|
+
if not (_is_unreached(d) and (not before_cutoff(d) or (d.file, d.line) in unreached_off))
|
|
472
|
+
]
|
|
473
|
+
diags.items[:pass2_start] = kept
|
|
474
|
+
pass2_start = len(kept)
|
|
475
|
+
n_generic = len(diags.items)
|
|
476
|
+
_run_pass(expressions.check, flat, profile, diags, env, _placement_rejected(flat, profile, diags))
|
|
477
|
+
_run_pass(expressions.check_timedcontrol_constants, flat, profile, diags)
|
|
478
|
+
_run_pass(expressions.check_table_members, flat, profile, diags)
|
|
479
|
+
_run_pass(expressions.check_digit_members, flat, profile, diags)
|
|
480
|
+
_run_pass(expressions.check_undefined_dotted_bases, flat, profile, diags, pass2_start)
|
|
481
|
+
_run_pass(instr_rules.check_debug_args, flat, profile, diags, env)
|
|
482
|
+
_run_pass(instr_rules.check_serialbrk_comport, flat, profile, leading, env)
|
|
483
|
+
if not profile.single_pass:
|
|
484
|
+
# compiled after the included file was read: no `<file>` prefix (oracle, CR6/CR1000X,
|
|
485
|
+
# round 40, docs/checker.md#include-prefix-of-pass-2-symbols-round-40)
|
|
486
|
+
renamed = {
|
|
487
|
+
id(d): dataclasses.replace(d, show_file=False)
|
|
488
|
+
for d in diags.items[pass2_start:]
|
|
489
|
+
if d.message.startswith("Invalid symbol in expression")
|
|
490
|
+
}
|
|
491
|
+
diags.items[pass2_start:] = [renamed.get(id(d), d) for d in diags.items[pass2_start:]]
|
|
492
|
+
# (tier C gates on the identity of these: keep `generic_errors` pointing at them)
|
|
493
|
+
generic_errors[:] = [renamed.get(id(d), d) for d in generic_errors]
|
|
494
|
+
# a `FieldNames` argument is resolved by its owning output instruction (docs/checker.md
|
|
495
|
+
# "FieldNames owner line", round 54)
|
|
496
|
+
window = diags.items[pass2_start:]
|
|
497
|
+
moved, added = instr_data.move_fieldnames_symbol_errors(flat, window)
|
|
498
|
+
diags.items[pass2_start:] = window
|
|
499
|
+
generic_errors[:] = [m for g in generic_errors if (m := moved.get(id(g), g)) is not None] + added
|
|
500
|
+
# `PulseCount must be used within a Scan...NextScan.` follows its own statement's symbol
|
|
501
|
+
# error (oracle, CR6/CR1000X, round 40)
|
|
502
|
+
window = diags.items[pass2_start:]
|
|
503
|
+
for d in [d for d in window if d.message.endswith("must be used within a Scan...NextScan.")]:
|
|
504
|
+
after = [
|
|
505
|
+
i
|
|
506
|
+
for i, o in enumerate(window)
|
|
507
|
+
if (o.file, o.line) == (d.file, d.line) and o.message.startswith("Invalid symbol in expression")
|
|
508
|
+
]
|
|
509
|
+
if after and after[-1] > window.index(d):
|
|
510
|
+
window.remove(d)
|
|
511
|
+
window.insert(after[-1], d)
|
|
512
|
+
diags.items[pass2_start:] = window
|
|
513
|
+
generic_errors += diags.items[n_generic:]
|
|
514
|
+
_run_pass(instr_rules.check_mqtt_publish_payload, flat, profile, diags)
|
|
515
|
+
if not profile.single_pass:
|
|
516
|
+
_run_pass(instr_data.check_eventcount_tables, flat, profile, diags)
|
|
517
|
+
_run_pass(ess.check, flat, profile, diags, env)
|
|
518
|
+
# Collected aside: reported with pass 2 but not hiding the tier-C checks (oracle:
|
|
519
|
+
# `PortSet(1,A(9))` gives both `Invalid port 1 ...` and `Variable A out of bounds.`).
|
|
520
|
+
if not profile.single_pass:
|
|
521
|
+
_run_pass(expressions.check_instruction_index_bounds, flat, profile, bounds, env)
|
|
522
|
+
_run_pass(instr_rules.check_avgrun_options, flat, profile, bounds, env)
|
|
523
|
+
_run_pass(instr_data.check_output_datatype, flat, profile, bounds, env)
|
|
524
|
+
_run_pass(instr_data.check_odd_pair_reps, flat, profile, leading, env)
|
|
525
|
+
_run_pass(instr_rules.check_option_values, flat, profile, bounds, env)
|
|
526
|
+
_run_pass(instr_rules.check_serialinrecord, flat, profile, bounds, env)
|
|
527
|
+
_run_pass(expressions.check_splitstr_whole_array, flat, profile, bounds, env)
|
|
528
|
+
_run_pass(expressions.check_splitstr_element_reps, flat, profile, bounds, env)
|
|
529
|
+
_run_pass(expressions.check_sdmsio4_count, flat, profile, bounds, env)
|
|
530
|
+
_run_pass(expressions.check_calibrate_size, flat, profile, bounds, env)
|
|
531
|
+
_run_pass(expressions.check_subcall_element_bounds, flat, profile, bounds, env)
|
|
532
|
+
_run_pass(instr_rules.check_fieldcal_variable_args, flat, profile, bounds, env)
|
|
533
|
+
_run_pass(instr_rules.check_pulsecount_channels, flat, profile, bounds)
|
|
534
|
+
_run_pass(instr_rules.check_terminal_conflicts, flat, profile, claims, env)
|
|
535
|
+
_run_pass(instr_rules.check_sdi12_nan_fill, flat, profile, bounds, env)
|
|
536
|
+
_run_pass(instr_rules.check_dnp_update, flat, profile, leading, env)
|
|
537
|
+
_run_pass(instr_rules.check_portbridge_array, flat, profile, bounds, env)
|
|
538
|
+
_run_pass(instr_rules.check_cdm_address_types, flat, profile, bounds, env)
|
|
539
|
+
_run_pass(instr_rules.check_avw200_scan_time, flat, profile, bounds, env)
|
|
540
|
+
_run_pass(instr_rules.check_epi_scan_buffers, flat, profile, bounds, env)
|
|
541
|
+
bounds.items = [d for d in bounds.items if before_cutoff(d)]
|
|
542
|
+
pipe.items = [d for d in pipe.items if before_cutoff(d)]
|
|
543
|
+
leading.items = [d for d in leading.items if before_cutoff(d)]
|
|
544
|
+
claims.items = [d for d in claims.items if before_cutoff(d)]
|
|
545
|
+
_run_pass(instr_rules.check_sdm_address, program, profile, diags, env)
|
|
546
|
+
# Collected aside like the index bounds: a finding neither hides tier C (oracle: a bad
|
|
547
|
+
# `SemaphoreGet` beside a bad `VoltDiff` channel reports both) nor is hidden by tier B.
|
|
548
|
+
if not profile.single_pass:
|
|
549
|
+
_run_pass(instr_measure.check_const_ranges, flat.statements, profile, ranges, env)
|
|
550
|
+
ranges.items = [d for d in ranges.items if before_cutoff(d)]
|
|
551
|
+
# `Could not find DataTable %s.` (round 6, `checker/instr_data.py`'s
|
|
552
|
+
# `check_unclosed_calltable` docstring): a `CallTable` referencing a `DataTable` that was
|
|
553
|
+
# declared but never closed with `EndTable`. Also tier B, oracle-confirmed the same way as
|
|
554
|
+
# the other two calls just above (accumulates with an unrelated `expressions.check` finding
|
|
555
|
+
# rather than being suppressed by it).
|
|
556
|
+
n_calltable = len(diags.items)
|
|
557
|
+
_run_pass(instr_data.check_unclosed_calltable, program, diags, profile)
|
|
558
|
+
# The unknown-table error ends the walk there but does not hide the tier-C findings above
|
|
559
|
+
# it (oracle, all four loggers, 2026-10-13: `Sample` outside a table, then an unknown
|
|
560
|
+
# `CallTable`, prints both)
|
|
561
|
+
unknown_table_diags += [
|
|
562
|
+
d for d in diags.items[n_calltable:] if d.message.startswith("Could not find DataTable ")
|
|
563
|
+
]
|
|
564
|
+
generic_errors += unknown_table_diags
|
|
565
|
+
# `SetSetting`/`SetStatus` field names: collected aside like the index bounds, so a finding
|
|
566
|
+
# neither hides tier C nor is hidden by tier-B findings (CR6/CR1000X; CR300/CR350 run it
|
|
567
|
+
# below, only when nothing else was found). `docs/instructions.md`.
|
|
568
|
+
if not profile.single_pass:
|
|
569
|
+
_run_pass(instr_rules.check_settings_fields, flat, profile, diags, fields, env)
|
|
570
|
+
fields.items = [d for d in fields.items if before_cutoff(d)]
|
|
571
|
+
# CR300/CR350 only (a no-op on every other logger, `checker/cr3xx.py`): one leading-word
|
|
572
|
+
# `%s is not defined.` line per bare word of an `S.Unparsed` statement's own source text,
|
|
573
|
+
# spliced in immediately ahead of `structure.check_pass2`'s generic "Invalid, or out of
|
|
574
|
+
# place expression" fallback for it (`docs/loggers.md` "Round 8"). Reacts to `diags.items`
|
|
575
|
+
# already emitted above, the same after-the-fact pattern `check_too_many_cascade` below
|
|
576
|
+
# also uses.
|
|
577
|
+
_run_pass(cr3xx.check_unknown_line_words, flat, profile, diags)
|
|
578
|
+
_run_pass(cr3xx.check_for_header, flat, profile, diags) # (`For ... To 1 Step-1`)
|
|
579
|
+
# CR300/CR350 only (a no-op on every other logger, `checker/cr3xx.py`): one `%s is not
|
|
580
|
+
# defined.` line per undefined bare-name argument of an `S.Call` that itself resolved to
|
|
581
|
+
# nothing, spliced in between that call's own `{name} is not defined.` line (`structure.
|
|
582
|
+
# _check_call_pass2`) and the generic fallback for it (`docs/loggers.md` "CDM_* argument
|
|
583
|
+
# names"). Same after-the-fact `diags.items` pattern as `check_unknown_line_words` above.
|
|
584
|
+
_run_pass(cr3xx.check_undefined_call_args, flat, profile, diags)
|
|
585
|
+
_run_pass(cr3xx.check_float_destinations, flat, profile, diags)
|
|
586
|
+
_run_pass(cr3xx.check_undefined_arg_aborts, flat, profile, diags)
|
|
587
|
+
_run_pass(cr3xx.drop_too_many_after_variable_illegal, flat, profile, diags)
|
|
588
|
+
_run_pass(cr3xx.drop_calltable_error_with_other_errors, program, profile, diags)
|
|
589
|
+
# Every pass-2 error past the placement-error's own line is dropped, reproducing the
|
|
590
|
+
# real compiler's "stop entirely" behaviour without any of the modules above needing
|
|
591
|
+
# to coordinate the cutoff themselves (`docs/checker.md` "Pass model"). Warnings and
|
|
592
|
+
# anything already present before this pass (pass-1 errors -- none, since `not
|
|
593
|
+
# diags.errors` just gated this whole block, but defensive) are left untouched.
|
|
594
|
+
diags.items = diags.items[:pass2_start] + [
|
|
595
|
+
d for d in diags.items[pass2_start:] if d.severity != "error" or not d.line or before_cutoff(d)
|
|
596
|
+
]
|
|
597
|
+
over = [
|
|
598
|
+
d
|
|
599
|
+
for d in diags.items[pass2_start:]
|
|
600
|
+
if d.severity == "error" and d.message.startswith("Dimensions not declared")
|
|
601
|
+
]
|
|
602
|
+
if over and not profile.single_pass:
|
|
603
|
+
# CR6/CR1000X: more than three subscripts is a pass-1 error: it hides the pass-2
|
|
604
|
+
# symbol errors, the asides and tier C (oracle, round 40,
|
|
605
|
+
# docs/checker.md#over-subscripted-arrays-are-pass-1-round-40)
|
|
606
|
+
diags.items = diags.items[:pass2_start] + [
|
|
607
|
+
d for d in diags.items[pass2_start:] if not d.message.startswith("Invalid symbol in expression")
|
|
608
|
+
]
|
|
609
|
+
generic_errors = [g for g in generic_errors if not any(g is d for d in over)]
|
|
610
|
+
for aside in (bounds, fields, ranges, leading, pipe, claims, early):
|
|
611
|
+
aside.items = []
|
|
612
|
+
|
|
613
|
+
if profile.single_pass:
|
|
614
|
+
_run_pass(structure.commit_deferred_sub_error, links, profile, diags)
|
|
615
|
+
|
|
616
|
+
if profile.single_pass and not diags.errors:
|
|
617
|
+
# CR300/CR350 reject an executable `DataTable` before the table-field checks that would
|
|
618
|
+
# otherwise diagnose its body (`structure.check_data_table_placement`).
|
|
619
|
+
_run_pass(structure.check_data_table_placement, program, profile, diags)
|
|
620
|
+
|
|
621
|
+
late_start = len(diags.items)
|
|
622
|
+
late_stop = None
|
|
623
|
+
if profile.single_pass and all(
|
|
624
|
+
d.message.startswith(("Could not find ", "Cannot find DataTable ")) for d in diags.errors
|
|
625
|
+
):
|
|
626
|
+
# CR300/CR350: runs unless an earlier finding other than an unknown `CallTable` table
|
|
627
|
+
# exists (`docs/instructions.md`, SetSetting and SetStatus field names).
|
|
628
|
+
_run_pass(instr_rules.check_settings_fields, _with_inline(program), profile, diags, fields, env)
|
|
629
|
+
_run_pass(instr_rules.check_option_values, _with_inline(program), profile, bounds, env)
|
|
630
|
+
if not diags.errors:
|
|
631
|
+
# After passes 1 and 2 are clean: CR300/CR350 table `Reps`/bounds, then unclosed
|
|
632
|
+
# `Scan`/`For`/`Do`/`If` at end of program.
|
|
633
|
+
if profile.single_pass:
|
|
634
|
+
_run_pass(expressions.check_instruction_index_bounds, program, profile, bounds, env)
|
|
635
|
+
_run_pass(instr_data.check_output_datatype, program, profile, bounds, env)
|
|
636
|
+
_run_pass(expressions.check_status_member_reads_late, program, profile, bounds)
|
|
637
|
+
_run_pass(expressions.check_splitstr_whole_array, program, profile, bounds, env)
|
|
638
|
+
_run_pass(expressions.check_splitstr_element_reps, program, profile, bounds, env)
|
|
639
|
+
_run_pass(expressions.check_calibrate_size, program, profile, bounds, env)
|
|
640
|
+
_run_pass(expressions.check_subcall_element_bounds, program, profile, bounds, env)
|
|
641
|
+
_run_pass(instr_rules.check_fieldcal_variable_args, program, profile, bounds, env)
|
|
642
|
+
_run_pass(instr_rules.check_matrix_destination, program, profile, bounds)
|
|
643
|
+
_run_pass(instr_rules.check_avgrun_options, program, profile, bounds, env)
|
|
644
|
+
_run_pass(instr_data.check_eventcount_tables, program, profile, bounds)
|
|
645
|
+
_run_pass(instr_data.check_table_reps_late, program, profile, diags, env)
|
|
646
|
+
if profile.single_pass:
|
|
647
|
+
_run_pass(instr_rules.check_dnp_update, _with_inline(program), profile, diags, env)
|
|
648
|
+
_run_pass(instr_rules.check_portbridge_array, _with_inline(program), profile, diags, env)
|
|
649
|
+
if (
|
|
650
|
+
not diags.errors
|
|
651
|
+
or (
|
|
652
|
+
profile.single_pass
|
|
653
|
+
and all(d.message.startswith(("Could not find ", "Cannot find DataTable ")) for d in diags.errors)
|
|
654
|
+
)
|
|
655
|
+
) and profile.early_warnings_gated:
|
|
656
|
+
# CR300/CR350: the early warnings follow the table `Reps` checks and precede `CallTable`
|
|
657
|
+
# placement and the unclosed-block checks (oracle: `Illegal Reps 0` hides an fnotch
|
|
658
|
+
# warning; `DO without LOOP.` and a `CallTable` placement error do not).
|
|
659
|
+
_run_pass(warnings.check_early, program, profile, diags, env)
|
|
660
|
+
n_late = len(diags.items)
|
|
661
|
+
stop = _run_pass_capture(structure.check_scan_placement_late, program, profile, diags)
|
|
662
|
+
# a `GetRecord`/`ResetTable`/`FileMark` of a missing table ends the walk (oracle, round 14)
|
|
663
|
+
ref_lines = [st.line for st, _m, ends in instr_data.unknown_table_references(program, profile) if ends]
|
|
664
|
+
if profile.single_pass:
|
|
665
|
+
ref_lines += [st.line for st, _name in instr_data.unknown_calltables(program)]
|
|
666
|
+
if ref_lines:
|
|
667
|
+
diags.items[n_late:] = [
|
|
668
|
+
d for d in diags.items[n_late:] if d.severity != "error" or d.line <= min(ref_lines)
|
|
669
|
+
]
|
|
670
|
+
if profile.single_pass:
|
|
671
|
+
# the dead statement that fails here prints no reachability warning, nor do later
|
|
672
|
+
# ones (oracle, CR300/CR350, 2026-10-12)
|
|
673
|
+
diags.items[:] = [d for d in diags.items if not (_is_unreached(d) and d.line >= min(ref_lines))]
|
|
674
|
+
if profile.single_pass and stop is not None and ref_lines and stop[1] < min(ref_lines):
|
|
675
|
+
# the earlier of the two ends the walk: a stray `NextScan` hides a later unknown table
|
|
676
|
+
drop = [
|
|
677
|
+
i
|
|
678
|
+
for i, d in enumerate(diags.items)
|
|
679
|
+
if (
|
|
680
|
+
d.severity == "error"
|
|
681
|
+
and d.message.startswith("Could not find ")
|
|
682
|
+
and d.file == stop[0]
|
|
683
|
+
and d.line > stop[1]
|
|
684
|
+
)
|
|
685
|
+
]
|
|
686
|
+
late_start -= sum(1 for i in drop if i < late_start)
|
|
687
|
+
diags.items[:] = [d for i, d in enumerate(diags.items) if i not in set(drop)]
|
|
688
|
+
late_stop = stop
|
|
689
|
+
if stop is not None:
|
|
690
|
+
# The walk ends there, so later warnings are never printed (oracle: `SetSetting`
|
|
691
|
+
# before `BeginProg` hides an fnotch warning further down).
|
|
692
|
+
diags.items = [d for d in diags.items if d.severity != "warning" or d.file != stop[0] or d.line <= stop[1]]
|
|
693
|
+
if profile.single_pass:
|
|
694
|
+
held_items = [d for d in held_items if late_stop is None or d.line <= late_stop[1]]
|
|
695
|
+
if any(not d.message.endswith("illegally occurs before BeginProg.") for d in diags.errors):
|
|
696
|
+
held_items = []
|
|
697
|
+
else:
|
|
698
|
+
held_items = [d for d in held_items if before_cutoff(d)]
|
|
699
|
+
# (the failed statement's own undefined-symbol findings are not reported: the index error
|
|
700
|
+
# aborts its compile -- `LoggerID=Status.StationName(1,1,Nope)`, oracle, cr6)
|
|
701
|
+
own = {(d.file, d.line) for d in held_items}
|
|
702
|
+
own_stmts = {d.stmt for d in held_items if d.stmt} # tagged: exact statement identity
|
|
703
|
+
diags.items[:] = [
|
|
704
|
+
d
|
|
705
|
+
for d in diags.items
|
|
706
|
+
if not (
|
|
707
|
+
((d.stmt in own_stmts) if d.stmt else (d.file, d.line) in own)
|
|
708
|
+
and d.severity == "error"
|
|
709
|
+
and (d.message.startswith("Invalid symbol in expression") or d.message.endswith("is not defined."))
|
|
710
|
+
)
|
|
711
|
+
]
|
|
712
|
+
diags.items.extend(held_items)
|
|
713
|
+
generic_errors += held_items
|
|
714
|
+
# CR300/CR350: these late errors do not start the `Too many parameters` cascade (oracle:
|
|
715
|
+
# `Sample (1,Nope,FP2)` gives only `Nope is not defined.`).
|
|
716
|
+
diags.items[late_start:] = [dataclasses.replace(d, cascade=False) for d in diags.items[late_start:]]
|
|
717
|
+
tier_c_start = len(diags.items)
|
|
718
|
+
table_stop = None
|
|
719
|
+
gate = [
|
|
720
|
+
d
|
|
721
|
+
for d in diags.errors
|
|
722
|
+
if not (
|
|
723
|
+
any(d is g for g in unknown_table_diags)
|
|
724
|
+
or not profile.single_pass
|
|
725
|
+
and (any(d is g for g in generic_errors) or instr_data.is_table_name_diag(d))
|
|
726
|
+
)
|
|
727
|
+
]
|
|
728
|
+
if not gate:
|
|
729
|
+
# Pass 2, tier C: the real compiler never reaches an instruction's own channel/range/
|
|
730
|
+
# table/value validation once *either* tier A or tier B recorded any error anywhere in the
|
|
731
|
+
# file -- oracle-confirmed, `docs/checker.md` "Two-pass compilation"/"Pass model":
|
|
732
|
+
# `ExciteV(Vx1,...)` (an `expressions.check` finding, tier B) on an earlier line suppresses
|
|
733
|
+
# a later, otherwise-invalid `TCDiff` channel argument entirely -- no accumulation across
|
|
734
|
+
# this boundary, unlike within tier B itself above.
|
|
735
|
+
_run_pass(instr_measure.check_reps, program, profile, diags, env)
|
|
736
|
+
_run_pass(instr_measure.check_dest_bounds, program, profile, diags, env)
|
|
737
|
+
_run_pass(var_types.check, program, profile, diags, env)
|
|
738
|
+
|
|
739
|
+
if profile.single_pass:
|
|
740
|
+
_run_pass(instr_measure.check_const_ranges, _with_inline(program).statements, profile, diags, env)
|
|
741
|
+
_run_pass(instructions.check, program, profile, diags, env)
|
|
742
|
+
# Also pass 2 (oracle-confirmed the same way, `docs/instructions.md`): output-processing
|
|
743
|
+
# instructions placed outside `DataTable...EndTable`, and an orphan `EndTable`.
|
|
744
|
+
table_stop = _run_pass_capture(instr_data.check_table_structure, _with_inline(program), profile, diags)
|
|
745
|
+
_run_pass(instr_data.check_fieldnames, program, profile, diags, env)
|
|
746
|
+
apply_diags = Diagnostics()
|
|
747
|
+
_run_pass(structure.check_apply_restart_sequence, program, profile, apply_diags)
|
|
748
|
+
for extra in apply_diags.items: # in source order among the tier-C findings
|
|
749
|
+
at = next(
|
|
750
|
+
(
|
|
751
|
+
i
|
|
752
|
+
for i in range(tier_c_start, len(diags.items))
|
|
753
|
+
if diags.items[i].severity == "error" and diags.items[i].line > extra.line
|
|
754
|
+
),
|
|
755
|
+
len(diags.items),
|
|
756
|
+
)
|
|
757
|
+
diags.items.insert(at, extra)
|
|
758
|
+
_run_pass(instr_rules.check_samplefieldcal, _with_inline(program), profile, diags)
|
|
759
|
+
# Also pass 2 (oracle-confirmed the same way, `docs/instructions.md`): `Range`/`SW12`/
|
|
760
|
+
# `Delay`/`CS616`/`TimerInput`/`ExciteV`/`ExciteI` value-set and value-range checks.
|
|
761
|
+
_run_pass(instr_measure.check, program, profile, diags, env)
|
|
762
|
+
_run_pass(instr_measure.check_scan_delay_budget, program, profile, diags, env)
|
|
763
|
+
oom = next((d for d in diags.items[tier_c_start:] if d.message == "Out of memory."), None)
|
|
764
|
+
if oom is not None:
|
|
765
|
+
# A failed builder allocation ends the compile there (CSAT3, round 31): nothing from a
|
|
766
|
+
# later statement is printed.
|
|
767
|
+
at = links.positions.get((oom.file, oom.line), oom.line)
|
|
768
|
+
tail = diags.items[tier_c_start:]
|
|
769
|
+
oom_index = next(i for i, d in enumerate(tail) if d is oom)
|
|
770
|
+
diags.items[tier_c_start:] = [
|
|
771
|
+
d
|
|
772
|
+
for i, d in enumerate(tail)
|
|
773
|
+
if d.severity != "error"
|
|
774
|
+
or not d.line
|
|
775
|
+
or links.positions.get((d.file, d.line), d.line) < at
|
|
776
|
+
or (d.line == oom.line and i <= oom_index)
|
|
777
|
+
]
|
|
778
|
+
ranges.items = [d for d in ranges.items if links.positions.get((d.file, d.line), d.line) < at]
|
|
779
|
+
bounds.items = [
|
|
780
|
+
d
|
|
781
|
+
for d in bounds.items # (a later index too, round 31 review)
|
|
782
|
+
if links.positions.get((d.file, d.line), d.line) < at
|
|
783
|
+
]
|
|
784
|
+
diags.items[:tier_c_start] = [ # (and a later early warning: `Scan never reached`)
|
|
785
|
+
d
|
|
786
|
+
for d in diags.items[:tier_c_start]
|
|
787
|
+
if d.severity != "warning" or not d.line or links.positions.get((d.file, d.line), d.line) < at
|
|
788
|
+
]
|
|
789
|
+
if generic_errors:
|
|
790
|
+
diags.items[tier_c_start:] = [
|
|
791
|
+
d for d in diags.items[tier_c_start:] if d.severity != "error" or not d.line or before_cutoff(d)
|
|
792
|
+
]
|
|
793
|
+
if profile.single_pass and unknown_table_diags:
|
|
794
|
+
# (CR300/CR350: the unknown table ends the walk there; tier C stops with it)
|
|
795
|
+
end_pos = min(links.positions.get((d.file, d.line), d.line) for d in unknown_table_diags)
|
|
796
|
+
diags.items[tier_c_start:] = [
|
|
797
|
+
d
|
|
798
|
+
for d in diags.items[tier_c_start:]
|
|
799
|
+
if d.severity != "error" or not d.line or links.positions.get((d.file, d.line), d.line) <= end_pos
|
|
800
|
+
]
|
|
801
|
+
if generic_errors and len(diags.items) > tier_c_start:
|
|
802
|
+
# Tier-C findings print in source order among the generic pass-2 ones (oracle, CR6/
|
|
803
|
+
# CR1000X, 2026-10-04: a bad channel on an earlier line precedes a later `foo bar`).
|
|
804
|
+
tail = diags.items[pass2_start:]
|
|
805
|
+
slots = [i for i, d in enumerate(tail) if d.severity == "error" and d.line]
|
|
806
|
+
ordered = sorted((tail[i] for i in slots), key=lambda d: links.positions.get((d.file, d.line), d.line))
|
|
807
|
+
for i, d in zip(slots, ordered):
|
|
808
|
+
tail[i] = d
|
|
809
|
+
diags.items[pass2_start:] = tail
|
|
810
|
+
if table_stop is not None and table_stop in links.positions:
|
|
811
|
+
late_pos = links.positions[table_stop]
|
|
812
|
+
diags.items[tier_c_start:] = [
|
|
813
|
+
d
|
|
814
|
+
for d in diags.items[tier_c_start:]
|
|
815
|
+
if d.severity != "error" or links.positions.get((d.file, d.line), late_pos) <= late_pos
|
|
816
|
+
]
|
|
817
|
+
if profile.single_pass and not diags.errors:
|
|
818
|
+
# CR300/CR350: structure member/assignment errors; they do not hide the early warnings
|
|
819
|
+
# (oracle: `Invalid fnotch value` still printed) and are sorted in by line.
|
|
820
|
+
flat_program = _with_inline(program)
|
|
821
|
+
_run_pass(structs.check_members, program, profile, diags, flat_program)
|
|
822
|
+
_run_pass(structs.check_struct_assignment, program, profile, diags, flat_program)
|
|
823
|
+
if not diags.errors and not fields.items and not pipe.items and not early.items:
|
|
824
|
+
# After tier C, not before (oracle, all four loggers: an unclosed `Scan`/`If`/`For` or a
|
|
825
|
+
# missing `BeginProg` plus a tier-C finding reports only the tier-C finding).
|
|
826
|
+
_run_pass(structure.check_end_blocks, program, diags)
|
|
827
|
+
if (
|
|
828
|
+
not diags.errors
|
|
829
|
+
and not early.items
|
|
830
|
+
and not pipe.items
|
|
831
|
+
and not fields.items
|
|
832
|
+
and not bounds.items
|
|
833
|
+
and not ranges.items
|
|
834
|
+
and not leading.items
|
|
835
|
+
):
|
|
836
|
+
# last: any other error, a missing `BeginProg` or an unclosed block included, hides it
|
|
837
|
+
# (oracle, CR6/CR1000X, 2026-10-03 and round 39)
|
|
838
|
+
_run_pass(instr_data.check_units_targets, program, profile, diags)
|
|
839
|
+
argument_error_first = structure.pipeline_argument_error_first_locations(program, profile)
|
|
840
|
+
ordinary_pipe = []
|
|
841
|
+
delayed_pipe = []
|
|
842
|
+
for diagnostic in pipe.items:
|
|
843
|
+
location = (diagnostic.file, diagnostic.line)
|
|
844
|
+
if location in argument_error_first and any(
|
|
845
|
+
item.severity == "error" and (item.file, item.line) == location for item in diags.items[tier_c_start:]
|
|
846
|
+
):
|
|
847
|
+
delayed_pipe.append(diagnostic)
|
|
848
|
+
else:
|
|
849
|
+
ordinary_pipe.append(diagnostic)
|
|
850
|
+
diags.items[tier_c_start:tier_c_start] = ordinary_pipe
|
|
851
|
+
for diagnostic in delayed_pipe:
|
|
852
|
+
location = (diagnostic.file, diagnostic.line)
|
|
853
|
+
index = (
|
|
854
|
+
min(
|
|
855
|
+
i
|
|
856
|
+
for i, item in enumerate(diags.items)
|
|
857
|
+
if item.severity == "error" and (item.file, item.line) == location
|
|
858
|
+
)
|
|
859
|
+
+ 1
|
|
860
|
+
)
|
|
861
|
+
diags.items.insert(index, diagnostic)
|
|
862
|
+
|
|
863
|
+
if early.items:
|
|
864
|
+
early.items = [d for d in early.items if before_cutoff(d)]
|
|
865
|
+
if table_stop is not None and table_stop in links.positions:
|
|
866
|
+
early.items = [
|
|
867
|
+
d for d in early.items if links.positions.get((d.file, d.line), 0) <= links.positions[table_stop]
|
|
868
|
+
]
|
|
869
|
+
if bounds.items:
|
|
870
|
+
_place_index_bounds(bounds, diags, flat, profile.single_pass)
|
|
871
|
+
diags.items += bounds.items + fields.items + ranges.items + early.items
|
|
872
|
+
for d in reversed(soft): # before the errors of its own and later lines (oracle, round 58)
|
|
873
|
+
here = (links.positions.get((d.file, d.line), d.line), d.line)
|
|
874
|
+
at = next(
|
|
875
|
+
(
|
|
876
|
+
i
|
|
877
|
+
for i, o in enumerate(diags.items)
|
|
878
|
+
if o.severity == "error" and (links.positions.get((o.file, o.line), o.line), o.line) >= here
|
|
879
|
+
),
|
|
880
|
+
len(diags.items),
|
|
881
|
+
)
|
|
882
|
+
diags.items.insert(at, d)
|
|
883
|
+
|
|
884
|
+
# Terminal conflicts hide nothing and print first on their line. An SDI-12 one is reported only
|
|
885
|
+
# when no error, another conflict included, precedes it (oracle, CR6/CR1000X, round 37).
|
|
886
|
+
def pos(d):
|
|
887
|
+
return links.positions.get((d.file, d.line), d.line)
|
|
888
|
+
|
|
889
|
+
others = [pos(d) for d in diags.items + leading.items + claims.items if d.severity == "error" and d.line]
|
|
890
|
+
leading.items += [
|
|
891
|
+
d for d in claims.items if not d.message.startswith("SDI12") or not any(p < pos(d) for p in others)
|
|
892
|
+
]
|
|
893
|
+
last: dict[tuple, Diagnostic] = {} # (file, line) -> its latest leading finding
|
|
894
|
+
for d in leading.items:
|
|
895
|
+
# Aside like `bounds` (tier C still runs), but printed first on its own line, in their own
|
|
896
|
+
# order (oracle: `SPIOpen must precede SPIWrite.` precedes `SPIWrite illegally occurs
|
|
897
|
+
# before BeginProg.`).
|
|
898
|
+
key = (d.file, d.line)
|
|
899
|
+
if key in last:
|
|
900
|
+
at = next(i for i, o in enumerate(diags.items) if o is last[key]) + 1
|
|
901
|
+
else:
|
|
902
|
+
at = next((i for i, o in enumerate(diags.items) if (o.file, o.line) == key), len(diags.items))
|
|
903
|
+
diags.items.insert(at, d)
|
|
904
|
+
last[key] = d
|
|
905
|
+
# `Call`s naming no declared `Sub` (`structure.finalize_call_links`): a link-step message
|
|
906
|
+
# after every other error, so it does not gate the tier-C checks above.
|
|
907
|
+
_run_pass(structure.finalize_call_links, links, profile, diags, pass1_errors)
|
|
908
|
+
|
|
909
|
+
# CR300/CR350 only (a no-op on every other logger, `checker/cr3xx.py`): once any undefined-
|
|
910
|
+
# name-class or genuine-`Too many parameters:` finding has fired anywhere above -- tier A
|
|
911
|
+
# (ungated: `cr3xx.check`'s own `SerialOpen`/`ComPort` finding, `instr_rules.check_too_many`'s
|
|
912
|
+
# own genuine finding), tier B, or tier C -- every following two-plus-argument recognised
|
|
913
|
+
# instruction, to end of file, gets its own spurious `Too many parameters: %s.` in place of
|
|
914
|
+
# whatever it would otherwise have found (`checker/cr3xx.py`'s own module-level comment for the
|
|
915
|
+
# oracle evidence). Run unconditionally, after every other diagnostic-producing pass, since the
|
|
916
|
+
# taint can originate from a tier-A finding that would otherwise skip tier B/C entirely.
|
|
917
|
+
_run_pass(cr3xx.drop_block_closer_errors_after_errors, program, profile, diags)
|
|
918
|
+
_run_pass(cr3xx.order_names_around_arity, program, profile, diags)
|
|
919
|
+
_run_pass(cr3xx.order_names_before_call_arity, program, profile, diags)
|
|
920
|
+
_run_pass(cr3xx.order_email_destination, program, profile, diags)
|
|
921
|
+
_run_pass(cr3xx.drop_shortfall_with_names, program, profile, diags)
|
|
922
|
+
if profile.single_pass:
|
|
923
|
+
_run_pass(instr_data.finalize_illegal_table_names, program, profile, diags)
|
|
924
|
+
_run_pass(cr3xx.check_tcpopen_handle_early, program, profile, diags)
|
|
925
|
+
_run_pass(cr3xx.check_too_many_cascade, _with_inline(program), profile, diags)
|
|
926
|
+
_run_pass(cr3xx.drop_compound_target_repeat, program, profile, diags)
|
|
927
|
+
_run_pass(cr3xx.order_block_keyword_calls, program, profile, diags)
|
|
928
|
+
_run_pass(cr3xx.fix_then_operand, program, profile, diags)
|
|
929
|
+
_run_pass(cr3xx.order_invalid_assignments, program, profile, diags)
|
|
930
|
+
_run_pass(cr3xx.order_target_name_first, program, profile, diags)
|
|
931
|
+
_run_pass(cr3xx.order_unterminated_string, program, profile, diags)
|
|
932
|
+
_run_pass(cr3xx.check_tcpopen_handle, program, profile, diags)
|
|
933
|
+
_run_pass(cr3xx.add_string_illegal, program, profile, diags)
|
|
934
|
+
_run_pass(warnings.add_truncate_warnings, _with_inline(program), profile, diags)
|
|
935
|
+
_run_pass(cr3xx.semicolon_headers, program, profile, diags)
|
|
936
|
+
_run_pass(intervals.fold_timeout_expressions, _with_inline(program), profile, diags, env, pass1_items)
|
|
937
|
+
_run_pass(intervals.order_timeout_negative, diags, profile, program)
|
|
938
|
+
_run_pass(cr3xx.drop_after_alias_collision, program, profile, diags)
|
|
939
|
+
|
|
940
|
+
def is_constant(expr: S.Expr | None) -> bool:
|
|
941
|
+
return consteval.is_constant(expr, env)
|
|
942
|
+
|
|
943
|
+
def value_of(expr: S.Expr | None):
|
|
944
|
+
return consteval.value_of(expr, env)
|
|
945
|
+
|
|
946
|
+
try:
|
|
947
|
+
result.mode = mode.decide(program.statements, profile, is_constant, value_of)
|
|
948
|
+
except Exception as exc:
|
|
949
|
+
_record_exception(mode.decide, exc)
|
|
950
|
+
result.mode = None
|
|
951
|
+
if os.environ.get("OPENCRBASIC_STRICT") == "1":
|
|
952
|
+
raise
|
|
953
|
+
|
|
954
|
+
if not diags.errors:
|
|
955
|
+
# `When Main Sequence Scan %d is running, background calibration will be disabled.`:
|
|
956
|
+
# round 6, moved here from the unconditional `warnings.check_early` call above -- oracle-
|
|
957
|
+
# confirmed *not* ungated the way `_check_reachability`/`_check_fnotch` are, unlike this
|
|
958
|
+
# module's own docstring previously claimed for all of `check_early` (`checker/warnings.py`'s
|
|
959
|
+
# `check_background_calibration` docstring has the oracle evidence). Independent of
|
|
960
|
+
# `tables_hook`'s own availability, so called ahead of that guard rather than inside it.
|
|
961
|
+
_run_pass(warnings.check_background_calibration, program, profile, diags, env)
|
|
962
|
+
_run_pass(warnings.check_slow_scan_skips, program, profile, diags, env)
|
|
963
|
+
_run_pass(instr_measure.check_cpi_time, program, profile, diags, env)
|
|
964
|
+
try:
|
|
965
|
+
from .. import tables_hook
|
|
966
|
+
except ImportError:
|
|
967
|
+
pass
|
|
968
|
+
else:
|
|
969
|
+
_run_pass(tables_hook.attach, program, profile, result)
|
|
970
|
+
# `Duplicate field name %s in DataTable %s.` -- needs `result.tables`, just attached
|
|
971
|
+
# above (`checker/instr_data.py`, `docs/checker.md` "Duplicate field names"). Its
|
|
972
|
+
# errors suppress the "declared but never called" warning.
|
|
973
|
+
_run_pass(instr_data.check_duplicate_fields, program, result, diags)
|
|
974
|
+
if not diags.errors:
|
|
975
|
+
_run_pass(storage.check, program, profile, result, diags)
|
|
976
|
+
if not diags.errors:
|
|
977
|
+
_run_pass(warnings.check, program, result, diags)
|