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,1547 @@
|
|
|
1
|
+
"""Instruction-specific measurement/I-O semantic checks that are not the channel/port *terminal*
|
|
2
|
+
range model (`terminals.py`, `data/<logger>/terminals.json`). Value-set and value-range checks for
|
|
3
|
+
`Range` codes, `SW12` ports, `Delay`, `CS616`, `TimerInput` and `ExciteV`/`ExciteI`, per logger,
|
|
4
|
+
driven by `data/<logger>/measure.json`. Pass 2 (`docs/checker.md` "Two-pass compilation"): called
|
|
5
|
+
from `checker/__init__.py` alongside `instructions.check`, inside the `if not diags.errors:` gate,
|
|
6
|
+
so (matching every existing pass-2 check) it never fires when the file has an earlier, unrelated
|
|
7
|
+
error. All rules below are oracle-confirmed per logger this round; see `docs/instructions.md`
|
|
8
|
+
"Measurement/I-O instruction checks" for the evidence and each logger's coverage.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
from typing import TypeGuard, cast
|
|
14
|
+
|
|
15
|
+
from .. import consteval
|
|
16
|
+
from .. import syntax as S
|
|
17
|
+
from ..diagnostics import Diagnostic, Diagnostics
|
|
18
|
+
from ..profiles import Profile
|
|
19
|
+
from . import longconv, scan_time, symbols
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
23
|
+
measure = profile.measure
|
|
24
|
+
if not measure:
|
|
25
|
+
return
|
|
26
|
+
file_reported = False # only the first bad `FileManage`/`RunProgram` file name is reported
|
|
27
|
+
sequential: bool | None = None
|
|
28
|
+
from .instr_data import _with_inline
|
|
29
|
+
|
|
30
|
+
# a one-line If's statement too (oracle, CR6/CR1000X, round 58, docs/checker.md#one-line-if-statements-in-tier-c-round-58)
|
|
31
|
+
slow = False
|
|
32
|
+
for stmt in _with_inline(program.statements):
|
|
33
|
+
if not isinstance(stmt, S.Instruction):
|
|
34
|
+
continue
|
|
35
|
+
kw = stmt.keyword.upper()
|
|
36
|
+
slow = kw == "SLOWSEQUENCE" or slow and kw != "ENDPROG"
|
|
37
|
+
if kw == "SW12" and "sw12_ports" in measure:
|
|
38
|
+
_check_sw12(stmt, measure, env, diags)
|
|
39
|
+
if kw == "DELAY" and "delay" in measure:
|
|
40
|
+
if sequential is None:
|
|
41
|
+
sequential = is_sequential(program, profile, env)
|
|
42
|
+
_check_delay(stmt, measure["delay"], env, diags, sequential, slow)
|
|
43
|
+
if kw == "EXCITEV" and measure.get("delay", {}).get("task_memory") and len(stmt.args) == 3:
|
|
44
|
+
delay = _int_value(stmt.args[2], env)
|
|
45
|
+
if delay is not None and scan_time.task_memory_overflow(max(scan_time.excite_delay_us(delay) - 10, 0)):
|
|
46
|
+
_task_memory_full(stmt, diags)
|
|
47
|
+
if kw == "CS616" and "cs616_min_reps_pos" in measure:
|
|
48
|
+
_check_cs616_reps(stmt, measure, env, diags)
|
|
49
|
+
if kw in ("EXCITEV", "EXCITEI") and "excite_max_mv" in measure:
|
|
50
|
+
_check_excite_magnitude(stmt, measure, env, diags)
|
|
51
|
+
if kw == "GPS" and "gps" in measure:
|
|
52
|
+
_check_structure_members(stmt, program, measure["gps"], env, diags)
|
|
53
|
+
_check_gps_nmea(stmt, program, env, diags)
|
|
54
|
+
_check_gps_port(stmt, measure["gps"], env, diags)
|
|
55
|
+
if kw == "CH400STATUS" and "CH400STATUS" in profile.instructions:
|
|
56
|
+
_check_ch400status(stmt, env, diags)
|
|
57
|
+
if kw == "FFT" and "FFT" in profile.instructions:
|
|
58
|
+
_check_fft_source(stmt, symbols.declared_all_dims(symbols.scoped_view(program, stmt), env), env, diags)
|
|
59
|
+
if kw == "SDI12RECORDER" and profile.single_pass:
|
|
60
|
+
_check_sdi12_structure_dest(stmt, program, diags)
|
|
61
|
+
if kw == "TABLEFILE" and "tablefile" in measure:
|
|
62
|
+
_check_tablefile(stmt, measure["tablefile"], env, diags)
|
|
63
|
+
for rule in measure.get("string_prefixes", ()):
|
|
64
|
+
if kw == rule["instr"] and not (file_reported and "ext" in rule):
|
|
65
|
+
file_reported = _check_string_prefix(stmt, rule, env, diags) or file_reported
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_const_ranges(flat: list, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
69
|
+
"""Data-driven constant-parameter checks (`measure.json` `const_ranges`, `docs/instructions.md`
|
|
70
|
+
"Constant-parameter range checks"): the parameter at `pos` converts as C's `(long)` and must
|
|
71
|
+
lie in `min..max` or in `values`, else `message` (`{value}` is the converted number, or that
|
|
72
|
+
of the parameter at `show`). `wrap16` rules (`StaticRoute` addresses) keep the low 16 bits of
|
|
73
|
+
the converted number. A variable or unresolved name is another check's error."""
|
|
74
|
+
by_instr: dict[str, list[dict]] = {}
|
|
75
|
+
for rule in profile.measure.get("const_ranges", ()):
|
|
76
|
+
by_instr.setdefault(rule["instr"], []).append(rule)
|
|
77
|
+
if not by_instr:
|
|
78
|
+
return
|
|
79
|
+
from .instr_rules import cdm_null_module, cdm_unsupported_module
|
|
80
|
+
|
|
81
|
+
names = longconv.float_names(flat), longconv.literal_names(flat)
|
|
82
|
+
for stmt in flat:
|
|
83
|
+
if not isinstance(stmt, S.Instruction):
|
|
84
|
+
continue
|
|
85
|
+
if stmt.keyword.upper().startswith("CDM_") and (
|
|
86
|
+
cdm_null_module(stmt, env) or cdm_unsupported_module(stmt, env)
|
|
87
|
+
):
|
|
88
|
+
continue
|
|
89
|
+
tccomp = bool(stmt.args) and stmt.keyword.upper() == "CDM_TCCOMP" and _int_value(stmt.args[0], env) == 224
|
|
90
|
+
for rule in by_instr.get(stmt.keyword.upper(), ()):
|
|
91
|
+
if tccomp and rule["pos"] == 5:
|
|
92
|
+
_tccomp_before_channel(stmt, env, profile, names, diags)
|
|
93
|
+
_apply_const_rule(stmt, rule, env, profile, names, diags)
|
|
94
|
+
if tccomp and rule["pos"] == 5:
|
|
95
|
+
_tccomp_after_channel(stmt, env, profile, names, diags)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _tccomp_before_channel(stmt, env, profile, names, diags) -> None:
|
|
99
|
+
"""`CDM_TCComp(TEMP120, ...)`: a negative channel first gives `Burst measurements unsupported,
|
|
100
|
+
input channel must be > 0!` (oracle, CR6/CR1000X, 2026-10-10; docs/instructions.md "CPI
|
|
101
|
+
measurement time")."""
|
|
102
|
+
channel = _long_arg(stmt, 5, {}, env, profile, names)
|
|
103
|
+
if channel is not None and channel < 0:
|
|
104
|
+
diags.error(stmt.line, "Burst measurements unsupported, input channel must be > 0!", stmt.file)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _tccomp_after_channel(stmt, env, profile, names, diags) -> None:
|
|
108
|
+
"""After the channel message: `Reps cycle beyond last channel!` when `channel - 1 + Reps > 20`
|
|
109
|
+
(a channel outside 1..20 counts as 1), then `Thermocouple type unsupported.` for a type
|
|
110
|
+
outside 0..7."""
|
|
111
|
+
channel, reps = (_long_arg(stmt, pos, {}, env, profile, names) for pos in (5, 4))
|
|
112
|
+
if channel is not None and reps is not None:
|
|
113
|
+
if not 1 <= channel <= 20:
|
|
114
|
+
channel = 1
|
|
115
|
+
if channel - 1 + reps > 20:
|
|
116
|
+
diags.error(stmt.line, "Reps cycle beyond last channel!", stmt.file)
|
|
117
|
+
tc_type = _long_arg(stmt, 6, {}, env, profile, names)
|
|
118
|
+
if tc_type is not None and not 0 <= tc_type <= 7:
|
|
119
|
+
diags.error(stmt.line, "Thermocouple type unsupported.", stmt.file)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _apply_cdm_channel(stmt, rule, env, profile, names, diags) -> None:
|
|
123
|
+
"""CDM analog measurement channel (`channel_by_module`: module value -> last channel; oracle,
|
|
124
|
+
CR6/CR1000X, 2026-10-12, docs/instructions.md "CDM measurement channels"). A negative channel
|
|
125
|
+
is a two-measurement pair counted by magnitude; `-2147483648` (a string, overflow) is only
|
|
126
|
+
the range line; `Reps` that run past the last channel are `Reps cycle input beyond last
|
|
127
|
+
channel!` unless `Reps` is itself out of range."""
|
|
128
|
+
last = rule["channel_by_module"].get(str(_int_value(stmt.args[0], env)))
|
|
129
|
+
value = _long_arg(stmt, rule["pos"], rule, env, profile, names)
|
|
130
|
+
if last is None or value is None:
|
|
131
|
+
return
|
|
132
|
+
int_min = -(2**31)
|
|
133
|
+
magnitude = -value if int_min < value < 0 else value
|
|
134
|
+
role = rule["role"]
|
|
135
|
+
if role == "ic":
|
|
136
|
+
if value == int_min or magnitude < 1 or magnitude > last:
|
|
137
|
+
diags.error(
|
|
138
|
+
stmt.line, rule["message"].format(value=magnitude if value != int_min else value, max=last), stmt.file
|
|
139
|
+
)
|
|
140
|
+
elif role == "rc":
|
|
141
|
+
reps = _long_arg(stmt, 4, rule, env, profile, names) if len(stmt.args) >= 4 else None
|
|
142
|
+
if value > last or (1 <= value and reps is not None and 1 <= reps <= 65536 and value - 1 + reps > last):
|
|
143
|
+
diags.error(stmt.line, rule["message"], stmt.file)
|
|
144
|
+
elif role == "tm" and int_min < value < 0 and magnitude > last:
|
|
145
|
+
diags.error(stmt.line, rule["message"], stmt.file)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _apply_const_rule(stmt, rule, env, profile, names, diags) -> None:
|
|
149
|
+
if "channel_by_module" in rule:
|
|
150
|
+
_apply_cdm_channel(stmt, rule, env, profile, names, diags)
|
|
151
|
+
return
|
|
152
|
+
if "skip_modules" in rule and stmt.args and str(_int_value(stmt.args[0], env)) in rule["skip_modules"]:
|
|
153
|
+
return
|
|
154
|
+
raw = _raw_arg(stmt, rule["pos"], env)
|
|
155
|
+
if raw is None or (rule.get("skip_strings") and isinstance(raw, str)):
|
|
156
|
+
return
|
|
157
|
+
if "invalid_raw" in rule:
|
|
158
|
+
if _matches_invalid_raw(raw, rule["invalid_raw"]):
|
|
159
|
+
diags.error(stmt.line, rule["message"].format(value=consteval.to_long(raw)), stmt.file)
|
|
160
|
+
return
|
|
161
|
+
value = _long_arg(stmt, rule["pos"], rule, env, profile, names)
|
|
162
|
+
if value is None:
|
|
163
|
+
return
|
|
164
|
+
if "lower_gate" in rule and value < rule["lower_gate"]:
|
|
165
|
+
return
|
|
166
|
+
if value in rule.get("allow", ()):
|
|
167
|
+
return
|
|
168
|
+
if value in rule.get("warn_values", ()):
|
|
169
|
+
diags.warning(stmt.line, rule["warn_message"], stmt.file)
|
|
170
|
+
return
|
|
171
|
+
if "by_module" in rule:
|
|
172
|
+
# a per-device port set (oracle 2026-10-12); a module outside the table is left alone
|
|
173
|
+
allowed = rule["by_module"].get(str(_int_value(stmt.args[0], env)), rule.get("values"))
|
|
174
|
+
if allowed is None or value in allowed:
|
|
175
|
+
return
|
|
176
|
+
elif value in rule["values"] if "values" in rule else rule["min"] <= value <= rule["max"]:
|
|
177
|
+
return
|
|
178
|
+
shown = _long_arg(stmt, rule.get("show", rule["pos"]), rule, env, profile, names)
|
|
179
|
+
if shown is not None:
|
|
180
|
+
diags.error(stmt.line, rule["message"].format(value=shown), stmt.file)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _matches_invalid_raw(value: object, invalid: dict) -> bool:
|
|
184
|
+
if invalid.get("strings") and isinstance(value, str):
|
|
185
|
+
return True
|
|
186
|
+
if not isinstance(value, (int, float)):
|
|
187
|
+
return False
|
|
188
|
+
if invalid.get("nan") and value != value:
|
|
189
|
+
return True
|
|
190
|
+
if value == float("inf") or value == float("-inf"):
|
|
191
|
+
return False
|
|
192
|
+
if invalid.get("outside_long") and (value < -2147483648 or value > 2147483647):
|
|
193
|
+
return True
|
|
194
|
+
return value in invalid.get("values", ())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _long_arg(stmt: S.Instruction, pos: int, rule: dict, env: consteval.Env, profile: Profile, names) -> int | None:
|
|
198
|
+
value = _raw_arg(stmt, pos, env)
|
|
199
|
+
if value is None:
|
|
200
|
+
return None
|
|
201
|
+
if rule.get("string_zero") and isinstance(value, str):
|
|
202
|
+
return 0
|
|
203
|
+
if rule.get("inf_max") and isinstance(value, (int, float)) and value >= 2**128 - 2**103:
|
|
204
|
+
return 2147483647 # (a Settling time that is a float infinity, `1E39`/`INF`, round 31)
|
|
205
|
+
if not rule.get("wrap16"):
|
|
206
|
+
# The profile-aware conversion (`checker/longconv.py`) where it covers the expression;
|
|
207
|
+
# `to_long` for the forms it does not (function calls, `TRUE`).
|
|
208
|
+
converted = longconv.long_value(stmt.args[pos - 1], env, profile, *names)
|
|
209
|
+
return consteval.to_long(value) if converted is None else converted
|
|
210
|
+
# A string or NaN is 0; a number outside Long range is 0 on CR6/CR1000X and unobserved
|
|
211
|
+
# (`overflow: null`, skipped) on CR300/CR350; infinity is 65535 (not modelled).
|
|
212
|
+
if isinstance(value, str) or value != value:
|
|
213
|
+
return 0
|
|
214
|
+
if abs(value) == float("inf"):
|
|
215
|
+
return None
|
|
216
|
+
if abs(value) >= 2**31:
|
|
217
|
+
return rule["overflow"]
|
|
218
|
+
return int(value) & 0xFFFF
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _raw_arg(stmt: S.Instruction, pos: int, env: consteval.Env):
|
|
222
|
+
return _fnotch_value(stmt.args[pos - 1], env) if pos <= len(stmt.args) and stmt.args[pos - 1] is not None else None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _check_string_prefix(stmt: S.Instruction, rule: dict, env: consteval.Env, diags: Diagnostics) -> bool:
|
|
226
|
+
"""A constant file name's drive (`prefixes`) and, for `FileManage`/`RunProgram` (`ext`), its
|
|
227
|
+
extension: with option bits 2 or 4 set the text after the last `.` must start with `crb` or
|
|
228
|
+
the logger's own program extension, else `FileManage name <name> invalid extension.`
|
|
229
|
+
(docs/instructions.md#filemanage-drive-and-dnp-comport-constants). True when it reported."""
|
|
230
|
+
idx = rule["pos"] - 1
|
|
231
|
+
if idx >= len(stmt.args) or stmt.args[idx] is None:
|
|
232
|
+
return False
|
|
233
|
+
value = _const_value(stmt.args[idx], env)
|
|
234
|
+
if not isinstance(value, str):
|
|
235
|
+
return False
|
|
236
|
+
drive = value[:4]
|
|
237
|
+
if drive.upper() not in (f"{p}:" for p in rule["prefixes"]):
|
|
238
|
+
diags.error(stmt.line, rule["message"].format(drive=drive), stmt.file)
|
|
239
|
+
return True
|
|
240
|
+
option = _int_value(stmt.args[1], env) if "ext" in rule and len(stmt.args) > 1 else None
|
|
241
|
+
if option is not None and abs(option) < 2**31 and option & 6:
|
|
242
|
+
ext = value.strip().rpartition(".")[2].upper() if "." in value else ""
|
|
243
|
+
if not ext.startswith(("CRB", rule["ext"])):
|
|
244
|
+
diags.error(stmt.line, f"FileManage name {value} invalid extension.", stmt.file)
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _check_ch400status(stmt: S.Instruction, env: consteval.Env, diags: Diagnostics) -> None:
|
|
250
|
+
"""`CH400Status(Interval, Units)`: the converted interval times the unit, in microseconds,
|
|
251
|
+
must be a whole number of seconds; docs/instructions.md "CH400Status interval and FFT source length"."""
|
|
252
|
+
if len(stmt.args) < 2:
|
|
253
|
+
return
|
|
254
|
+
value, unit = _int_value(stmt.args[0], env), _const_value(stmt.args[1], env)
|
|
255
|
+
if value is None or not isinstance(unit, (int, float)) or int(unit) not in _DELAY_US_PER_UNIT:
|
|
256
|
+
return
|
|
257
|
+
if value * _DELAY_US_PER_UNIT[int(unit)] % 1_000_000:
|
|
258
|
+
diags.error(stmt.line, "CH400Status update interval must be an integer multiple of 1 second.", stmt.file)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _check_fft_source(stmt: S.Instruction, dims: dict, env: consteval.Env, diags: Diagnostics) -> None:
|
|
262
|
+
"""`FFT(Source, Res, N, ...)`: the source from the given element on must hold `N` values
|
|
263
|
+
(`N` a power of two), else `<name> must be of length >= N.`"""
|
|
264
|
+
if len(stmt.args) < 3 or not isinstance(stmt.args[0], (S.Name, S.Index)):
|
|
265
|
+
return
|
|
266
|
+
n = _int_value(stmt.args[2], env)
|
|
267
|
+
shape = dims.get(stmt.args[0].name.upper())
|
|
268
|
+
if n is None or n < 1 or n & (n - 1) or shape is None:
|
|
269
|
+
return
|
|
270
|
+
offset = _flat_offset(stmt.args[0], shape)
|
|
271
|
+
if offset is not None and math.prod(shape) - offset < n:
|
|
272
|
+
diags.error(stmt.line, f"{stmt.args[0].name} must be of length >= {n}.", stmt.file)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _check_sdi12_structure_dest(stmt: S.Instruction, program: S.Program, diags: Diagnostics) -> None:
|
|
276
|
+
"""CR300/CR350: a StructureType variable as the `SDI12Recorder` destination gives `Could not
|
|
277
|
+
find DataTable <argument text>.`; docs/instructions.md#sdi12recorder-structure-destination."""
|
|
278
|
+
dest = stmt.args[0] if stmt.args else None
|
|
279
|
+
if not isinstance(dest, (S.Name, S.Index)) or (isinstance(dest, S.Index) and None in dest.args):
|
|
280
|
+
return
|
|
281
|
+
type_name = symbols.declared_variable_types(program).get(dest.name.upper())
|
|
282
|
+
if type_name is not None and any(
|
|
283
|
+
isinstance(st, S.Instruction)
|
|
284
|
+
and st.keyword.upper() == "STRUCTURETYPE"
|
|
285
|
+
and st.args
|
|
286
|
+
and isinstance(st.args[0], S.Name)
|
|
287
|
+
and st.args[0].name.upper() == type_name
|
|
288
|
+
for st in program.statements
|
|
289
|
+
):
|
|
290
|
+
from .expressions import _arg_text
|
|
291
|
+
|
|
292
|
+
diags.error(stmt.line, f"Could not find DataTable {_arg_text(stmt, 0)}.", stmt.file)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _check_tablefile(stmt: S.Instruction, cfg: dict, env: consteval.Env, diags: Diagnostics) -> None:
|
|
296
|
+
"""`TableFile` file name drive and file-type option; docs/instructions.md#tablefile-drive."""
|
|
297
|
+
name = _name_text(stmt.args[0], env) if stmt.args else None
|
|
298
|
+
if not name:
|
|
299
|
+
return
|
|
300
|
+
if name[:4].upper() not in (f"{d}:" for d in cfg["drives"]):
|
|
301
|
+
diags.error(stmt.line, f"Invalid drive or no drive specified in filename {name}.", stmt.file)
|
|
302
|
+
option = _int_value(stmt.args[1], env) if len(stmt.args) > 1 else None
|
|
303
|
+
if option is not None and cfg.get("option_mod100"):
|
|
304
|
+
low = abs(int(math.fmod(option, 100)))
|
|
305
|
+
if option >= 0 and low >= 64 and name[:4].upper() in (f"{d}:" for d in cfg.get("tob3_drives", ())):
|
|
306
|
+
diags.error(stmt.line, "TOB3 TableFile output only allowed on CRD: drive.", stmt.file)
|
|
307
|
+
if 24 <= low <= 31 or low >= 40:
|
|
308
|
+
diags.error(stmt.line, "Invalid file type option.", stmt.file)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _name_text(arg: S.Expr | None, env: consteval.Env) -> str | None:
|
|
312
|
+
"""A constant file name; `Status.SerialNumber` folds to the empty string (oracle)."""
|
|
313
|
+
if (
|
|
314
|
+
isinstance(arg, S.Member)
|
|
315
|
+
and isinstance(arg.base, S.Name)
|
|
316
|
+
and arg.args is None
|
|
317
|
+
and (arg.base.name.upper(), arg.member.upper()) == ("STATUS", "SERIALNUMBER")
|
|
318
|
+
):
|
|
319
|
+
return ""
|
|
320
|
+
if isinstance(arg, S.Binary) and arg.op in ("+", "&"):
|
|
321
|
+
left, right = _name_text(arg.left, env), _name_text(arg.right, env)
|
|
322
|
+
return None if left is None or right is None else left + right
|
|
323
|
+
value = _const_value(arg, env)
|
|
324
|
+
return value if isinstance(value, str) else None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _check_structure_members(
|
|
328
|
+
stmt: S.Instruction, program: S.Program, cfg: dict, env: consteval.Env, diags: Diagnostics
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Data-driven checks on members of a StructureType used as an instruction destination."""
|
|
331
|
+
rules = cfg.get("structure_members", ())
|
|
332
|
+
if not rules or not stmt.args:
|
|
333
|
+
return
|
|
334
|
+
dest = stmt.args[0]
|
|
335
|
+
if not isinstance(dest, (S.Name, S.Index)):
|
|
336
|
+
return
|
|
337
|
+
type_name = symbols.declared_variable_types(program).get(dest.name.upper())
|
|
338
|
+
if type_name is None:
|
|
339
|
+
return
|
|
340
|
+
members = _structure_members(program, type_name)
|
|
341
|
+
for rule in rules:
|
|
342
|
+
member = members.get(rule["member"])
|
|
343
|
+
if member is None or (member.type or "FLOAT").upper() != rule["type"]:
|
|
344
|
+
continue
|
|
345
|
+
size = _int_value(member.string_size, env)
|
|
346
|
+
if size is not None and size < rule["min_string_size"]:
|
|
347
|
+
diags.error(stmt.line, rule["message"], stmt.file)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _structure_members(program: S.Program, type_name: str) -> dict[str, S.VarDecl]:
|
|
351
|
+
members: dict[str, S.VarDecl] = {}
|
|
352
|
+
active = False
|
|
353
|
+
for candidate in program.statements:
|
|
354
|
+
if isinstance(candidate, S.Instruction):
|
|
355
|
+
kw = candidate.keyword.upper()
|
|
356
|
+
if kw == "STRUCTURETYPE":
|
|
357
|
+
active = bool(
|
|
358
|
+
candidate.args
|
|
359
|
+
and isinstance(candidate.args[0], S.Name)
|
|
360
|
+
and candidate.args[0].name.upper() == type_name
|
|
361
|
+
)
|
|
362
|
+
continue
|
|
363
|
+
if kw == "ENDSTRUCTURETYPE" and active:
|
|
364
|
+
break
|
|
365
|
+
if active and isinstance(candidate, S.Declaration) and candidate.kind == "STRUCTUREMEMBER":
|
|
366
|
+
members.update({var.name.upper(): var for var in candidate.vars})
|
|
367
|
+
return members
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def check_pass1_values(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
371
|
+
"""CR6/CR1000X pass 1 value checks on const-family positions: `Range` code, then `fN1`
|
|
372
|
+
(position order within one statement). Oracle: both are reported next to an earlier pass-1
|
|
373
|
+
error (`Dim Z banana`, `Delay(0,5,Foo)`), unlike the tier-C checks of `check`
|
|
374
|
+
(`docs/instructions.md` "fN1 / fnotch"). A statement that already has an error on its line
|
|
375
|
+
(unresolved name, variable in a constant position) is abandoned before these run."""
|
|
376
|
+
from . import expressions, scan_time
|
|
377
|
+
|
|
378
|
+
measure = profile.measure
|
|
379
|
+
fnotch = profile.fnotch
|
|
380
|
+
ranges = measure.get("range_code_instructions", {}) if measure else {}
|
|
381
|
+
fn_pos = fnotch.get("instructions", {}) if fnotch and "min_hz" in fnotch else {}
|
|
382
|
+
if not ranges and not fn_pos:
|
|
383
|
+
return
|
|
384
|
+
variable_names = frozenset(
|
|
385
|
+
name
|
|
386
|
+
for name in symbols.declared_variable_names(program)
|
|
387
|
+
if not symbols.is_reserved_predefined_name(name, profile)
|
|
388
|
+
)
|
|
389
|
+
earlier = {(d.line, d.file) for d in diags.items if d.severity == "error"}
|
|
390
|
+
flat = []
|
|
391
|
+
for st in program.statements:
|
|
392
|
+
flat.append(st)
|
|
393
|
+
if isinstance(st, S.If):
|
|
394
|
+
flat.extend((st.inline or []) + (st.inline_else or []))
|
|
395
|
+
for stmt in flat:
|
|
396
|
+
if not isinstance(stmt, S.Instruction):
|
|
397
|
+
continue
|
|
398
|
+
kw = stmt.keyword.upper()
|
|
399
|
+
if kw not in ranges and kw not in fn_pos:
|
|
400
|
+
continue
|
|
401
|
+
if (stmt.line, stmt.file) in earlier:
|
|
402
|
+
continue
|
|
403
|
+
if any(
|
|
404
|
+
expressions._find_arity_problem(a, profile)
|
|
405
|
+
or expressions._find_bare_function_ref(a, profile, variable_names)
|
|
406
|
+
for a in stmt.args
|
|
407
|
+
if a is not None
|
|
408
|
+
):
|
|
409
|
+
continue
|
|
410
|
+
if kw in ranges:
|
|
411
|
+
# a bridge checks its one Range twice, `BrHalf4W`/`BrFull6W` each of their two
|
|
412
|
+
# (oracle, CR6/CR1000X, round 59)
|
|
413
|
+
for pos in ranges[kw] if isinstance(ranges[kw], list) else [ranges[kw]]:
|
|
414
|
+
_check_range_code(stmt, measure, pos, env, diags)
|
|
415
|
+
if kw in fn_pos:
|
|
416
|
+
_check_fnotch_min(stmt, fnotch, fn_pos[kw], env, diags, scan_time.MODELS[profile.name].integ_num)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
_INF_NAMES = {"INF": float("inf"), "NAN": float("nan")}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _fnotch_value(arg: S.Expr | None, env: consteval.Env):
|
|
423
|
+
"""Folded `fN1` value; `NAN`/`INF` (not named constants in `consteval`) handled here."""
|
|
424
|
+
inner, sign = arg, 1.0
|
|
425
|
+
while isinstance(inner, S.Unary) and inner.op == "-":
|
|
426
|
+
inner, sign = inner.operand, -sign
|
|
427
|
+
if isinstance(inner, S.Name) and inner.name.upper() in _INF_NAMES and inner.name.upper() not in env.consts:
|
|
428
|
+
return sign * _INF_NAMES[inner.name.upper()]
|
|
429
|
+
return _const_value(arg, env)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _check_fnotch_min(
|
|
433
|
+
stmt: S.Instruction, cfg: dict, pos: int, env: consteval.Env, diags: Diagnostics, integ_num: float
|
|
434
|
+
) -> None:
|
|
435
|
+
"""`Invalid fnotch, minimum allowed is N Hz.` (CR6 2, CR1000X .5; docs/instructions.md). Zero
|
|
436
|
+
(also as a float, `1E-50`) and +-INF pass, NAN, strings and values under the logger's minimum
|
|
437
|
+
fail; there is no maximum. A negative value fails when its integration code
|
|
438
|
+
`int(integ_num / fN1 + 0.5)` is negative, so a large one passes (CR6 `-1E5`, round 31)."""
|
|
439
|
+
from .scan_time import _f32
|
|
440
|
+
|
|
441
|
+
idx = pos - 1
|
|
442
|
+
if idx >= len(stmt.args) or stmt.args[idx] is None:
|
|
443
|
+
return
|
|
444
|
+
value = _fnotch_value(stmt.args[idx], env)
|
|
445
|
+
if value is None:
|
|
446
|
+
return
|
|
447
|
+
if isinstance(value, str):
|
|
448
|
+
bad = True
|
|
449
|
+
else:
|
|
450
|
+
value = float(value)
|
|
451
|
+
f = _f32(value)
|
|
452
|
+
bad = f != f or (
|
|
453
|
+
f != 0
|
|
454
|
+
and abs(f) != float("inf")
|
|
455
|
+
and (value < cfg["min_hz"] if f > 0 else _f32(_f32(integ_num) / f) <= -1.5)
|
|
456
|
+
)
|
|
457
|
+
if bad:
|
|
458
|
+
times = 2 if stmt.keyword.upper() in cfg.get("twice", ()) else 1
|
|
459
|
+
for _ in range(times):
|
|
460
|
+
diags.error(stmt.line, cfg["message"], stmt.file)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _const_value(arg: S.Expr | None, env: consteval.Env):
|
|
464
|
+
"""The folded constant value of `arg`, or `None` if it is missing, a variable, or otherwise
|
|
465
|
+
not resolvable at compile time (in every case, some *other* check's concern, not this one's --
|
|
466
|
+
matching `terminals.py`'s own convention)."""
|
|
467
|
+
if arg is None or not consteval.is_constant(arg, env):
|
|
468
|
+
return None
|
|
469
|
+
try:
|
|
470
|
+
return consteval.value_of(arg, env)
|
|
471
|
+
except consteval.NotConstant:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _int_value(arg: S.Expr | None, env: consteval.Env) -> int | None:
|
|
476
|
+
v = _const_value(arg, env)
|
|
477
|
+
if isinstance(v, int): # (a huge integer literal: `math.isfinite` would overflow, round 69)
|
|
478
|
+
return v
|
|
479
|
+
return int(v) if isinstance(v, float) and math.isfinite(v) else None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _check_gps_nmea(stmt: S.Instruction, program: S.Program, env: consteval.Env, diags: Diagnostics) -> None:
|
|
483
|
+
"""`GPS`'s optional fifth argument must be a String variable of at least 96 characters
|
|
484
|
+
(declared size; 24 when none is given); docs/instructions.md#gps-ports."""
|
|
485
|
+
if len(stmt.args) < 5 or stmt.args[4] is None:
|
|
486
|
+
return
|
|
487
|
+
arg = stmt.args[4]
|
|
488
|
+
if isinstance(arg, S.String):
|
|
489
|
+
bad = True
|
|
490
|
+
elif isinstance(arg, (S.Name, S.Index)):
|
|
491
|
+
decl = next(
|
|
492
|
+
(
|
|
493
|
+
v
|
|
494
|
+
for st in reversed(symbols.scoped_view(program, stmt).statements)
|
|
495
|
+
if isinstance(st, S.Declaration) and st.kind in ("PUBLIC", "DIM", "STATIC")
|
|
496
|
+
for v in st.vars
|
|
497
|
+
if v.name.upper() == arg.name.upper()
|
|
498
|
+
),
|
|
499
|
+
None,
|
|
500
|
+
)
|
|
501
|
+
if decl is None:
|
|
502
|
+
return
|
|
503
|
+
if (decl.type or "FLOAT").upper() != "STRING":
|
|
504
|
+
bad = (decl.type or "FLOAT").upper() in ("FLOAT", "LONG", "BOOLEAN", "DOUBLE")
|
|
505
|
+
else:
|
|
506
|
+
size_expr = decl.string_size
|
|
507
|
+
if isinstance(size_expr, S.Binary) and size_expr.op == "=":
|
|
508
|
+
size_expr = size_expr.left # `As String * 16 = {...}` parses the initializer in
|
|
509
|
+
size = _int_value(size_expr, env) if size_expr is not None else 24
|
|
510
|
+
bad = size is not None and size < 96
|
|
511
|
+
else:
|
|
512
|
+
return
|
|
513
|
+
if bad:
|
|
514
|
+
diags.error(stmt.line, "Optional parameter must be an array of type string of length >= 100", stmt.file)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _check_gps_port(stmt: S.Instruction, cfg: dict, env: consteval.Env, diags: Diagnostics) -> None:
|
|
518
|
+
"""GPS port sign selects PPS or NMEA synchronization; docs/instructions.md#gps-ports."""
|
|
519
|
+
if len(stmt.args) < 2:
|
|
520
|
+
return
|
|
521
|
+
port = _int_value(stmt.args[1], env)
|
|
522
|
+
if port is None:
|
|
523
|
+
return
|
|
524
|
+
if abs(port) not in cfg["ports"]:
|
|
525
|
+
diags.error(stmt.line, cfg["port_message"], stmt.file)
|
|
526
|
+
elif port > 0 and port != cfg["pps_port"]:
|
|
527
|
+
diags.error(stmt.line, cfg["pps_message"], stmt.file)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# --- Range codes: `Invalid range code, valid options are: mV5000, mV1000, mV200, or AutoRange.`
|
|
531
|
+
# (CR6/CR1000X only; CR300/CR350 have no such check -- their own `mv5000`/`mv200` etc. are simply
|
|
532
|
+
# not symbols at all, an unrelated, already-handled undefined-symbol rejection, `docs/
|
|
533
|
+
# instructions.md`). Fires for `VoltSe`/`VoltDiff`/`TCSe`/`TCDiff`/`CurrentSe`'s own `Range`
|
|
534
|
+
# argument (position 3 on all five) when the folded value is not one of the CHM's documented
|
|
535
|
+
# "mV5000/mV1000/mV200/AutoRange" family, including their open-detection `...C` siblings.
|
|
536
|
+
def _check_range_code(stmt: S.Instruction, measure: dict, pos: int, env: consteval.Env, diags: Diagnostics) -> None:
|
|
537
|
+
idx = pos - 1
|
|
538
|
+
if idx >= len(stmt.args):
|
|
539
|
+
return
|
|
540
|
+
value = _const_value(stmt.args[idx], env)
|
|
541
|
+
if value is None:
|
|
542
|
+
return
|
|
543
|
+
valid = measure["range_codes"]
|
|
544
|
+
# any string is invalid, `"800"` too (oracle, CR6/CR1000X, round 58)
|
|
545
|
+
if isinstance(value, str) or int(value) not in valid:
|
|
546
|
+
diags.error(stmt.line, measure["range_code_message"], stmt.file)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# --- `SW12`: `Invalid SW12 port %d.` -- an exact, small, per-logger set of valid ports (both a
|
|
550
|
+
# plain-integer slot number and the named `SW12_n` constant resolve to different raw values but
|
|
551
|
+
# both compile; every other value in the probed neighbourhood fails), `docs/instructions.md`.
|
|
552
|
+
def _check_sw12(stmt: S.Instruction, measure: dict, env: consteval.Env, diags: Diagnostics) -> None:
|
|
553
|
+
if not stmt.args:
|
|
554
|
+
return
|
|
555
|
+
value = _int_value(stmt.args[0], env)
|
|
556
|
+
if isinstance(stmt.args[0], S.String) and measure.get("string_port_int_min"):
|
|
557
|
+
value = -2147483648 # (`SW12("x",0)`, oracle CR6/CR350 2026-10-12: a string folds to INT_MIN)
|
|
558
|
+
if value is None:
|
|
559
|
+
return
|
|
560
|
+
if value not in measure["sw12_ports"]:
|
|
561
|
+
text = measure.get("sw12_port_messages", {}).get(str(value), f"Invalid SW12 port {value}.")
|
|
562
|
+
diags.error(stmt.line, text, stmt.file)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# --- `Delay`: `Delay(Option, Interval, Units)`. Three checks, oracle-confirmed per logger
|
|
566
|
+
# (`docs/instructions.md`):
|
|
567
|
+
# - `Illegal option %d` (all four loggers) when `Option` is not 0, 1 or 2 -- shown value is the
|
|
568
|
+
# folded number truncated to an unsigned byte (oracle: `Option=-1` shows as `255`).
|
|
569
|
+
# - `Delay interval cannot be of type String.` (all four loggers, any `Option`) when `Interval`
|
|
570
|
+
# folds to a string constant.
|
|
571
|
+
# - Depending on `Option` (0 = measurement-task-sequence delay, 1 = processing delay, 2 = SDM
|
|
572
|
+
# delay): `Option=0` needs `Interval` to itself be a compile-time constant
|
|
573
|
+
# (`Measurement delay must have constant interval.`) and, once resolved, at most 60 minutes
|
|
574
|
+
# (`Measurement Delay Exceeds 60 min.`) -- both CR6/CR1000X only, oracle-confirmed absent on
|
|
575
|
+
# CR300/CR350 (`Delay(0,61,min)` compiles clean there); `Option=1` needs the resolved interval
|
|
576
|
+
# to be strictly less than 24 hours (`Max Delay is 24 Hours`, all four loggers); `Option=2`
|
|
577
|
+
# needs `Interval` to be a compile-time constant (`SDM Delay interval must be constant.`, all
|
|
578
|
+
# four loggers).
|
|
579
|
+
_DELAY_US_PER_UNIT = {0: 1, 1: 1_000, 2: 1_000_000, 3: 60_000_000, 4: 3_600_000_000, 5: 86_400_000_000}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _delay_option_byte(value: int) -> int:
|
|
583
|
+
"""`Option` is taken as an unsigned byte (256 acts as 0, 257 as 1, -254 as 2). A constant
|
|
584
|
+
outside the Long range is a float32: its low byte is 0, or 255 above the float32 maximum
|
|
585
|
+
(oracle, all four loggers; docs/instructions.md#delay). Not modelled: CR300/CR350 report
|
|
586
|
+
255 for exactly 2147483648."""
|
|
587
|
+
if value > 3.4028235e38:
|
|
588
|
+
return 255
|
|
589
|
+
if not -(2**31) <= value < 2**31:
|
|
590
|
+
return 0
|
|
591
|
+
return value % 256
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# `Program too big for task memory` repeats for every further task word until the 4096-byte
|
|
595
|
+
# message buffer is full (`compiler.py`); 100 copies always fill it
|
|
596
|
+
_TASK_MEMORY_COPIES = 100
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _task_memory_full(stmt: S.Instruction, diags: Diagnostics) -> None:
|
|
600
|
+
for _ in range(_TASK_MEMORY_COPIES):
|
|
601
|
+
diags.error(stmt.line, "Program too big for task memory", stmt.file)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _check_delay(
|
|
605
|
+
stmt: S.Instruction, cfg: dict, env: consteval.Env, diags: Diagnostics, sequential: bool = False, slow: bool = False
|
|
606
|
+
) -> None:
|
|
607
|
+
args = stmt.args
|
|
608
|
+
option_arg = args[0] if len(args) > 0 else None
|
|
609
|
+
interval_arg = args[1] if len(args) > 1 else None
|
|
610
|
+
units_arg = args[2] if len(args) > 2 else None
|
|
611
|
+
|
|
612
|
+
option = _int_value(option_arg, env)
|
|
613
|
+
if option is not None:
|
|
614
|
+
option = _delay_option_byte(option)
|
|
615
|
+
if option not in (0, 1, 2):
|
|
616
|
+
diags.error(stmt.line, f"Illegal option {option}", stmt.file)
|
|
617
|
+
|
|
618
|
+
interval_value = _const_value(interval_arg, env)
|
|
619
|
+
if isinstance(interval_value, str):
|
|
620
|
+
diags.error(stmt.line, "Delay interval cannot be of type String.", stmt.file)
|
|
621
|
+
return # oracle: the string-type rejection is the only diagnostic for a string interval.
|
|
622
|
+
|
|
623
|
+
if option == 0 and cfg.get("option0_limit"):
|
|
624
|
+
if sequential:
|
|
625
|
+
# SequentialMode checks neither the interval nor its expansion (oracle, CR6, round 57)
|
|
626
|
+
return
|
|
627
|
+
if interval_arg is not None and not consteval.is_constant(interval_arg, env):
|
|
628
|
+
diags.error(stmt.line, "Measurement delay must have constant interval.", stmt.file)
|
|
629
|
+
return
|
|
630
|
+
unit = _const_value(units_arg, env)
|
|
631
|
+
if (
|
|
632
|
+
not isinstance(interval_value, (int, float))
|
|
633
|
+
or isinstance(interval_value, bool)
|
|
634
|
+
or not math.isfinite(interval_value)
|
|
635
|
+
or not isinstance(unit, int)
|
|
636
|
+
or isinstance(unit, bool)
|
|
637
|
+
or unit not in _DELAY_US_PER_UNIT
|
|
638
|
+
):
|
|
639
|
+
return
|
|
640
|
+
# the unsigned 32-bit microsecond count, so a negative interval is too long as well
|
|
641
|
+
# (oracle, CR6/CR1000X, rounds 35 and 57; docs/checker.md#task-memory-overflow-of-long-delays-round-57)
|
|
642
|
+
us = scan_time.delay_us(interval_value, unit)
|
|
643
|
+
if us > cfg["option0_limit"]:
|
|
644
|
+
diags.error(stmt.line, "Measurement Delay Exceeds 60 min.", stmt.file)
|
|
645
|
+
if cfg.get("task_memory") and scan_time.task_memory_overflow(us):
|
|
646
|
+
_task_memory_full(stmt, diags)
|
|
647
|
+
elif option == 1 and cfg.get("option1_limit"):
|
|
648
|
+
us = _delay_interval_us(interval_arg, units_arg, env)
|
|
649
|
+
if us is not None and us >= cfg["option1_limit"]:
|
|
650
|
+
diags.error(stmt.line, "Max Delay is 24 Hours", stmt.file)
|
|
651
|
+
elif option == 2 and cfg.get("option2_constant", True):
|
|
652
|
+
if interval_arg is not None and not consteval.is_constant(interval_arg, env):
|
|
653
|
+
diags.error(stmt.line, "SDM Delay interval must be constant.", stmt.file)
|
|
654
|
+
elif interval_value == 0 and cfg.get("option0_limit"): # (CR6/CR1000X data only)
|
|
655
|
+
# `Delay(2,0,Sec)` (oracle, CR6/CR1000X, 2026-10-25, round 35; 1 and -1 compile)
|
|
656
|
+
diags.error(stmt.line, "Invalid time specified.", stmt.file)
|
|
657
|
+
elif slow and cfg.get("sdm_slow_sequence"):
|
|
658
|
+
# written after `SlowSequence` in the program body, also in SequentialMode and in a
|
|
659
|
+
# one-line If; a Sub it calls is not checked (oracle, CR6/CR1000X, round 64,
|
|
660
|
+
# docs/checker.md#sdm-delay-in-a-slow-sequence-round-64)
|
|
661
|
+
diags.error(stmt.line, "SDM Delay Not Allowed in Slow Sequence.", stmt.file)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _delay_interval_us(interval_arg, units_arg, env: consteval.Env) -> float | None:
|
|
665
|
+
value = _const_value(interval_arg, env)
|
|
666
|
+
unit = _const_value(units_arg, env)
|
|
667
|
+
if not consteval.is_finite_number(value) or not consteval.is_finite_number(unit) or value < 0:
|
|
668
|
+
return None
|
|
669
|
+
if value >= 2**31: # outside the Long range: no limit applies (oracle, CR6/CR1000X, round 16)
|
|
670
|
+
return None
|
|
671
|
+
us_per_unit = _DELAY_US_PER_UNIT.get(int(unit))
|
|
672
|
+
return value * us_per_unit if us_per_unit is not None else None
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
# --- `CS616`: `Invalid reps %d, must be > 0` for a non-positive `Reps` (CR6 only, oracle-
|
|
676
|
+
# confirmed; not chased on CR1000X/CR300/CR350 this round, `docs/instructions.md`).
|
|
677
|
+
def _check_cs616_reps(stmt: S.Instruction, measure: dict, env: consteval.Env, diags: Diagnostics) -> None:
|
|
678
|
+
pos = measure["cs616_min_reps_pos"]
|
|
679
|
+
idx = pos - 1
|
|
680
|
+
if idx >= len(stmt.args):
|
|
681
|
+
return
|
|
682
|
+
value = _int_value(stmt.args[idx], env)
|
|
683
|
+
if value is not None and value <= 0:
|
|
684
|
+
diags.error(stmt.line, f"Invalid reps {value}, must be > 0", stmt.file)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# --- `ExciteV`/`ExciteI`: `Excitation exceeds 2500.000000.` when `|ExmV|`/`|ExmI|` > 2500 (CR6
|
|
688
|
+
# only, oracle-confirmed; CR1000X accepts at least 2501 mV with no error, so this is deliberately
|
|
689
|
+
# not applied there -- `docs/instructions.md`). `ExciteI` does not exist on CR1000X/CR300/CR350 at
|
|
690
|
+
# all (`docs/loggers.md`), so this only ever matches on CR6 regardless.
|
|
691
|
+
def _check_excite_magnitude(stmt: S.Instruction, measure: dict, env: consteval.Env, diags: Diagnostics) -> None:
|
|
692
|
+
pos = measure["excite_magnitude_pos"]
|
|
693
|
+
idx = pos - 1
|
|
694
|
+
if idx >= len(stmt.args):
|
|
695
|
+
return
|
|
696
|
+
value = _const_value(stmt.args[idx], env)
|
|
697
|
+
if isinstance(value, (int, float)) and abs(value) > measure["excite_max_mv"]:
|
|
698
|
+
diags.error(stmt.line, f"Excitation exceeds {float(measure['excite_max_mv']):.6f}.", stmt.file)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
_UNIT_NAMES = {1: "mSec", 2: "Sec", 3: "Min"}
|
|
702
|
+
_OPENERS = frozenset({"SELECT", "WHILE"})
|
|
703
|
+
_CLOSERS = frozenset({"ENDIF", "ENDSELECT", "NEXTSUBSCAN", "WEND"})
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def is_sequential(program: S.Program, profile: Profile, env: consteval.Env) -> bool:
|
|
707
|
+
"""`mode.decide` == "Sequential"; a failure is left for the compile's own `mode.decide` call
|
|
708
|
+
to record (here it reads as Pipeline)."""
|
|
709
|
+
from .. import mode
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
return (
|
|
713
|
+
mode.decide(
|
|
714
|
+
program.statements,
|
|
715
|
+
profile,
|
|
716
|
+
lambda e: consteval.is_constant(e, env),
|
|
717
|
+
lambda e: consteval.value_of(e, env),
|
|
718
|
+
)
|
|
719
|
+
== "Sequential"
|
|
720
|
+
)
|
|
721
|
+
except Exception:
|
|
722
|
+
return False
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def check_scan_delay_budget(
|
|
726
|
+
program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env, zero: bool = False
|
|
727
|
+
) -> None:
|
|
728
|
+
"""CR6/CR1000X: the measurement time of a main `Scan` (`checker/scan_time.py`, 100 us units)
|
|
729
|
+
above its interval -> `Scan interval too short for measurement by %d %s.` at `NextScan`, the
|
|
730
|
+
excess rounded up to the scan's own unit (`Scan(50,mSec)` + `Delay(0,1,Sec)` -> `by 951 mSec`;
|
|
731
|
+
`Scan(1,Sec)` + `Delay(0,1,Sec)` -> `by 1 Sec`; a `SubScan(0,0,n)` body counts n times).
|
|
732
|
+
A `SubScan(Interval,...)` body longer than its interval gives `SubScan interval too short for
|
|
733
|
+
measurement by %d %s.` at its `NextSubScan`. A Sec/Min `Scan` whose interval is 0 or past
|
|
734
|
+
`INT32_MAX` counts as interval 0. The zero-interval scans (`zero=True`) are reported even
|
|
735
|
+
though the scan's own parameter error already failed the compile, so the caller runs them
|
|
736
|
+
ahead of the error-gated tier C. SequentialMode never reports it. docs/instructions.md "Scan
|
|
737
|
+
time budget"."""
|
|
738
|
+
if profile.name not in ("cr6", "cr1000x"):
|
|
739
|
+
return
|
|
740
|
+
from . import scan_time
|
|
741
|
+
|
|
742
|
+
# SequentialMode never reports it (oracle, CR6/CR1000X, 2026-10-08; docs/fuzz-findings.md).
|
|
743
|
+
if is_sequential(program, profile, env):
|
|
744
|
+
return
|
|
745
|
+
for b in scan_time.budgets(program, env, profile, include_zero=zero):
|
|
746
|
+
if (b.interval == 0) != zero:
|
|
747
|
+
continue
|
|
748
|
+
for stmt, amount, unit in b.sub_short:
|
|
749
|
+
diags.error(
|
|
750
|
+
stmt.line,
|
|
751
|
+
f"SubScan interval too short for measurement by {amount} {scan_time.UNIT_NAMES[unit]}.",
|
|
752
|
+
stmt.file,
|
|
753
|
+
)
|
|
754
|
+
if b.total % 2**32 > b.interval: # unsigned: a negative total also fails (round 31)
|
|
755
|
+
amount = scan_time.amount(scan_time.i32(b.total - b.interval), b.msg_unit)
|
|
756
|
+
diags.error(
|
|
757
|
+
b.end.line,
|
|
758
|
+
f"Scan interval too short for measurement by {amount} {scan_time.UNIT_NAMES[b.msg_unit]}.",
|
|
759
|
+
b.end.file,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def check_cpi_time(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
764
|
+
"""CR6/CR1000X: a `Scan` shorter than the CPI measurement time of a module -> `Insufficient
|
|
765
|
+
time (%d ms) for measurements from CPI address %d (needs at least %d ms).`, on the program's
|
|
766
|
+
last line, only when the compile has no other error (docs/instructions.md "CPI measurement
|
|
767
|
+
time"). The binary walks the module list in order of first use; for each module it totals the
|
|
768
|
+
module's time in every scan holding CDM instructions, keeps per scan the (first) module with
|
|
769
|
+
the largest total so far, and then reports every such scan whose interval is below that
|
|
770
|
+
figure, so a program with k modules can print up to k lines per scan, naming the leader so far.
|
|
771
|
+
Modelled: `CDM_TCComp(TEMP120, ...)`, `Reps*430+275` microseconds rounded up to 100 us per
|
|
772
|
+
statement, summed per address and rounded up to whole ms. Every occurrence counts once wherever
|
|
773
|
+
it sits (loops, `If` branches, `Select` cases in SequentialMode). A `SubScan(Interval,Units,n)`
|
|
774
|
+
body counts n times at `Interval` spacing (zero: its own length); an interval shorter than the
|
|
775
|
+
body gives `SubScan interval too short for measurements from CPI address %d by %d uSec.` first.
|
|
776
|
+
Programs with any other CDM instruction, an unreadable instruction or a SubScan form not
|
|
777
|
+
probed are skipped whole."""
|
|
778
|
+
if profile.name not in ("cr6", "cr1000x") or not program.statements:
|
|
779
|
+
return
|
|
780
|
+
from .instr_rules import _US_PER_UNIT, _scan_raw
|
|
781
|
+
|
|
782
|
+
scans: list[dict] = [] # {"ms": interval, "times": {addr: 100 us units}, "short": [(addr, by)]}
|
|
783
|
+
order: list[int] = [] # CPI addresses in order of first use
|
|
784
|
+
scan: dict | None = None
|
|
785
|
+
sub: tuple[int, int] | None = None # open SubScan: (count, interval in 100 us units)
|
|
786
|
+
sub_start: dict[int, int] = {}
|
|
787
|
+
sequential = is_sequential(program, profile, env)
|
|
788
|
+
depth = 0 # open If/For/Do/Select blocks
|
|
789
|
+
items: list[tuple[S.Stmt, bool]] = []
|
|
790
|
+
for stmt in program.statements:
|
|
791
|
+
kw = stmt.keyword.upper() if isinstance(stmt, S.Instruction) else ""
|
|
792
|
+
text = stmt.text or ""
|
|
793
|
+
if (
|
|
794
|
+
isinstance(stmt, S.For)
|
|
795
|
+
or (isinstance(stmt, S.DoLoop) and stmt.keyword == "DO")
|
|
796
|
+
or (isinstance(stmt, S.If) and stmt.inline is None)
|
|
797
|
+
or kw in _OPENERS
|
|
798
|
+
):
|
|
799
|
+
items.append((stmt, depth > 0))
|
|
800
|
+
depth += 1
|
|
801
|
+
continue
|
|
802
|
+
if (
|
|
803
|
+
(kw == "NEXT" and not re.match(r"(?i)\s*Next\s+(Sub)?Scan\b", text))
|
|
804
|
+
or (isinstance(stmt, S.DoLoop) and stmt.keyword == "LOOP")
|
|
805
|
+
or kw in _CLOSERS - {"NEXTSUBSCAN"}
|
|
806
|
+
or (kw == "END" and re.match(r"(?i)\s*End\s+(If|Select)\b", text))
|
|
807
|
+
):
|
|
808
|
+
depth = max(depth - 1, 0)
|
|
809
|
+
items.append((stmt, depth > 0))
|
|
810
|
+
if isinstance(stmt, S.If):
|
|
811
|
+
items += [(child, True) for child in (stmt.inline or []) + (stmt.inline_else or [])]
|
|
812
|
+
for stmt, conditional in items:
|
|
813
|
+
kw = stmt.keyword.upper() if isinstance(stmt, S.Instruction) else ""
|
|
814
|
+
text = stmt.text or ""
|
|
815
|
+
if kw == "NEXT" and re.match(r"(?i)\s*Next\s+Scan\b", text):
|
|
816
|
+
kw = "NEXTSCAN"
|
|
817
|
+
elif kw == "NEXT" and re.match(r"(?i)\s*Next\s+SubScan\b", text):
|
|
818
|
+
kw = "NEXTSUBSCAN"
|
|
819
|
+
if kw.startswith("CDM_") and kw != "CDM_TCCOMP" or kw == "CPIADDMODULE2":
|
|
820
|
+
return
|
|
821
|
+
if isinstance(stmt, S.Instruction) and (kw == "SCAN"):
|
|
822
|
+
raw = _scan_raw(stmt, env)
|
|
823
|
+
if raw is None:
|
|
824
|
+
return
|
|
825
|
+
scan = {"ms": raw[0] * _US_PER_UNIT[raw[1]] // 1000, "times": {}, "short": []}
|
|
826
|
+
scans.append(scan)
|
|
827
|
+
elif scan is None:
|
|
828
|
+
continue
|
|
829
|
+
elif kw == "NEXTSCAN":
|
|
830
|
+
if sub is not None:
|
|
831
|
+
return
|
|
832
|
+
scan = None
|
|
833
|
+
elif isinstance(stmt, S.Instruction) and (kw == "SUBSCAN"):
|
|
834
|
+
args = [_value(a, env) for a in stmt.args]
|
|
835
|
+
count = args[2] if len(args) == 3 else None
|
|
836
|
+
step = 0
|
|
837
|
+
if not isinstance(count, int) or isinstance(count, bool) or count < 1 or sub is not None:
|
|
838
|
+
return
|
|
839
|
+
if args[0] != 0:
|
|
840
|
+
unit = _US_PER_UNIT.get(args[1]) if isinstance(args[1], int) else None
|
|
841
|
+
if unit is None or not consteval.is_finite_number(args[0]) or args[0] <= 0:
|
|
842
|
+
return
|
|
843
|
+
step = int(args[0]) * unit // 100
|
|
844
|
+
if sequential and step <= 10: # "SequentialMode SubScan interval must be > 1 mS."
|
|
845
|
+
return
|
|
846
|
+
if step * count > scan["ms"] * 10: # "Product of SubScan interval and iterations ..."
|
|
847
|
+
return
|
|
848
|
+
sub = (count, step)
|
|
849
|
+
sub_start = dict(scan["times"])
|
|
850
|
+
elif kw == "NEXTSUBSCAN":
|
|
851
|
+
if sub is None:
|
|
852
|
+
return
|
|
853
|
+
count, step = sub
|
|
854
|
+
for addr, units in list(scan["times"].items()):
|
|
855
|
+
length = units - sub_start.get(addr, 0)
|
|
856
|
+
if length > 0:
|
|
857
|
+
if step and length > step:
|
|
858
|
+
scan["short"].append((addr, (length - step) * 100))
|
|
859
|
+
scan["times"][addr] = units + (step or length) * (count - 1)
|
|
860
|
+
sub = None
|
|
861
|
+
elif isinstance(stmt, S.Instruction) and (kw == "CDM_TCCOMP"):
|
|
862
|
+
module, addr, reps = (_int_value(stmt.args[i], env) if i < len(stmt.args) else None for i in (0, 1, 3))
|
|
863
|
+
if module != 224 or addr is None or not 1 <= addr <= 120 or reps is None or reps < 1:
|
|
864
|
+
return
|
|
865
|
+
if conditional and not sequential: # "not allowed inside of a conditional statement"
|
|
866
|
+
return
|
|
867
|
+
if addr not in order:
|
|
868
|
+
order.append(addr)
|
|
869
|
+
scan["times"][addr] = scan["times"].get(addr, 0) + -(-(reps * 430 + 275) // 100)
|
|
870
|
+
last = program.statements[-1].line
|
|
871
|
+
leader: dict[int, tuple[int, int]] = {} # scan index -> (ms needed, address)
|
|
872
|
+
for addr in order:
|
|
873
|
+
for i, sc in enumerate(scans):
|
|
874
|
+
if not sc["times"]:
|
|
875
|
+
continue
|
|
876
|
+
for a, by in sc["short"]:
|
|
877
|
+
if a == addr:
|
|
878
|
+
diags.error(last, f"SubScan interval too short for measurements from CPI address {a} by {by} uSec.")
|
|
879
|
+
need = -(-sc["times"].get(addr, 0) // 10)
|
|
880
|
+
if need > leader.get(i, (0, 0))[0]:
|
|
881
|
+
leader[i] = (need, addr)
|
|
882
|
+
for i, sc in enumerate(scans):
|
|
883
|
+
if i in leader and sc["ms"] < leader[i][0]:
|
|
884
|
+
diags.error(
|
|
885
|
+
last,
|
|
886
|
+
f"Insufficient time ({sc['ms']} ms) for measurements from CPI "
|
|
887
|
+
f"address {leader[i][1]} (needs at least {leader[i][0]} ms).",
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _value(expr: S.Expr | None, env: consteval.Env) -> object | None:
|
|
892
|
+
if expr is None or not consteval.is_constant(expr, env):
|
|
893
|
+
return None
|
|
894
|
+
try:
|
|
895
|
+
return consteval.value_of(expr, env)
|
|
896
|
+
except consteval.NotConstant:
|
|
897
|
+
return None
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
# `Reps` (the second argument) of measurement instructions, oracle-probed 2026-10-01 (the decompiled
|
|
901
|
+
# CR6 check `FUN_005163b0` is shared by 23 instructions; only the ones probed on a logger are
|
|
902
|
+
# listed). CR6/CR1000X: `Invalid reps %d, must be > 0` (no period) for Reps <= 0, and `Variable %s
|
|
903
|
+
# out of bounds.` when the Float destination cannot hold the output count (also for negative
|
|
904
|
+
# output counts). Optional outputs adjust that count below. CR300/CR350: `Invalid reps 0, must
|
|
905
|
+
# be > 0.` for Reps 0 only.
|
|
906
|
+
# CR300/CR350 (round 31): all of these check the destination like VoltSe (round 30); Reps <= 0
|
|
907
|
+
# is `Invalid reps %d, must be > 0.`, except a negative Reps on Therm/PeriodAvg, which instead
|
|
908
|
+
# (with PulseCount) overruns the destination.
|
|
909
|
+
_SINGLE_PASS_REPS = frozenset(
|
|
910
|
+
{
|
|
911
|
+
"VOLTSE",
|
|
912
|
+
"VOLTDIFF",
|
|
913
|
+
"TCSE",
|
|
914
|
+
"TCDIFF",
|
|
915
|
+
"BRHALF",
|
|
916
|
+
"BRHALF3W",
|
|
917
|
+
"BRHALF4W",
|
|
918
|
+
"BRFULL",
|
|
919
|
+
"BRFULL6W",
|
|
920
|
+
"THERM107",
|
|
921
|
+
"THERM108",
|
|
922
|
+
"THERM109",
|
|
923
|
+
"CURRENTSE",
|
|
924
|
+
"PERIODAVG",
|
|
925
|
+
"PULSECOUNT",
|
|
926
|
+
}
|
|
927
|
+
)
|
|
928
|
+
_NEGATIVE_REPS_SILENT = frozenset({"THERM107", "THERM108", "THERM109", "PERIODAVG"})
|
|
929
|
+
_NEGATIVE_REPS_DEST = frozenset({"THERM107", "THERM108", "THERM109", "PERIODAVG", "PULSECOUNT"})
|
|
930
|
+
_REPS_CHECKED = {
|
|
931
|
+
"cr6": frozenset(
|
|
932
|
+
{
|
|
933
|
+
"VOLTSE",
|
|
934
|
+
"VOLTDIFF",
|
|
935
|
+
"TCDIFF",
|
|
936
|
+
"TCSE",
|
|
937
|
+
"BRHALF",
|
|
938
|
+
"BRHALF3W",
|
|
939
|
+
"BRHALF4W",
|
|
940
|
+
"BRFULL",
|
|
941
|
+
"BRFULL6W",
|
|
942
|
+
"RESISTANCE",
|
|
943
|
+
"RESISTANCE2W",
|
|
944
|
+
"RESISTANCE3W",
|
|
945
|
+
"PULSECOUNT",
|
|
946
|
+
"CS616",
|
|
947
|
+
"PERIODAVG",
|
|
948
|
+
"CURRENTSE",
|
|
949
|
+
"THERM107",
|
|
950
|
+
"THERM108",
|
|
951
|
+
"THERM109",
|
|
952
|
+
}
|
|
953
|
+
),
|
|
954
|
+
"cr1000x": frozenset(
|
|
955
|
+
{
|
|
956
|
+
"VOLTSE",
|
|
957
|
+
"VOLTDIFF",
|
|
958
|
+
"TCDIFF",
|
|
959
|
+
"TCSE",
|
|
960
|
+
"CURRENTSE",
|
|
961
|
+
"PERIODAVG",
|
|
962
|
+
"PULSECOUNT",
|
|
963
|
+
"BRHALF",
|
|
964
|
+
"BRHALF3W",
|
|
965
|
+
"BRHALF4W",
|
|
966
|
+
"BRFULL",
|
|
967
|
+
"BRFULL6W",
|
|
968
|
+
"CS616",
|
|
969
|
+
"THERM107",
|
|
970
|
+
"THERM108",
|
|
971
|
+
"THERM109",
|
|
972
|
+
}
|
|
973
|
+
),
|
|
974
|
+
"cr300": _SINGLE_PASS_REPS,
|
|
975
|
+
"cr350": _SINGLE_PASS_REPS,
|
|
976
|
+
}
|
|
977
|
+
_REPS_DEST_SIZE = frozenset(
|
|
978
|
+
{
|
|
979
|
+
"VOLTSE",
|
|
980
|
+
"VOLTDIFF",
|
|
981
|
+
"TCDIFF",
|
|
982
|
+
"TCSE",
|
|
983
|
+
"CURRENTSE",
|
|
984
|
+
"PERIODAVG",
|
|
985
|
+
"BRHALF",
|
|
986
|
+
"BRHALF3W",
|
|
987
|
+
"BRHALF4W",
|
|
988
|
+
"BRFULL",
|
|
989
|
+
"BRFULL6W",
|
|
990
|
+
"RESISTANCE",
|
|
991
|
+
"RESISTANCE2W",
|
|
992
|
+
"RESISTANCE3W",
|
|
993
|
+
"PULSECOUNT",
|
|
994
|
+
"CS616",
|
|
995
|
+
}
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _measurement_output_count(stmt: S.Instruction, reps: int, env: consteval.Env) -> int:
|
|
1000
|
+
"""Optional extra outputs, from the bridge/resistance builders and round-25 oracle probes.
|
|
1001
|
+
|
|
1002
|
+
The option is an integer equal to 1, not a Boolean: True (-1) does not enable it.
|
|
1003
|
+
Bridges return one extra value per repetition; resistance returns one extra value overall.
|
|
1004
|
+
"""
|
|
1005
|
+
kw = stmt.keyword.upper()
|
|
1006
|
+
option_index = {"BRHALF4W": 14, "BRFULL6W": 14, "RESISTANCE": 13, "RESISTANCE2W": 10, "RESISTANCE3W": 10}.get(kw)
|
|
1007
|
+
if option_index is None or len(stmt.args) <= option_index:
|
|
1008
|
+
return reps
|
|
1009
|
+
if _int_value(stmt.args[option_index], env) != 1:
|
|
1010
|
+
return reps
|
|
1011
|
+
return reps * 2 if kw in {"BRHALF4W", "BRFULL6W"} else reps + 1
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _measurement_aliases(program: S.Program, tables, env: consteval.Env):
|
|
1015
|
+
"""Alias destinations: `(backing elements, start offset, diagnostic name, declared size)`,
|
|
1016
|
+
the size None for a bare alias name (rounds 26 and 30).
|
|
1017
|
+
|
|
1018
|
+
Bind at the alias declaration, so a later local shadow of its backing name does not
|
|
1019
|
+
change the storage.
|
|
1020
|
+
"""
|
|
1021
|
+
aliases = {}
|
|
1022
|
+
routine = None
|
|
1023
|
+
for st in program.statements:
|
|
1024
|
+
if isinstance(st, S.SubDef):
|
|
1025
|
+
routine = st
|
|
1026
|
+
elif S.ends_routine(st):
|
|
1027
|
+
routine = None
|
|
1028
|
+
if not isinstance(st, S.Instruction) or st.keyword.upper() != "ALIAS" or not st.args:
|
|
1029
|
+
continue
|
|
1030
|
+
target = st.args[0]
|
|
1031
|
+
if not isinstance(target, (S.Name, S.Index)):
|
|
1032
|
+
continue
|
|
1033
|
+
scope = tables(st)
|
|
1034
|
+
shape = scope.dims.get(target.name.upper())
|
|
1035
|
+
param = (
|
|
1036
|
+
next((p for p in routine.params if p.name.upper() == target.name.upper()), None)
|
|
1037
|
+
if routine is not None
|
|
1038
|
+
else None
|
|
1039
|
+
)
|
|
1040
|
+
if param is not None and (param.type or "FLOAT").upper() == "FLOAT":
|
|
1041
|
+
# a Float parameter's own header shape backs the alias (round 30 review, `Sub S(P(4))`)
|
|
1042
|
+
shape = tuple(_int_value(d, env) for d in param.dim_exprs) or (1,)
|
|
1043
|
+
if any(n is None or n < 1 for n in shape):
|
|
1044
|
+
continue
|
|
1045
|
+
elif shape is None or scope.types.get(target.name.upper()) != "FLOAT":
|
|
1046
|
+
continue
|
|
1047
|
+
# Alias declaration subscripts may be named constants.
|
|
1048
|
+
if isinstance(target, S.Index):
|
|
1049
|
+
indices = [_int_value(a, env) for a in target.args]
|
|
1050
|
+
if any(i is None for i in indices):
|
|
1051
|
+
continue
|
|
1052
|
+
indices = cast(list[int], indices)
|
|
1053
|
+
if len(indices) == 1:
|
|
1054
|
+
offset = indices[0] - 1
|
|
1055
|
+
elif len(indices) == len(shape) and all(1 <= i <= n for i, n in zip(indices, shape)):
|
|
1056
|
+
offset = 0
|
|
1057
|
+
for i, n in zip(indices, shape):
|
|
1058
|
+
offset = offset * n + i - 1
|
|
1059
|
+
else:
|
|
1060
|
+
continue
|
|
1061
|
+
else:
|
|
1062
|
+
offset = 0
|
|
1063
|
+
total = math.prod(shape)
|
|
1064
|
+
declared_names = {
|
|
1065
|
+
v.name.upper(): v.name
|
|
1066
|
+
for decl in symbols.scoped_view(program, st).statements
|
|
1067
|
+
if isinstance(decl, S.Declaration)
|
|
1068
|
+
for v in decl.vars
|
|
1069
|
+
}
|
|
1070
|
+
entries = {}
|
|
1071
|
+
for alias in st.args[1:]:
|
|
1072
|
+
if not isinstance(alias, (S.Name, S.Index)):
|
|
1073
|
+
break
|
|
1074
|
+
size = (
|
|
1075
|
+
1 if isinstance(alias, S.Name) else (_int_value(alias.args[0], env) if len(alias.args) == 1 else None)
|
|
1076
|
+
)
|
|
1077
|
+
if size is None or size < 1 or not 0 <= offset < total:
|
|
1078
|
+
break
|
|
1079
|
+
entries[alias.name.upper()] = (
|
|
1080
|
+
total,
|
|
1081
|
+
offset,
|
|
1082
|
+
declared_names.get(target.name.upper(), target.name) if offset == 0 else alias.name,
|
|
1083
|
+
None if isinstance(alias, S.Name) else size,
|
|
1084
|
+
)
|
|
1085
|
+
offset += size
|
|
1086
|
+
aliases[id(st)] = entries
|
|
1087
|
+
cache = {}
|
|
1088
|
+
parameters = {
|
|
1089
|
+
id(tables(st)): {p.name.upper() for p in st.params} for st in program.statements if isinstance(st, S.SubDef)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
def lookup(stmt, dest):
|
|
1093
|
+
if dest.name.upper() in tables(stmt).dims or dest.name.upper() in parameters.get(id(tables(stmt)), set()):
|
|
1094
|
+
return None
|
|
1095
|
+
key = id(tables(stmt))
|
|
1096
|
+
if key not in cache:
|
|
1097
|
+
visible = {}
|
|
1098
|
+
for st in symbols.scoped_view(program, stmt).statements:
|
|
1099
|
+
visible.update(aliases.get(id(st), {}))
|
|
1100
|
+
cache[key] = visible
|
|
1101
|
+
entry = cache[key].get(dest.name.upper())
|
|
1102
|
+
if entry is not None and isinstance(dest, S.Index) and entry[3] is None:
|
|
1103
|
+
return None # a subscripted bare alias is not modelled
|
|
1104
|
+
return entry
|
|
1105
|
+
|
|
1106
|
+
return lookup
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _alias_overrun(stmt: S.Instruction, dest, alias, count: int, profile: Profile, env: consteval.Env) -> bool:
|
|
1110
|
+
"""An alias destination's explicit index must lie within its declared size; the outputs
|
|
1111
|
+
must then fit the backing storage from the destination element (round 30). CR6/CR1000X
|
|
1112
|
+
count from the alias's own start, CR300/CR350 from the backing variable's start."""
|
|
1113
|
+
total, offset, _, size = alias
|
|
1114
|
+
index = 0
|
|
1115
|
+
if isinstance(dest, S.Index):
|
|
1116
|
+
k = _index_value(dest.args[0], env) if len(dest.args) == 1 else None
|
|
1117
|
+
if k is None:
|
|
1118
|
+
return False
|
|
1119
|
+
if not 1 <= k <= size:
|
|
1120
|
+
return True
|
|
1121
|
+
index = k - 1
|
|
1122
|
+
available = total - index - (0 if profile.single_pass else offset)
|
|
1123
|
+
return count < 0 or count > available
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def check_reps(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
1127
|
+
checked = _REPS_CHECKED.get(profile.name, frozenset())
|
|
1128
|
+
if not checked:
|
|
1129
|
+
return
|
|
1130
|
+
from . import symbols
|
|
1131
|
+
|
|
1132
|
+
tables = symbols.scope_tables(program, env, scalars=True, parameters=True)
|
|
1133
|
+
alias_storage = _measurement_aliases(program, tables, env)
|
|
1134
|
+
for stmt in program.statements:
|
|
1135
|
+
if not isinstance(stmt, S.Instruction) or stmt.keyword.upper() not in checked or len(stmt.args) < 2:
|
|
1136
|
+
continue
|
|
1137
|
+
scope = tables(stmt)
|
|
1138
|
+
reps = _int_value(stmt.args[1], env)
|
|
1139
|
+
if reps is None and isinstance(_const_value(stmt.args[1], env), str):
|
|
1140
|
+
# `Therm107(T,"fuzz",...)` is 0 on CR300; CR6/CR1000X take any string as INT_MIN
|
|
1141
|
+
# (`PulseCount(R(1),"2",C1,...)`, oracle, 2026-10-25, round 35)
|
|
1142
|
+
reps = 0 if profile.single_pass else -2147483648
|
|
1143
|
+
if reps is None:
|
|
1144
|
+
continue
|
|
1145
|
+
if not profile.single_pass and not -(2**31) <= reps < 2**31:
|
|
1146
|
+
reps = -2147483648 # outside the Long range (oracle, CR1000X, round 67)
|
|
1147
|
+
if profile.single_pass:
|
|
1148
|
+
# before this line's other diagnostics, e.g. its fnotch warning (oracle, CR300, fuzz
|
|
1149
|
+
# `84bd80cc420c`); destination bounds: rounds 30-31
|
|
1150
|
+
kw = stmt.keyword.upper()
|
|
1151
|
+
messages: list[str | None] = []
|
|
1152
|
+
if reps == 0 or (reps < 0 and kw not in _NEGATIVE_REPS_SILENT):
|
|
1153
|
+
messages.append(f"Invalid reps {reps}, must be > 0.")
|
|
1154
|
+
if reps > 0 or (reps < 0 and kw in _NEGATIVE_REPS_DEST):
|
|
1155
|
+
messages.append(_dest_overrun(stmt, stmt.args[0], reps, profile, env, alias_storage, scope))
|
|
1156
|
+
at = next(
|
|
1157
|
+
(i for i, d in enumerate(diags.items) if (d.line, d.file) == (stmt.line, stmt.file)), len(diags.items)
|
|
1158
|
+
)
|
|
1159
|
+
diags.items[at:at] = [Diagnostic("error", stmt.line, m, stmt.file) for m in messages if m]
|
|
1160
|
+
continue
|
|
1161
|
+
if reps <= 0 and stmt.keyword.upper() != "CS616": # (CS616: `_check_cs616_reps`)
|
|
1162
|
+
diags.error(stmt.line, f"Invalid reps {reps}, must be > 0", stmt.file)
|
|
1163
|
+
dest = stmt.args[0]
|
|
1164
|
+
if reps == 0 or not isinstance(dest, (S.Name, S.Index)):
|
|
1165
|
+
continue
|
|
1166
|
+
if stmt.keyword.upper() not in _REPS_DEST_SIZE:
|
|
1167
|
+
# Therm107/108/109: no output count, but an alias index past its size (round 31)
|
|
1168
|
+
alias = alias_storage(stmt, dest) if isinstance(dest, (S.Name, S.Index)) else None
|
|
1169
|
+
if alias is not None and _alias_overrun(stmt, dest, alias, 0, profile, env):
|
|
1170
|
+
diags.error(stmt.line, f"Variable {alias[2]} out of bounds.", stmt.file)
|
|
1171
|
+
continue
|
|
1172
|
+
message = _dest_overrun(stmt, dest, reps, profile, env, alias_storage, scope)
|
|
1173
|
+
if message is not None:
|
|
1174
|
+
diags.error(stmt.line, message, stmt.file)
|
|
1175
|
+
if reps > 1 and stmt.keyword.upper() == "CURRENTSE" and profile.name == "cr6":
|
|
1176
|
+
diags.error(stmt.line, "Reps >1 not allowed in this device.", stmt.file) # (round 31)
|
|
1177
|
+
_check_bridge_burst(program, profile, diags, env)
|
|
1178
|
+
_check_bridge_span(program, profile, diags, env)
|
|
1179
|
+
_check_pulse_span(program, profile, diags, env)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
# CR6 bridges: (channel, ExChan, MeasPEx argument indexes, measurement groups, channel stride)
|
|
1183
|
+
_BRIDGE_SPAN = {
|
|
1184
|
+
"BRHALF": (3, 4, 5, 1, 1),
|
|
1185
|
+
"BRHALF3W": (3, 4, 5, 2, 1),
|
|
1186
|
+
"BRFULL": (3, 4, 5, 1, 2),
|
|
1187
|
+
"BRHALF4W": (4, 5, 6, 2, 2),
|
|
1188
|
+
"BRFULL6W": (4, 5, 6, 2, 2),
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _check_bridge_span(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
1193
|
+
"""CR6 bridge span validator (round 31, docs/checker.md "Bridge channel spans"): an excitation
|
|
1194
|
+
start inside the measurement span -> `Measurement channels and excitation channels overlap.`;
|
|
1195
|
+
the first excitation, else (Reps > 1) measurement, channel past U12 -> `Channel %d exceeds
|
|
1196
|
+
number of valid channels.` In line order after the destination bounds error. The start channel
|
|
1197
|
+
itself is `terminals.py`'s."""
|
|
1198
|
+
if profile.name != "cr6":
|
|
1199
|
+
return
|
|
1200
|
+
for stmt in program.statements:
|
|
1201
|
+
if not isinstance(stmt, S.Instruction) or stmt.keyword.upper() not in _BRIDGE_SPAN:
|
|
1202
|
+
continue
|
|
1203
|
+
cp, xp, mp, groups, stride = _BRIDGE_SPAN[stmt.keyword.upper()]
|
|
1204
|
+
if len(stmt.args) <= mp:
|
|
1205
|
+
continue
|
|
1206
|
+
reps, chan, ex, meas = (_int_value(stmt.args[i], env) for i in (1, cp, xp, mp))
|
|
1207
|
+
if (
|
|
1208
|
+
(reps is None or chan is None or ex is None or meas is None)
|
|
1209
|
+
or reps < 1
|
|
1210
|
+
or not (201 <= chan <= 212 and 201 <= ex <= 212)
|
|
1211
|
+
or (stride == 2 and chan % 2 == 0)
|
|
1212
|
+
):
|
|
1213
|
+
continue
|
|
1214
|
+
s, e, meas = chan - 201, ex - 201, max(meas, 1)
|
|
1215
|
+
meas_last = s + (reps - 1) * groups * stride + (groups - 1) * stride
|
|
1216
|
+
ex_last = e + -(-reps // meas) - 1
|
|
1217
|
+
found = []
|
|
1218
|
+
if s < e <= meas_last: # (the binary's other branch, e < s, cannot fire for Reps >= 1)
|
|
1219
|
+
found.append("Measurement channels and excitation channels overlap.")
|
|
1220
|
+
past = next((c for c in range(e + 1, ex_last + 1) if c + 1 > 12), None)
|
|
1221
|
+
if past is None and ex_last <= 11 and reps > 1:
|
|
1222
|
+
past = next((c for c in range(s + stride, meas_last + 1, stride) if c + 1 > 12), None)
|
|
1223
|
+
if past is not None:
|
|
1224
|
+
found.append(f"Channel {past + 1} exceeds number of valid channels.")
|
|
1225
|
+
_insert_in_line_order(diags, stmt, found)
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _insert_in_line_order(diags: Diagnostics, stmt: S.Instruction, texts: list[str]) -> None:
|
|
1229
|
+
"""After this tier's errors on earlier lines and on this line, before later lines."""
|
|
1230
|
+
at = next(
|
|
1231
|
+
(i for i, d in enumerate(diags.items) if d.severity == "error" and d.file == stmt.file and d.line > stmt.line),
|
|
1232
|
+
len(diags.items),
|
|
1233
|
+
)
|
|
1234
|
+
diags.items[at:at] = [Diagnostic("error", stmt.line, text, stmt.file) for text in texts]
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _pulse_index(code: int) -> tuple[int, int] | None:
|
|
1238
|
+
"""(port index, highest index Reps may reach) of a CR6 PulseCount channel (round 31). From
|
|
1239
|
+
201 up, the port converter keeps `code % 200` (`412` is U12, `400` C4; docs/checker.md)."""
|
|
1240
|
+
idx = code - 101 if 101 <= code <= 116 else code % 200 + 3 if code >= 201 and code % 200 <= 12 else None
|
|
1241
|
+
return None if idx is None else (idx, 4 if idx < 4 else 16) # one past C4 / U12
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def _check_pulse_span(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
1245
|
+
"""CR6 `PulseCount`: Reps channels from the start must stay in its terminal family ->
|
|
1246
|
+
`Invalid PulseCount reps.` after the destination bounds error (round 31, docs/checker.md
|
|
1247
|
+
"Bridge channel spans"; CR1000X: the `pulse_chan_cr1000x` terminal span)."""
|
|
1248
|
+
if profile.name != "cr6":
|
|
1249
|
+
return
|
|
1250
|
+
for stmt in program.statements:
|
|
1251
|
+
if not isinstance(stmt, S.Instruction) or stmt.keyword.upper() != "PULSECOUNT" or len(stmt.args) < 3:
|
|
1252
|
+
continue
|
|
1253
|
+
reps, chan = _int_value(stmt.args[1], env), _int_value(stmt.args[2], env)
|
|
1254
|
+
port = _pulse_index(chan) if chan is not None else None
|
|
1255
|
+
if reps is None or port is None:
|
|
1256
|
+
continue
|
|
1257
|
+
if max(reps, 1) - 1 + port[0] > port[1]:
|
|
1258
|
+
_insert_in_line_order(diags, stmt, ["Invalid PulseCount reps."])
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def _dest_overrun(stmt, dest, reps, profile, env, alias_storage, scope) -> str | None:
|
|
1262
|
+
"""`Variable %s out of bounds.` when the Float destination cannot hold the output count."""
|
|
1263
|
+
if not isinstance(dest, (S.Name, S.Index)):
|
|
1264
|
+
return None
|
|
1265
|
+
count = _measurement_output_count(stmt, reps, env)
|
|
1266
|
+
alias = alias_storage(stmt, dest)
|
|
1267
|
+
if alias is not None:
|
|
1268
|
+
return f"Variable {alias[2]} out of bounds." if _alias_overrun(stmt, dest, alias, count, profile, env) else None
|
|
1269
|
+
if scope.types.get(dest.name.upper()) != "FLOAT" or dest.name.upper() not in scope.dims:
|
|
1270
|
+
return None
|
|
1271
|
+
shape = scope.dims[dest.name.upper()]
|
|
1272
|
+
offset = _flat_offset(dest, shape, env)
|
|
1273
|
+
if offset is None:
|
|
1274
|
+
return None
|
|
1275
|
+
total = math.prod(shape)
|
|
1276
|
+
return f"Variable {dest.name} out of bounds." if count < 0 or count > total - offset else None
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _check_bridge_burst(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
1280
|
+
"""The shared bridge builder checks burst MeasPEx after resolving its destination."""
|
|
1281
|
+
if profile.name not in {"cr6", "cr1000x"}:
|
|
1282
|
+
return
|
|
1283
|
+
for stmt in program.statements:
|
|
1284
|
+
if (
|
|
1285
|
+
not isinstance(stmt, S.Instruction)
|
|
1286
|
+
or stmt.keyword.upper() not in {"BRHALF4W", "BRFULL6W"}
|
|
1287
|
+
or len(stmt.args) < 7
|
|
1288
|
+
):
|
|
1289
|
+
continue
|
|
1290
|
+
reps, channel, meas = (_int_value(stmt.args[i], env) for i in (1, 4, 6))
|
|
1291
|
+
if reps is None or channel is None or meas is None or meas < 1:
|
|
1292
|
+
continue
|
|
1293
|
+
# Only valid burst channels: invalid channels have their own conversion diagnostics.
|
|
1294
|
+
valid = (-211 <= channel <= -201 and channel % 2 != 0) if profile.name == "cr6" else -8 <= channel <= -1
|
|
1295
|
+
if valid and meas != reps:
|
|
1296
|
+
diags.error(stmt.line, "When channel < 0, measurements per excitation must equal reps.", stmt.file)
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
# Destination size rules (oracle, 2026-10-02; docs/checker.md#destination-bounds): elements the
|
|
1300
|
+
# Float destination must hold from its start -> `Variable %s out of bounds.`. `(per_rep,)` scales the
|
|
1301
|
+
# Reps argument (index 1); `REALTIME` needs a fixed nine (Therm107-109: `check_reps`, round 31).
|
|
1302
|
+
# Only CalFile also rejects negative Reps.
|
|
1303
|
+
_DEST_FIXED = {"REALTIME": 9}
|
|
1304
|
+
_DEST_PER_REP = {
|
|
1305
|
+
"CALFILE": 1,
|
|
1306
|
+
"AVGRUN": 1,
|
|
1307
|
+
"MOVE": 1,
|
|
1308
|
+
"MINRUN": 1,
|
|
1309
|
+
"MAXRUN": 1,
|
|
1310
|
+
"TOTALRUN": 1,
|
|
1311
|
+
"STDDEVRUN": 1, # (the other running statistics: oracle, CR6/CR1000X, round 35)
|
|
1312
|
+
"PRTCALC": 1,
|
|
1313
|
+
} # (oracle, CR6/CR1000X, round 57)
|
|
1314
|
+
_DEST_NEGATIVE_OOB = frozenset({"CALFILE", "PRTCALC"})
|
|
1315
|
+
_RUNNING_STATS = frozenset({"MINRUN", "MAXRUN", "TOTALRUN", "STDDEVRUN"}) # (AvgRun not probed)
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
# Element counts the CR6 instruction builders pass to the shared destination resolver, read from the
|
|
1319
|
+
# decompiled builders and confirmed on the real compilers (docs/checker.md "Builder destination counts").
|
|
1320
|
+
# Arguments the builder stores in a byte are reduced modulo 256 (`_byte`).
|
|
1321
|
+
_SDM_MODE_VALUES = {
|
|
1322
|
+
"CS7500": (2, 2, 4, 3, 11, 3, 4, 4),
|
|
1323
|
+
"LI7200": (2, 5, 10, 12, 10, 5, 1, 5),
|
|
1324
|
+
"LI7700": (3, 5, 9, 13, 17, 3, 7, 9),
|
|
1325
|
+
}
|
|
1326
|
+
_TGA_VALUES = ((2, 3, 4), (5, 7, 9), (8, 10, 12), (19, 25, 31), (21, 27, 33))
|
|
1327
|
+
_EC100_VALUES = (8, 12, 13, 15, 36)
|
|
1328
|
+
_BUILDER_FIXED = {
|
|
1329
|
+
"CSAT3B": ((0, 5),),
|
|
1330
|
+
"CSAT3BMONITOR": ((0, 4),),
|
|
1331
|
+
"HYDRAPROBE": ((0, 11), (1, 4)),
|
|
1332
|
+
"GOESGPS": ((0, 6), (1, 7)),
|
|
1333
|
+
"RECTPOLAR": ((0, 2), (1, 2)),
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def _byte(value: int) -> int:
|
|
1338
|
+
return value & 0xFF
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _builder_counts(kw: str, args: list, env: consteval.Env) -> list[tuple[int, int]]:
|
|
1342
|
+
"""`(argument, elements)` pairs a destination or source must hold from its start."""
|
|
1343
|
+
|
|
1344
|
+
def arg(i: int) -> int | None:
|
|
1345
|
+
return _int_value(args[i], env) if i < len(args) else None
|
|
1346
|
+
|
|
1347
|
+
if kw in _BUILDER_FIXED:
|
|
1348
|
+
return [(i, n) for i, n in _BUILDER_FIXED[kw] if i < len(args)]
|
|
1349
|
+
if kw == "SDMGENERIC":
|
|
1350
|
+
return [(0, count)] if (count := arg(5)) is not None else []
|
|
1351
|
+
if kw == "SDMSW8A":
|
|
1352
|
+
return [(0, _byte(count))] if (count := arg(1)) is not None else []
|
|
1353
|
+
if kw == "SDMCD16AC":
|
|
1354
|
+
return [(0, 16 * _byte(count))] if (count := arg(1)) is not None else []
|
|
1355
|
+
if kw == "CSAT3":
|
|
1356
|
+
if (count := arg(1)) is None:
|
|
1357
|
+
return []
|
|
1358
|
+
reps = _byte(count)
|
|
1359
|
+
return [(0, 5 * reps)] if reps < 128 else [] # a signed byte: negative is `Out of memory.`
|
|
1360
|
+
if kw == "PEAKVALLEY":
|
|
1361
|
+
return [(i, count) for i in (0, 1, 3) if i < len(args)] if (count := arg(2)) is not None else []
|
|
1362
|
+
if kw in _SDM_MODE_VALUES:
|
|
1363
|
+
reps, mode = arg(1), arg(3)
|
|
1364
|
+
if reps is None or mode is None or _byte(mode) > 7:
|
|
1365
|
+
return []
|
|
1366
|
+
return [(0, _SDM_MODE_VALUES[kw][_byte(mode)] * _byte(reps))]
|
|
1367
|
+
if kw == "TGA":
|
|
1368
|
+
first, second = arg(2), arg(3)
|
|
1369
|
+
if first is None or second is None or not (1 <= _byte(first) <= 5 and 1 <= _byte(second) <= 3):
|
|
1370
|
+
return []
|
|
1371
|
+
return [(0, _TGA_VALUES[_byte(first) - 1][_byte(second) - 1])]
|
|
1372
|
+
if kw == "EC100":
|
|
1373
|
+
option = arg(2)
|
|
1374
|
+
if option is None or (option != 2718 and _byte(option) > 3):
|
|
1375
|
+
return []
|
|
1376
|
+
return [(0, _EC100_VALUES[4 if option == 2718 else _byte(option)])]
|
|
1377
|
+
return []
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
# Spatial family (oracle, 2026-10-02, all four loggers): argument 3, the source, needs Swath
|
|
1381
|
+
# elements (Float or Long; negative Swath is out of bounds); the destination needs a fixed 2
|
|
1382
|
+
# (Max/Min: value + location), Swath (SortSpa) or 2 * Swath (SortSpaIndexed); Avg/StdDev/RMS
|
|
1383
|
+
# impose nothing beyond the index rule. Entries are (fixed, per Swath).
|
|
1384
|
+
_SPA_DEST = {"MAXSPA": (2, 0), "MINSPA": (2, 0), "SORTSPA": (0, 1), "SORTSPAINDEXED": (0, 2)}
|
|
1385
|
+
# (oracle, round 56: `MaxSpa(L,...)` -> `Dest type must be Float.`, `SortSpa(LongDest,6,Float)` ->
|
|
1386
|
+
# `Source and Dest types must be the same.`)
|
|
1387
|
+
_SPA_TYPED = frozenset({"MAXSPA", "MINSPA", "SORTSPA"}) # (SortSpaIndexed: expressions.py)
|
|
1388
|
+
_SPA_SOURCE = frozenset({"AVGSPA", "MAXSPA", "MINSPA", "RMSSPA", "STDDEVSPA", "SORTSPA", "SORTSPAINDEXED"})
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def check_dest_bounds(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
1392
|
+
from . import symbols
|
|
1393
|
+
|
|
1394
|
+
tables = symbols.scope_tables(program, env, scalars=True)
|
|
1395
|
+
# the running statistics also bound a routine's scalar parameter (`Sub R(Q)` / `MinRun(Q,2,...)`
|
|
1396
|
+
# is `Variable Q out of bounds.`; oracle, CR6/CR1000X, 2026-10-25, round 35)
|
|
1397
|
+
with_parameters = symbols.scope_tables(program, env, scalars=True, parameters=True)
|
|
1398
|
+
dims: dict[str, tuple[int, ...]] = {}
|
|
1399
|
+
types: dict[str, str] = {}
|
|
1400
|
+
arrays: set[str] = set()
|
|
1401
|
+
|
|
1402
|
+
def overruns(arg: S.Expr | None, need: int, negative: bool, kinds: tuple[str, ...]) -> TypeGuard[S.Name | S.Index]:
|
|
1403
|
+
if not isinstance(arg, (S.Name, S.Index)):
|
|
1404
|
+
return False
|
|
1405
|
+
shape = dims.get(arg.name.upper())
|
|
1406
|
+
if shape is None or types.get(arg.name.upper()) not in kinds:
|
|
1407
|
+
return False
|
|
1408
|
+
offset = _flat_offset(arg, shape)
|
|
1409
|
+
if offset is None:
|
|
1410
|
+
return False
|
|
1411
|
+
total = 1
|
|
1412
|
+
for n in shape:
|
|
1413
|
+
total *= n
|
|
1414
|
+
if offset >= total: # an index past the dimension is the index rule's
|
|
1415
|
+
return False
|
|
1416
|
+
return negative or need > total - offset
|
|
1417
|
+
|
|
1418
|
+
names = longconv.float_names(program.statements), longconv.literal_names(program.statements)
|
|
1419
|
+
alias_storage = _measurement_aliases(program, tables, env)
|
|
1420
|
+
for stmt in program.statements:
|
|
1421
|
+
scope = tables(stmt)
|
|
1422
|
+
dims, types, arrays = scope.dims, scope.types, scope.arrays
|
|
1423
|
+
if (
|
|
1424
|
+
isinstance(stmt, S.Instruction)
|
|
1425
|
+
and stmt.keyword.upper() == "CDM_TCCOMP"
|
|
1426
|
+
and len(stmt.args) > 3
|
|
1427
|
+
and isinstance(stmt.args[2], (S.Name, S.Index))
|
|
1428
|
+
):
|
|
1429
|
+
# Reps (fourth) of the destination (third); negative Reps overruns too (oracle, CR6/
|
|
1430
|
+
# CR1000X, 2026-10-10; docs/instructions.md "CPI measurement time")
|
|
1431
|
+
reps = longconv.long_value(stmt.args[3], env, profile, *names)
|
|
1432
|
+
if reps is not None and overruns(stmt.args[2], reps, reps < 0, ("FLOAT",)):
|
|
1433
|
+
diags.error(stmt.line, f"Variable {stmt.args[2].name} out of bounds.", stmt.file)
|
|
1434
|
+
continue
|
|
1435
|
+
if not isinstance(stmt, S.Instruction) or not stmt.args or not isinstance(stmt.args[0], (S.Name, S.Index)):
|
|
1436
|
+
continue
|
|
1437
|
+
kw = stmt.keyword.upper()
|
|
1438
|
+
if kw in _RUNNING_STATS:
|
|
1439
|
+
scope = with_parameters(stmt)
|
|
1440
|
+
dims, types, arrays = scope.dims, scope.types, scope.arrays
|
|
1441
|
+
dest = stmt.args[0]
|
|
1442
|
+
name = dest.name.upper()
|
|
1443
|
+
bad: list[S.Name | S.Index] = []
|
|
1444
|
+
if kw in _SPA_SOURCE and len(stmt.args) > 2:
|
|
1445
|
+
swath = _int_value(stmt.args[1], env)
|
|
1446
|
+
if swath is not None:
|
|
1447
|
+
fixed, per = _SPA_DEST.get(kw, (0, 0))
|
|
1448
|
+
if (fixed or per) and overruns(
|
|
1449
|
+
dest, fixed + per * swath, per > 0 and swath < 0, ("FLOAT", "LONG")
|
|
1450
|
+
): # (Long too, round 56)
|
|
1451
|
+
bad.append(dest)
|
|
1452
|
+
if overruns(source := stmt.args[2], swath, swath < 0, ("FLOAT", "LONG")):
|
|
1453
|
+
bad.append(source)
|
|
1454
|
+
elif kw == "SPLITSTR": # a scalar destination takes one value (Reps > 1 overruns it)
|
|
1455
|
+
reps = _int_value(stmt.args[3], env) if len(stmt.args) > 3 else None
|
|
1456
|
+
if (
|
|
1457
|
+
isinstance(dest, S.Name)
|
|
1458
|
+
and name in dims
|
|
1459
|
+
and name not in arrays
|
|
1460
|
+
and reps is not None
|
|
1461
|
+
and reps > 1
|
|
1462
|
+
and types.get(name) in ("FLOAT", "STRING")
|
|
1463
|
+
): # a structure takes Reps 3
|
|
1464
|
+
bad.append(dest)
|
|
1465
|
+
elif (
|
|
1466
|
+
kw == "CSAT3"
|
|
1467
|
+
and len(stmt.args) > 1
|
|
1468
|
+
and (reps := _int_value(stmt.args[1], env)) is not None
|
|
1469
|
+
and _byte(reps) >= 128
|
|
1470
|
+
):
|
|
1471
|
+
# a negative signed-byte Reps fails the builder's allocation before the destination
|
|
1472
|
+
# is resolved; the compile ends there (round 31, docs/checker.md "Bridge channel spans")
|
|
1473
|
+
diags.error(stmt.line, "Out of memory.", stmt.file)
|
|
1474
|
+
return
|
|
1475
|
+
elif (
|
|
1476
|
+
kw in _BUILDER_FIXED
|
|
1477
|
+
or kw in ("SDMGENERIC", "SDMSW8A", "SDMCD16AC", "CSAT3", "PEAKVALLEY", "TGA", "EC100")
|
|
1478
|
+
or kw in _SDM_MODE_VALUES
|
|
1479
|
+
):
|
|
1480
|
+
# SDMGeneric checks any non-String destination, including a negative count
|
|
1481
|
+
kinds = ("FLOAT", "LONG", "BOOLEAN") if kw == "SDMGENERIC" else ("FLOAT",)
|
|
1482
|
+
for i, need in _builder_counts(kw, stmt.args, env):
|
|
1483
|
+
if overruns(target := stmt.args[i], need, need < 0, kinds):
|
|
1484
|
+
bad.append(target)
|
|
1485
|
+
elif kw in _DEST_FIXED or kw in _DEST_PER_REP:
|
|
1486
|
+
negative = False
|
|
1487
|
+
if kw in _DEST_FIXED:
|
|
1488
|
+
need = _DEST_FIXED[kw]
|
|
1489
|
+
else:
|
|
1490
|
+
reps = _int_value(stmt.args[1], env) if len(stmt.args) > 1 else None
|
|
1491
|
+
if reps is None or (reps < 0 and kw not in _DEST_NEGATIVE_OOB):
|
|
1492
|
+
continue
|
|
1493
|
+
negative = reps < 0
|
|
1494
|
+
need = reps * _DEST_PER_REP[kw]
|
|
1495
|
+
alias = alias_storage(stmt, dest) if kw in _DEST_PER_REP else None
|
|
1496
|
+
if alias is not None:
|
|
1497
|
+
# an alias destination counts from the alias (oracle, CR6/CR1000X, round 57: PRTCalc,
|
|
1498
|
+
# CalFile, Move and the running statistics)
|
|
1499
|
+
if _alias_overrun(stmt, dest, alias, -1 if negative else need, profile, env):
|
|
1500
|
+
diags.error(stmt.line, f"Variable {alias[2]} out of bounds.", stmt.file)
|
|
1501
|
+
elif overruns(dest, need, negative, ("FLOAT",)):
|
|
1502
|
+
bad.append(dest)
|
|
1503
|
+
# single-pass loggers: before this line's warnings and channel errors (oracle)
|
|
1504
|
+
for arg in reversed(bad) if profile.single_pass else bad:
|
|
1505
|
+
diag = Diagnostic("error", stmt.line, f"Variable {arg.name} out of bounds.", stmt.file)
|
|
1506
|
+
if profile.single_pass:
|
|
1507
|
+
at = next(
|
|
1508
|
+
(i for i, d in enumerate(diags.items) if (d.line, d.file) == (stmt.line, stmt.file)),
|
|
1509
|
+
len(diags.items),
|
|
1510
|
+
)
|
|
1511
|
+
diags.items.insert(at, diag)
|
|
1512
|
+
else:
|
|
1513
|
+
diags.items.append(diag)
|
|
1514
|
+
if kw in _SPA_TYPED and len(stmt.args) > 2 and profile.name in ("cr6", "cr1000x"):
|
|
1515
|
+
source = stmt.args[2]
|
|
1516
|
+
dest_type = types.get(name)
|
|
1517
|
+
source_type = types.get(source.name.upper()) if isinstance(source, (S.Name, S.Index)) else None
|
|
1518
|
+
if kw in ("MAXSPA", "MINSPA") and dest_type not in (None, "FLOAT"):
|
|
1519
|
+
diags.error(stmt.line, "Dest type must be Float.", stmt.file)
|
|
1520
|
+
elif kw not in ("MAXSPA", "MINSPA") and dest_type and source_type and dest_type != source_type:
|
|
1521
|
+
diags.error(stmt.line, "Source and Dest types must be the same.", stmt.file)
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
def _index_value(arg: S.Expr | None, env: consteval.Env | None) -> int | None:
|
|
1525
|
+
"""An integer literal index, or with `env` any integer constant (`X(K)`, round 30 review)."""
|
|
1526
|
+
if isinstance(arg, S.Number) and isinstance(arg.value, int):
|
|
1527
|
+
return arg.value
|
|
1528
|
+
v = _const_value(arg, env) if env is not None else None
|
|
1529
|
+
return v if isinstance(v, int) and not isinstance(v, bool) else None
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def _flat_offset(dest: S.Name | S.Index, shape: tuple[int, ...], env: consteval.Env | None = None) -> int | None:
|
|
1533
|
+
"""0-based row-major offset of the destination's first element, or None if not literal
|
|
1534
|
+
(or, with `env`, constant)."""
|
|
1535
|
+
if isinstance(dest, S.Name) or not dest.args or all(a is None for a in dest.args):
|
|
1536
|
+
return 0
|
|
1537
|
+
index = [k for a in dest.args if (k := _index_value(a, env)) is not None]
|
|
1538
|
+
if len(index) != len(dest.args):
|
|
1539
|
+
return None
|
|
1540
|
+
if len(index) == 1:
|
|
1541
|
+
return index[0] - 1 if 1 <= index[0] else None
|
|
1542
|
+
if len(index) != len(shape) or any(not 1 <= k <= n for k, n in zip(index, shape)):
|
|
1543
|
+
return None
|
|
1544
|
+
offset = 0
|
|
1545
|
+
for k, n in zip(index, shape):
|
|
1546
|
+
offset = offset * n + (k - 1)
|
|
1547
|
+
return offset
|