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,661 @@
|
|
|
1
|
+
"""Warnings: post-table "declared but never called" (needs the whole post-`#If`-resolution
|
|
2
|
+
statement list, `tables_hook`-gated, pass 2) plus a small set of *ungated* warnings that the real
|
|
3
|
+
compiler emits regardless of whether the file has other errors (oracle-confirmed per warning
|
|
4
|
+
below, and generally in `docs/instructions.md`'s "Two of these run even on a failed compile"
|
|
5
|
+
section) -- reachability ("never reached at run time"), the CR6/CR1000X fast-scan
|
|
6
|
+
background-calibration note, and the CR300/CR350 fixed-set `fnotch` value check. Rules and oracle
|
|
7
|
+
evidence: `docs/instructions.md`; the pre-existing table-never-called rule's own evidence stays in
|
|
8
|
+
`docs/checker.md`/`docs/tdf-rules.md` section 13.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from .. import consteval
|
|
14
|
+
from .. import syntax as S
|
|
15
|
+
from ..diagnostics import Diagnostic, Diagnostics
|
|
16
|
+
from ..profiles import Profile
|
|
17
|
+
from . import instr_rules, structure, symbols
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _table_arg_name(stmt: S.Instruction) -> str | None:
|
|
21
|
+
if stmt.args and isinstance(stmt.args[0], S.Name):
|
|
22
|
+
return stmt.args[0].name
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _with_inline_branches(statements):
|
|
27
|
+
"""The statements plus those in every single-line `If ... Then stmt [Else stmt]` branch, at any
|
|
28
|
+
depth. A `CallTable` there counts as a use even when the branch is never taken (oracle,
|
|
29
|
+
CR6/CR1000X, 2026-10-11: `If Batt > 1 Then CallTable (Msgs)`, `If False Then CallTable (Msgs)`,
|
|
30
|
+
`If Batt > 1 Then Battery (Batt) Else CallTable (Msgs)`, `If ... Then CallTable (Msgs) : Battery`
|
|
31
|
+
-- `emailrecv_1.CR6` has its only call this way, and no `Table ... never called` warning)."""
|
|
32
|
+
for stmt in statements:
|
|
33
|
+
yield stmt
|
|
34
|
+
if isinstance(stmt, S.If):
|
|
35
|
+
yield from _with_inline_branches(stmt.inline or ())
|
|
36
|
+
yield from _with_inline_branches(stmt.inline_else or ())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check(program: S.Program, result, diags: Diagnostics) -> None:
|
|
40
|
+
# User `DataTable(...)...EndTable` blocks only -- deliberately *not* `result.tables` (which
|
|
41
|
+
# also carries the logger's own system tables, `Status`/`DataTableInfo`/`GlobalConfigTable`/
|
|
42
|
+
# the `Public` table, none of which are ever `CallTable`d and must not get this warning).
|
|
43
|
+
declared: dict[str, str] = {}
|
|
44
|
+
for stmt in program.statements:
|
|
45
|
+
if isinstance(stmt, S.Instruction) and stmt.keyword.upper() == "DATATABLE":
|
|
46
|
+
name = _table_arg_name(stmt)
|
|
47
|
+
if name:
|
|
48
|
+
from .instr_data import _const_table_name
|
|
49
|
+
|
|
50
|
+
name = _const_table_name(name, program)
|
|
51
|
+
declared[name.upper()] = name
|
|
52
|
+
|
|
53
|
+
# "Called" is deliberately not just `CallTable`: `AcceptDataRecords`' own TableName argument
|
|
54
|
+
# also counts (oracle-confirmed regression, `acceptdatarecords_1.CR6`: `AcceptDataRecords
|
|
55
|
+
# (3000,32769,CR1000XTable)` with no `CallTable` at all still gets no warning). Rather than
|
|
56
|
+
# enumerate every instruction with a table-name argument, any bare-name argument anywhere
|
|
57
|
+
# that matches a declared table counts as a use -- a table name is not reused as an ordinary
|
|
58
|
+
# variable, so this is a safe superset of the real "called" set.
|
|
59
|
+
from .instr_data import _const_table_name
|
|
60
|
+
|
|
61
|
+
called = {
|
|
62
|
+
name
|
|
63
|
+
for stmt in _with_inline_branches(program.statements)
|
|
64
|
+
if isinstance(stmt, S.Instruction) and stmt.keyword.upper() != "DATATABLE"
|
|
65
|
+
for arg in stmt.args
|
|
66
|
+
if isinstance(arg, S.Name)
|
|
67
|
+
# (a String `Const` names the table its value spells, round 67)
|
|
68
|
+
for name in (arg.name.upper(), _const_table_name(arg.name, program).upper())
|
|
69
|
+
if name in declared
|
|
70
|
+
}
|
|
71
|
+
for key, name in declared.items():
|
|
72
|
+
if key not in called:
|
|
73
|
+
# oracle-confirmed (docs/tdf-rules.md #13, `fixeduncalled`): fires whenever no
|
|
74
|
+
# `CallTable`/`CallTable(...)` for this table survives `#If` resolution, regardless
|
|
75
|
+
# of `DataInterval`/fixed size; `Main.CR6`'s `Shunt_Data` is the repo's own example
|
|
76
|
+
# (its only `CallTable` is inside a false-by-default `#If`).
|
|
77
|
+
diags.warning(0, f"Table {name}, is declared but never called.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- Ungated warnings (checker/__init__.py calls `check_early` regardless of pass-1 errors; see
|
|
81
|
+
# module docstring and docs/instructions.md for the oracle evidence that these fire even on an
|
|
82
|
+
# otherwise-failed compile) -------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_uint1_dims(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
86
|
+
"""CR6/CR1000X: a UINT1 variable whose element count is not a multiple of 4 is padded, one
|
|
87
|
+
warning per variable on its declaration line, also in a failed compile (oracle, round 56)."""
|
|
88
|
+
if profile.name not in ("cr6", "cr1000x"):
|
|
89
|
+
return
|
|
90
|
+
for stmt in program.statements:
|
|
91
|
+
if not isinstance(stmt, S.Declaration):
|
|
92
|
+
continue
|
|
93
|
+
for v in stmt.vars:
|
|
94
|
+
if v.type != "UINT1":
|
|
95
|
+
continue
|
|
96
|
+
count = 1
|
|
97
|
+
for d in v.dims:
|
|
98
|
+
try:
|
|
99
|
+
size = consteval.value_of(d, env)
|
|
100
|
+
except consteval.NotConstant:
|
|
101
|
+
break
|
|
102
|
+
if not isinstance(size, int) or isinstance(size, bool) or size < 1:
|
|
103
|
+
break
|
|
104
|
+
count *= size
|
|
105
|
+
else:
|
|
106
|
+
if count % 4:
|
|
107
|
+
diags.warning(
|
|
108
|
+
stmt.line,
|
|
109
|
+
"Adjusting dimensionality of UINT1 so number of bytes evenly divisible by 4",
|
|
110
|
+
stmt.file,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_early(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
115
|
+
_check_uint1_dims(program, profile, diags, env)
|
|
116
|
+
_check_reachability(program, profile, diags, env)
|
|
117
|
+
_check_fnotch(program, profile, diags, env)
|
|
118
|
+
# Numeric `Scan`/`DataInterval` value checks: also ungated (oracle-confirmed,
|
|
119
|
+
# `docs/instructions.md`), implemented in `instr_rules.py` alongside this module's other
|
|
120
|
+
# required-parameter check since they are instruction-argument-shaped, not warnings.
|
|
121
|
+
instr_rules.check_scan_params(program, profile, diags, env)
|
|
122
|
+
instr_rules.check_scan_max_interval(program, profile, diags, env)
|
|
123
|
+
instr_rules.check_data_interval(program, profile, diags, env)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _kw(stmt: S.Stmt) -> str:
|
|
127
|
+
if isinstance(stmt, S.Instruction):
|
|
128
|
+
return stmt.keyword.upper()
|
|
129
|
+
if isinstance(stmt, S.SubDef):
|
|
130
|
+
return stmt.keyword.upper() # SUB / FUNCTION
|
|
131
|
+
if isinstance(stmt, S.Unparsed):
|
|
132
|
+
word = re.match(r"\s*(DataLong|Data)\s", stmt.text or "", re.I)
|
|
133
|
+
if word:
|
|
134
|
+
return word.group(1).upper() # `Data foo bar`: still a `Data` line
|
|
135
|
+
return type(stmt).__name__.upper()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Instruction keywords that are declarations/structural markers, never "executed" and so never
|
|
139
|
+
# flagged by `_check_reachability` even when they sit right after unreachable code (oracle:
|
|
140
|
+
# `docs/instructions.md` -- `Dim`/`Public`/`Const`/`ConstTable`/`Alias`/`Units`/`DataTable`/
|
|
141
|
+
# `StructureType`/`ObjectType` bodies are all silent; `BeginProg`/`EndProg` themselves were never
|
|
142
|
+
# observed flagged either). `SLOWSEQUENCE` is handled separately (it *resets* reachability, see
|
|
143
|
+
# below, rather than merely being skipped).
|
|
144
|
+
_NON_EXECUTABLE_KEYWORDS = frozenset(
|
|
145
|
+
{
|
|
146
|
+
"BEGINPROG",
|
|
147
|
+
"ENDPROG",
|
|
148
|
+
"DATATABLE",
|
|
149
|
+
"ENDTABLE",
|
|
150
|
+
"ALIAS",
|
|
151
|
+
"UNITS",
|
|
152
|
+
"CONSTTABLE",
|
|
153
|
+
"ENDCONSTTABLE",
|
|
154
|
+
"STRUCTURE",
|
|
155
|
+
"ENDSTRUCTURE",
|
|
156
|
+
"STRUCTURETYPE",
|
|
157
|
+
"ENDSTRUCTURETYPE",
|
|
158
|
+
"OBJECTTYPE",
|
|
159
|
+
"ENDOBJECTTYPE",
|
|
160
|
+
"DATA",
|
|
161
|
+
"DATALONG", # (oracle, 2026-10-01: `Data 1,2` after `NextScan` is not flagged)
|
|
162
|
+
"SEQUENTIALMODE",
|
|
163
|
+
"PIPELINEMODE", # (oracle, CR6, 2026-10-02: not flagged after `NextScan`)
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_program_end(stmt: S.Stmt) -> bool:
|
|
169
|
+
"""Bare `End` and `End Prog` (any whitespace between the words) are never flagged as unreachable
|
|
170
|
+
and do not stop the search (oracle, all four loggers, 2026-10-12: `End` then `a = 1` after an
|
|
171
|
+
endless `Scan` warns only for `a = 1`)."""
|
|
172
|
+
if not (isinstance(stmt, S.Instruction) and stmt.keyword.upper() == "END"):
|
|
173
|
+
return False
|
|
174
|
+
return not stmt.args or (
|
|
175
|
+
len(stmt.args) == 1 and isinstance(stmt.args[0], S.Name) and stmt.args[0].name.upper() == "PROG"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _display_name(stmt: S.Stmt, profile: Profile) -> str:
|
|
180
|
+
"""The name the real compiler echoes in `%s never reached at run time.` -- an `Instruction`'s
|
|
181
|
+
canonical table spelling (`Delay`, `Scan`, proper-cased; oracle:
|
|
182
|
+
`tests/corpus/fuzz/cr6/mode_diff/19a402de98a4.CR6`/`cr1000x/mode_diff/cf78b8596121.json`), or
|
|
183
|
+
`Expr`, the compiler's own generic bare-expression-statement pseudo-instruction (`data/
|
|
184
|
+
instructions.json` index 5; already used the same way by `structure.py`'s "before BeginProg"
|
|
185
|
+
rule) for anything else -- a plain assignment (oracle: `Expr never reached`, this session's own
|
|
186
|
+
minimal probe) or a statement that failed to parse as a recognised instruction at all (oracle:
|
|
187
|
+
`tests/corpus/fuzz/cr6/message_diff/caf5c15cd16b.CR6`, a mutated `EndProg` that fails to parse
|
|
188
|
+
gets both `Invalid, or out of place expression:` *and* `Expr never reached at run time.`)."""
|
|
189
|
+
if isinstance(stmt, S.Instruction):
|
|
190
|
+
record = profile.instructions.get(stmt.keyword.upper())
|
|
191
|
+
return record["name"] if record else stmt.keyword
|
|
192
|
+
if isinstance(stmt, (S.DoLoop, S.For)):
|
|
193
|
+
# oracle: `Do never reached`, `Loop never reached`, `FOR never reached` (table spelling)
|
|
194
|
+
keyword = stmt.keyword.upper() if isinstance(stmt, S.DoLoop) else "FOR"
|
|
195
|
+
record = profile.instructions.get(keyword)
|
|
196
|
+
return record["name"] if record else keyword
|
|
197
|
+
if isinstance(stmt, S.Call):
|
|
198
|
+
return "Call" # `Call S(1)`, `S(1)`: oracle, CR6/CR300, 2026-10-08 (docs/fuzz-findings.md)
|
|
199
|
+
return "Expr"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _open_conditional(statements: list[S.Stmt], start: int, end: int) -> int:
|
|
203
|
+
"""How many `If`/`Select` blocks opened inside `statements[start:end]` are still open at its
|
|
204
|
+
end. Statements after `NextScan` inside them are not flagged; the one that closes the last of
|
|
205
|
+
them is (oracle, CR6: `If X Then` / `NextScan` / `X = Q` gives no warning, `If X Then` /
|
|
206
|
+
`NextScan` / `EndIf` gives `EndIf never reached at run time.`)."""
|
|
207
|
+
depth = 0
|
|
208
|
+
for stmt in statements[start:end]:
|
|
209
|
+
kw = _kw(stmt)
|
|
210
|
+
if (kw == "IF" and getattr(stmt, "inline", None) is None) or kw == "SELECT":
|
|
211
|
+
depth += 1
|
|
212
|
+
elif kw in ("ENDIF", "ENDSELECT") and depth:
|
|
213
|
+
depth -= 1
|
|
214
|
+
return depth
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _scan_span(statements: list[S.Stmt], start: int) -> tuple[int, bool]:
|
|
218
|
+
"""`statements[start]` is a top-level `SCAN`. Returns (index right after its matching
|
|
219
|
+
`NEXTSCAN`, whether an `EXITSCAN` appears anywhere in between). `SubScan`'s own block uses the
|
|
220
|
+
distinct `SUBSCAN`/`NEXTSUBSCAN` keywords (`data/instructions.json`), so a same-keyword depth
|
|
221
|
+
counter here never confuses the two."""
|
|
222
|
+
depth = 1
|
|
223
|
+
has_exit = False
|
|
224
|
+
j = start + 1
|
|
225
|
+
n = len(statements)
|
|
226
|
+
while j < n and depth > 0:
|
|
227
|
+
kw = _kw(statements[j])
|
|
228
|
+
if kw == "SCAN":
|
|
229
|
+
depth += 1
|
|
230
|
+
elif kw == "NEXTSCAN":
|
|
231
|
+
depth -= 1
|
|
232
|
+
elif kw == "EXITSCAN" or any(
|
|
233
|
+
_kw(x) == "EXITSCAN"
|
|
234
|
+
for x in (getattr(statements[j], "inline", None) or [])
|
|
235
|
+
+ (getattr(statements[j], "inline_else", None) or [])
|
|
236
|
+
):
|
|
237
|
+
has_exit = True # (also in a one-line `If X Then ExitScan`, oracle 2026-10-10)
|
|
238
|
+
j += 1
|
|
239
|
+
return j, has_exit
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _scan_count_is_zero(stmt: S.Instruction, env: consteval.Env) -> bool | None:
|
|
243
|
+
"""The `Scan(Interval,Units,BufferOption,Count)` `Count` argument's constant value compared to
|
|
244
|
+
0 (`True`/`False`), or `None` when it is missing or not a compile-time constant -- in which
|
|
245
|
+
case the caller must *not* treat the scan as provably infinite (AGENTS.md: never guess into a
|
|
246
|
+
false positive)."""
|
|
247
|
+
args = stmt.args
|
|
248
|
+
if len(args) < 4 or args[3] is None:
|
|
249
|
+
return None
|
|
250
|
+
try:
|
|
251
|
+
return consteval.value_of(args[3], env) == 0
|
|
252
|
+
except consteval.NotConstant:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _check_reachability(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
257
|
+
"""`%s never reached at run time.` -- oracle-confirmed (`docs/instructions.md`) to fire for the
|
|
258
|
+
*first* executable top-level statement that textually follows a `Scan(...,0)` (loop forever)
|
|
259
|
+
with no `ExitScan` anywhere in its body, up to (but not including) the next `SlowSequence`
|
|
260
|
+
(which starts a genuinely separate, concurrently-scheduled task, oracle-confirmed *not*
|
|
261
|
+
flagged) or the end of the statement list. Fires even when the file has unrelated errors
|
|
262
|
+
elsewhere (unlike `instructions.py`'s pass-2 checks) and, once found, this checker stops
|
|
263
|
+
looking for a second one -- every corpus/oracle example found only ever shows one, matching
|
|
264
|
+
this project's other "one diagnostic, then silence" findings (`docs/checker.md`).
|
|
265
|
+
|
|
266
|
+
A `Sub`/`Function` body is skipped entirely (its own statements are reachable whenever the sub
|
|
267
|
+
is called, independent of where its definition sits in the file).
|
|
268
|
+
"""
|
|
269
|
+
if any(d.message.startswith("Illegal END or EXIT ") for d in diags.items):
|
|
270
|
+
return # a pass-1 error of this kind hides the warning (oracle, CR6, 2026-10-03)
|
|
271
|
+
statements = program.statements
|
|
272
|
+
n = len(statements)
|
|
273
|
+
dead = False
|
|
274
|
+
open_depth = 0
|
|
275
|
+
sub_depth = 0
|
|
276
|
+
begun = False
|
|
277
|
+
tasks = profile.pipeline_tasks["tasks"]
|
|
278
|
+
pipeline_delays = {
|
|
279
|
+
id(st)
|
|
280
|
+
for st, _ in structure.pipeline_task_errors(program, profile)
|
|
281
|
+
if _kw(st) == "DELAY" or tasks.get(_kw(st), {}).get("hides_unreached")
|
|
282
|
+
}
|
|
283
|
+
i = 0
|
|
284
|
+
while i < n:
|
|
285
|
+
stmt = statements[i]
|
|
286
|
+
kw = _kw(stmt)
|
|
287
|
+
|
|
288
|
+
if sub_depth > 0:
|
|
289
|
+
if kw in ("SUB", "FUNCTION"):
|
|
290
|
+
sub_depth += 1
|
|
291
|
+
elif S.ends_routine(stmt):
|
|
292
|
+
sub_depth -= 1
|
|
293
|
+
i += 1
|
|
294
|
+
continue
|
|
295
|
+
if kw in ("SUB", "FUNCTION"):
|
|
296
|
+
sub_depth = 1
|
|
297
|
+
i += 1
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
if kw == "BEGINPROG":
|
|
301
|
+
begun = True
|
|
302
|
+
if kw == "SLOWSEQUENCE":
|
|
303
|
+
# A `SlowSequence` starts a genuinely separate, concurrently-scheduled task (oracle:
|
|
304
|
+
# `docs/instructions.md` -- a `SlowSequence`/`Scan`/`NextScan` right after an infinite,
|
|
305
|
+
# no-`ExitScan` main scan is never itself flagged). This checker has no oracle evidence
|
|
306
|
+
# either way for reachability *within* a slow sequence's own scan chain (a second,
|
|
307
|
+
# chained slow `Scan...NextScan` the way the main sequence supports, `docs/checker.md`'s
|
|
308
|
+
# own CHM "Scan Loop #1/#2" example) or for its own `EndSequence` terminator, which a
|
|
309
|
+
# false positive here (10 corpus regressions during development, all real, oracle-
|
|
310
|
+
# accepted programs, e.g. `ftpclient_1.CR6`'s `EndSequence`) showed is *not* flagged
|
|
311
|
+
# either. Rather than guess at slow-sequence-internal reachability, stop this analysis
|
|
312
|
+
# entirely once a `SlowSequence` is seen -- AGENTS.md: never guess into a false warning.
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
if kw == "SCAN" and isinstance(stmt, S.Instruction):
|
|
316
|
+
if dead:
|
|
317
|
+
diags.warning(stmt.line, f"{_display_name(stmt, profile)} never reached at run time.", stmt.file)
|
|
318
|
+
return
|
|
319
|
+
j, has_exit = _scan_span(statements, i)
|
|
320
|
+
count_is_zero = _scan_count_is_zero(stmt, env)
|
|
321
|
+
dead = bool(count_is_zero) and not has_exit
|
|
322
|
+
if not begun and profile.name in ("cr6", "cr1000x"):
|
|
323
|
+
dead = False # `Scan illegally occurs before BeginProg.`: the scan is not
|
|
324
|
+
# compiled, so nothing after it is unreachable (oracle, 2026-10-12, fuzz R15)
|
|
325
|
+
closer = statements[j - 1] if j <= n and j > i + 1 else None
|
|
326
|
+
if dead and closer is not None and hasattr(closer, "hoisted_last"):
|
|
327
|
+
# `If C Then NextScan` closes the endless `Scan` from inside a one-line `If`: the
|
|
328
|
+
# `If`'s own end is the first thing after it (oracle, all four loggers, 2026-10-12);
|
|
329
|
+
# more statements after the `NextScan` in the body silence the check
|
|
330
|
+
if closer.hoisted_last and not (
|
|
331
|
+
j < n and (statements[j].line, statements[j].file) == (closer.line, closer.file)
|
|
332
|
+
):
|
|
333
|
+
diags.warning(closer.line, "EndIf never reached at run time.", closer.file)
|
|
334
|
+
return
|
|
335
|
+
open_depth = _open_conditional(statements, i, j)
|
|
336
|
+
i = j
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
if dead and open_depth:
|
|
340
|
+
if kw in ("ENDIF", "ENDSELECT"):
|
|
341
|
+
open_depth -= 1
|
|
342
|
+
elif (isinstance(stmt, S.If) and stmt.inline is None) or kw == "SELECT":
|
|
343
|
+
open_depth += 1
|
|
344
|
+
if open_depth:
|
|
345
|
+
i += 1
|
|
346
|
+
continue
|
|
347
|
+
if dead and id(stmt) in pipeline_delays:
|
|
348
|
+
i += 1 # its pipeline-mode error replaces its own warning (oracle); later ones follow
|
|
349
|
+
continue
|
|
350
|
+
if dead and (kw in structure.SCAN_ONLY or kw == "SUBSCAN"):
|
|
351
|
+
return # its compile error ends pass 2 before the reachability check (oracle; a `SubScan` after the `NextScan` is outside any `Scan`)
|
|
352
|
+
if dead and (isinstance(stmt, S.If) or kw in ("SELECT", "NEXTSCAN")):
|
|
353
|
+
# An unreachable `If`/`Select` (block or one-line), or a stray `NextScan`, silences the
|
|
354
|
+
# check for the rest of the file (oracle, CR6: `If X Then` / `EndIf` / `X = 2` after a `NextScan` gives no
|
|
355
|
+
# warning at all; `docs/instructions.md`).
|
|
356
|
+
return
|
|
357
|
+
if dead:
|
|
358
|
+
# An `ExitScan` outside any `Scan` compiles to nothing (oracle: `ExitScan not within a
|
|
359
|
+
# Scan.` and no warning), unlike `ContinueScan`.
|
|
360
|
+
is_declaration = (
|
|
361
|
+
isinstance(stmt, (S.Declaration, S.ConstDecl, S.Label))
|
|
362
|
+
or kw in _NON_EXECUTABLE_KEYWORDS
|
|
363
|
+
or kw == "EXITSCAN"
|
|
364
|
+
or _is_program_end(stmt)
|
|
365
|
+
)
|
|
366
|
+
if not is_declaration:
|
|
367
|
+
name = _display_name(stmt, profile)
|
|
368
|
+
if (
|
|
369
|
+
isinstance(stmt, S.Call)
|
|
370
|
+
and not stmt.explicit
|
|
371
|
+
and not stmt.tail_ignored
|
|
372
|
+
and profile.name in ("cr6", "cr1000x")
|
|
373
|
+
):
|
|
374
|
+
# a bare `Nope`/`Nope(x)`/`Abs(x)` that is no Sub or Function is `Expr`; a
|
|
375
|
+
# variable gives no warning at all (oracle, CR6/CR1000X, 2026-10-12, fuzz R15)
|
|
376
|
+
routines = {st.name.upper() for st in statements if isinstance(st, S.SubDef)}
|
|
377
|
+
if stmt.name.upper() not in routines:
|
|
378
|
+
if stmt.name.upper() in symbols.declared_variable_names(program):
|
|
379
|
+
return
|
|
380
|
+
name = "Expr"
|
|
381
|
+
diags.warning(stmt.line, f"{name} never reached at run time.", stmt.file)
|
|
382
|
+
return
|
|
383
|
+
i += 1
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def check_background_calibration(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
387
|
+
"""`When Main Sequence Scan %d is running, background calibration will be disabled.` --
|
|
388
|
+
oracle-confirmed (`docs/instructions.md`) to fire once per *main-sequence* `Scan` (not one
|
|
389
|
+
declared under `SlowSequence`, whose own minimum interval, 100mSec, is far above every
|
|
390
|
+
threshold found) whose folded `Interval`/`Units` is below a logger-specific threshold in whole
|
|
391
|
+
milliseconds (`profile.background_calibration`, empty for CR300/CR350: oracle-probed down to a
|
|
392
|
+
100us scan interval with no such warning). Reported at the *last* statement's line, like
|
|
393
|
+
`structure.py`'s `No BeginProg in Program.` (same oracle-confirmed convention, independently
|
|
394
|
+
re-derived here since `structure.py` is a different workstream's owned file, `PLAN.md`).
|
|
395
|
+
|
|
396
|
+
**Round 6, gating correction**: unlike `_check_reachability`/`_check_fnotch` (still called from
|
|
397
|
+
`check_early`, genuinely ungated -- oracle-confirmed, `docs/instructions.md` "Two ungated call
|
|
398
|
+
sites"), this warning is **not** ungated on CR6/CR1000X either, despite living in the same
|
|
399
|
+
`check_early` function until this round and being covered by that same doc section's blanket
|
|
400
|
+
claim. Oracle-confirmed (a fresh sweep this round, an unrelated undefined-symbol error placed
|
|
401
|
+
both *before* and, separately, *after* an otherwise-calibration-warning-triggering `Scan(1,
|
|
402
|
+
mSec,...)` in the same file): the warning is suppressed **entirely** whenever the file has any
|
|
403
|
+
error anywhere, regardless of that error's own position relative to the `Scan` -- unlike
|
|
404
|
+
`_check_reachability`, which still fires on a later statement even with an earlier, unrelated
|
|
405
|
+
`Invalid symbol in expression` error present (`Delay never reached at run time.` alongside it,
|
|
406
|
+
same probe file). A warning-only file (no errors, e.g. a reachability warning with no error
|
|
407
|
+
anywhere) still gets *both* warnings together, confirming this is specifically errors, not "any
|
|
408
|
+
diagnostic", that gates it. Moved to `checker/__init__.py`'s own final `if not diags.errors:`
|
|
409
|
+
tier (alongside `warnings.check`/`tables_hook.attach`, the same "only on an otherwise-clean
|
|
410
|
+
compile" gate already used there) instead of `check_early`'s unconditional call site -- this is
|
|
411
|
+
the only change; the rule and message above are unchanged. Found as a side effect of developing
|
|
412
|
+
`instr_rules.check_scan_min_interval` (this round): dozens of pre-existing `false_accept`/
|
|
413
|
+
`message_diff` fuzz reproducers (`tests/corpus/fuzz/cr6/message_diff/ba333c2ee146.CR6` and
|
|
414
|
+
siblings) already showed this exact extra, spurious warning line alongside an unrelated real
|
|
415
|
+
error -- a genuine, previously-undocumented false-accept-shaped bug independent of this round's
|
|
416
|
+
own `Scan`-interval work, not a regression it introduced.
|
|
417
|
+
"""
|
|
418
|
+
data = profile.background_calibration
|
|
419
|
+
if not data:
|
|
420
|
+
return
|
|
421
|
+
threshold_ms = data["threshold_ms"]
|
|
422
|
+
statements = program.statements
|
|
423
|
+
last_line = statements[-1].line if statements else 1
|
|
424
|
+
|
|
425
|
+
# A scan with measurement time warns when its slack (interval minus time) is under
|
|
426
|
+
# `busy_slack` units of 100 us, or under its longest integration time plus a per-logger constant
|
|
427
|
+
# (oracle, 2026-10-10: CR6 `Scan(100,mSec)` + `Delay(0,97700,uSec)` warns, 97600 does not;
|
|
428
|
+
# `VoltSe(...,250)` alone warns up to Scan(10,mSec), not 11; CR1000X delay boundary
|
|
429
|
+
# 98000/97900). Such a scan never uses the empty-scan interval threshold above.
|
|
430
|
+
from . import scan_time
|
|
431
|
+
from .instr_measure import is_sequential
|
|
432
|
+
|
|
433
|
+
# Not in SequentialMode (oracle, CR6, 2026-10-10: not even for `Scan(1,mSec)`).
|
|
434
|
+
if is_sequential(program, profile, env):
|
|
435
|
+
return
|
|
436
|
+
busy_slack = data.get("busy_slack", 0)
|
|
437
|
+
busy, timed = set(), set()
|
|
438
|
+
for b in scan_time.budgets(program, env, profile):
|
|
439
|
+
if b.total:
|
|
440
|
+
timed.add(id(b.scan))
|
|
441
|
+
# a scan whose only timed instructions are PulseCounts never warns (oracle, CR6,
|
|
442
|
+
# 2026-10-10: Scan(1..9,mSec) + PulseCount)
|
|
443
|
+
gap = b.max_integ + scan_time.MODELS[profile.name].busy_extra if b.max_integ > 0 else 0
|
|
444
|
+
if b.interval - b.total < max(busy_slack, gap) and not b.pulse_only:
|
|
445
|
+
busy.add(id(b.scan))
|
|
446
|
+
in_slow = False
|
|
447
|
+
main_scan_index = 0
|
|
448
|
+
for stmt in statements:
|
|
449
|
+
kw = _kw(stmt)
|
|
450
|
+
if kw == "SLOWSEQUENCE":
|
|
451
|
+
in_slow = True
|
|
452
|
+
continue
|
|
453
|
+
if kw != "SCAN" or in_slow or not isinstance(stmt, S.Instruction):
|
|
454
|
+
continue
|
|
455
|
+
main_scan_index += 1
|
|
456
|
+
interval_ms = _scan_interval_ms(stmt, env)
|
|
457
|
+
if id(stmt) in busy or (id(stmt) not in timed and interval_ms is not None and interval_ms < threshold_ms):
|
|
458
|
+
diags.warning(
|
|
459
|
+
last_line,
|
|
460
|
+
f"When Main Sequence Scan {main_scan_index} is running, background calibration will be disabled.",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _value_in(args: list, position: int, values: list, env) -> bool:
|
|
465
|
+
"""Whether 1-based argument `position` is a constant among `values`."""
|
|
466
|
+
try:
|
|
467
|
+
return position <= len(args) and consteval.value_of(args[position - 1], env) in values
|
|
468
|
+
except consteval.NotConstant:
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def check_slow_scan_skips(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
473
|
+
"""`Slow Seq %d, Scan %d, may skip scans if running with Scan %d.` (round 54, oracle CR6 and
|
|
474
|
+
CR1000X, `docs/checker.md` "Task instructions in PipeLine Mode"): at the last statement's line,
|
|
475
|
+
for every slow-sequence `Scan` that holds a measurement instruction of
|
|
476
|
+
`pipeline_tasks["slow_scan_tasks"]` and every main-sequence `Scan` whose constant interval is not
|
|
477
|
+
shorter than the slow one's; nothing in SequentialMode or with errors elsewhere. Order: slow
|
|
478
|
+
sequence, slow scan, main scan."""
|
|
479
|
+
tasks = profile.pipeline_tasks.get("slow_scan_tasks")
|
|
480
|
+
if not tasks:
|
|
481
|
+
return
|
|
482
|
+
from .instr_measure import is_sequential
|
|
483
|
+
|
|
484
|
+
if is_sequential(program, profile, env):
|
|
485
|
+
return
|
|
486
|
+
rules = profile.pipeline_tasks["tasks"]
|
|
487
|
+
mains: list[float | None] = []
|
|
488
|
+
slows: list[list] = [] # [sequence, scan, interval ms or None, has a task]
|
|
489
|
+
seq = scan_no = 0
|
|
490
|
+
cur: list | None = None
|
|
491
|
+
for stmt in _with_inline_branches(program.statements):
|
|
492
|
+
kw = _kw(stmt)
|
|
493
|
+
if kw == "NEXT" and re.match(r"(?i)\s*Next\s+Scan\b", stmt.text or ""):
|
|
494
|
+
kw = "NEXTSCAN"
|
|
495
|
+
if kw == "SLOWSEQUENCE":
|
|
496
|
+
seq, scan_no = seq + 1, 0
|
|
497
|
+
elif kw == "SCAN" and isinstance(stmt, S.Instruction):
|
|
498
|
+
interval = _scan_interval_ms(stmt, env)
|
|
499
|
+
if seq == 0:
|
|
500
|
+
mains.append(interval)
|
|
501
|
+
else:
|
|
502
|
+
scan_no += 1
|
|
503
|
+
cur = [seq, scan_no, interval, False]
|
|
504
|
+
slows.append(cur)
|
|
505
|
+
elif kw == "NEXTSCAN":
|
|
506
|
+
cur = None
|
|
507
|
+
elif cur is not None and kw in tasks and isinstance(stmt, S.Instruction):
|
|
508
|
+
constants = rules.get(kw, {}).get("constant_values", {}) # (`Delay` option 0 only)
|
|
509
|
+
# (`Delay(0,0,...)` and `PortSet(...,1)` start no task, oracle round 55)
|
|
510
|
+
no_task = profile.pipeline_tasks.get("slow_scan_no_task", {}).get(kw, {})
|
|
511
|
+
try:
|
|
512
|
+
cur[3] = (
|
|
513
|
+
cur[3]
|
|
514
|
+
or all(
|
|
515
|
+
consteval.value_of(stmt.args[int(p) - 1], env) in allowed for p, allowed in constants.items()
|
|
516
|
+
)
|
|
517
|
+
and not any(_value_in(stmt.args, int(p), vals, env) for p, vals in no_task.items())
|
|
518
|
+
)
|
|
519
|
+
except consteval.NotConstant, IndexError:
|
|
520
|
+
pass
|
|
521
|
+
last_line = program.statements[-1].line if program.statements else 1
|
|
522
|
+
for seq_no, scan_no, interval, has_task in slows:
|
|
523
|
+
for main_no, main_interval in enumerate(mains, 1):
|
|
524
|
+
if has_task and interval is not None and main_interval is not None and interval <= main_interval:
|
|
525
|
+
diags.warning(
|
|
526
|
+
last_line, f"Slow Seq {seq_no}, Scan {scan_no}, may skip scans if running with Scan {main_no}."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _scan_interval_ms(stmt: S.Instruction, env: consteval.Env) -> float | None:
|
|
531
|
+
"""The `Scan` interval in milliseconds as the compiler holds it (100 us units from the 32-bit
|
|
532
|
+
seconds/microseconds of `instr_rules.scan_split`, so a wrapped `Scan(2147483647,Min,..)` is
|
|
533
|
+
negative and counts as shorter than any threshold)."""
|
|
534
|
+
split = instr_rules.scan_split(stmt, env)
|
|
535
|
+
if split is None:
|
|
536
|
+
return None
|
|
537
|
+
units = split[0] * 10000 + (abs(split[1]) // 100) * (1 if split[1] >= 0 else -1)
|
|
538
|
+
units &= 0xFFFFFFFF
|
|
539
|
+
return (units - 2**32 if units >= 2**31 else units) / 10
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _check_fnotch(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
543
|
+
"""`Invalid fnotch value, must be 50,60 400 or 4000` (CR300/CR350 only, no trailing period --
|
|
544
|
+
verbatim oracle text, `docs/instructions.md`) -- every CR300/CR350 instruction with a `fN1`
|
|
545
|
+
filter-frequency argument (found exhaustively: every instruction whose `data/<logger>/
|
|
546
|
+
instructions.json` `params` contains type code 20, `checker/warnings.py`'s sibling
|
|
547
|
+
`docs/instructions.md` cross-checks each one's argument position against its own CHM `Syntax`
|
|
548
|
+
line) restricts it to exactly `{50, 60, 400, 4000}`; CR6/CR1000X accept any value in a
|
|
549
|
+
continuous range instead (oracle: `PanelTemp(...,15000)` compiles clean on both, `15000` is
|
|
550
|
+
even the CHM's own example value) so `profile.fnotch` is empty there and this is a no-op.
|
|
551
|
+
Fires even when the file has unrelated errors elsewhere (oracle: `docs/instructions.md`), so
|
|
552
|
+
(like `_check_reachability`/`_check_background_calibration`) this is called ungated.
|
|
553
|
+
"""
|
|
554
|
+
data = profile.fnotch
|
|
555
|
+
if not data or "valid" not in data:
|
|
556
|
+
return # CR6/CR1000X carry a minimum-frequency error instead (`instr_measure.check_pass1_values`)
|
|
557
|
+
valid = set(data["valid"])
|
|
558
|
+
message = data["message"]
|
|
559
|
+
positions: dict[str, int] = data["instructions"]
|
|
560
|
+
|
|
561
|
+
for stmt in program.statements:
|
|
562
|
+
if not isinstance(stmt, S.Instruction):
|
|
563
|
+
continue
|
|
564
|
+
pos = positions.get(stmt.keyword.upper())
|
|
565
|
+
if pos is None:
|
|
566
|
+
continue
|
|
567
|
+
idx = pos - 1
|
|
568
|
+
if idx >= len(stmt.args) or stmt.args[idx] is None:
|
|
569
|
+
continue
|
|
570
|
+
arg = stmt.args[idx]
|
|
571
|
+
if not consteval.is_constant(arg, env):
|
|
572
|
+
continue # a variable here is a different, unrelated check's concern (not ours).
|
|
573
|
+
try:
|
|
574
|
+
value = consteval.value_of(arg, env)
|
|
575
|
+
except consteval.NotConstant:
|
|
576
|
+
continue
|
|
577
|
+
if value not in valid:
|
|
578
|
+
diags.warning(stmt.line, message, stmt.file)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
_TRUNCATE_CALLS = {"IFTIME": 3, "TIMEINTOINTERVAL": 3}
|
|
582
|
+
_TRUNCATE_CALL_RE = re.compile(r"(?i)\b(IfTime|TimeIntoInterval)\s*\(")
|
|
583
|
+
_ARITY_ERROR_PREFIXES = (
|
|
584
|
+
"Expression expects , :",
|
|
585
|
+
"No Matching, or out of place )",
|
|
586
|
+
"Invalid, or out of place expression:",
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _call_args(text: str, start: int) -> list[str] | None:
|
|
591
|
+
"""The top-level argument texts of the call whose `(` ends just before `start`."""
|
|
592
|
+
depth, args, cur, i = 1, [], "", start
|
|
593
|
+
while i < len(text):
|
|
594
|
+
ch = text[i]
|
|
595
|
+
if ch == '"':
|
|
596
|
+
j = text.find('"', i + 1)
|
|
597
|
+
if j < 0:
|
|
598
|
+
return None
|
|
599
|
+
cur += text[i : j + 1]
|
|
600
|
+
i = j + 1
|
|
601
|
+
continue
|
|
602
|
+
if ch in "({":
|
|
603
|
+
depth += 1
|
|
604
|
+
elif ch in ")}":
|
|
605
|
+
depth -= 1
|
|
606
|
+
if depth == 0:
|
|
607
|
+
args.append(cur)
|
|
608
|
+
return [x for x in args] if any(x.strip() for x in args) else []
|
|
609
|
+
elif ch == "," and depth == 1:
|
|
610
|
+
args.append(cur)
|
|
611
|
+
cur = ""
|
|
612
|
+
i += 1
|
|
613
|
+
continue
|
|
614
|
+
cur += ch
|
|
615
|
+
i += 1
|
|
616
|
+
return None
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def add_truncate_warnings(program: S.Program, profile, diags: Diagnostics) -> None:
|
|
620
|
+
"""All four loggers (oracle, 2026-10-12, `docs/checker.md` "Round 14 truncate warnings"): an
|
|
621
|
+
`IfTime`/`TimeIntoInterval` call that fails its argument count prints `Warning: Parameter will
|
|
622
|
+
truncate to the nearest integer.` once per missing argument and once per argument that holds a
|
|
623
|
+
variable, at its line before the arity error (`Expression expects , :`, `No Matching, or out of
|
|
624
|
+
place )`, or the empty-call `Invalid, or out of place expression:`). Not modelled: the same warnings
|
|
625
|
+
of a valid call with a variable argument (printed twice, in both passes)."""
|
|
626
|
+
variables = symbols.declared_variable_names(program)
|
|
627
|
+
for stmt in program.statements:
|
|
628
|
+
text = getattr(stmt, "text", None) or ""
|
|
629
|
+
m = _TRUNCATE_CALL_RE.search(text)
|
|
630
|
+
if not m:
|
|
631
|
+
continue
|
|
632
|
+
args = _call_args(text, m.end())
|
|
633
|
+
if args is None:
|
|
634
|
+
continue
|
|
635
|
+
need = _TRUNCATE_CALLS[m.group(1).upper()]
|
|
636
|
+
if len(args) == need:
|
|
637
|
+
continue
|
|
638
|
+
with_vars = sum(
|
|
639
|
+
1
|
|
640
|
+
for a in args
|
|
641
|
+
if any(w.upper() in variables for w in re.findall(r"[A-Za-z_]\w*", re.sub(r'"[^"]*"', "", a)))
|
|
642
|
+
)
|
|
643
|
+
count = max(need - len(args), 0) + with_vars
|
|
644
|
+
key = (stmt.file, stmt.line)
|
|
645
|
+
at = next(
|
|
646
|
+
(
|
|
647
|
+
j
|
|
648
|
+
for j, d in enumerate(diags.items)
|
|
649
|
+
if (d.file, d.line) == key
|
|
650
|
+
and d.severity == "error"
|
|
651
|
+
and d.message.startswith(_ARITY_ERROR_PREFIXES)
|
|
652
|
+
and m.group(1).lower() in d.message.lower()
|
|
653
|
+
),
|
|
654
|
+
None,
|
|
655
|
+
)
|
|
656
|
+
if at is None or not count:
|
|
657
|
+
continue
|
|
658
|
+
diags.items[at:at] = [
|
|
659
|
+
Diagnostic("warning", stmt.line, "Parameter will truncate to the nearest integer.", stmt.file)
|
|
660
|
+
for _ in range(count)
|
|
661
|
+
]
|