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,249 @@
|
|
|
1
|
+
"""Per-logger analog-channel and port terminal model.
|
|
2
|
+
|
|
3
|
+
Generalises the analog-channel range check (originally CR6-only, hard-coded `U1..U12`) across all
|
|
4
|
+
four loggers, driven by oracle-probed per-logger data (`data/<logger>/terminals.json`; evidence,
|
|
5
|
+
probes and the message-text differences: `docs/loggers.md`, `docs/instructions.md`).
|
|
6
|
+
|
|
7
|
+
Each logger's `terminals.json` names its own channel "kinds" -- CR6 has separate `u_terminal`
|
|
8
|
+
(single-ended-shaped: any `U1..U12`, no oddness constraint; `ACPower`, `CS616`'s `CS616Chan`,
|
|
9
|
+
`ExciteV`/`ExciteI`'s `ExChan`), `u_terminal_diff` (`VoltDiff`/`TCDiff`'s `DiffChan`: `U1..U12` only
|
|
10
|
+
on an *odd* terminal, a second, distinct message when the value is otherwise in range) and
|
|
11
|
+
`u_terminal_se` (`VoltSe`/`TCSe`'s `SEChan`: any `U1..U12`, no oddness) -- both of the latter two
|
|
12
|
+
also allow a burst-mode negative value (`transform: "abs"`); CR1000X and CR300/CR350 instead use
|
|
13
|
+
`diff_chan`/`se_chan`, a plain integer with a different valid range and message
|
|
14
|
+
(`docs/loggers.md`). A `"port"`-shaped kind (`port_c1c4_u1u12` on CR6, `port_c1c8` on CR1000X) is a
|
|
15
|
+
disjoint union of ranges (`PortSet`/`PortGet`'s `Port`, `CS616`'s `CS616Port`); `sdi_port` (CR6) /
|
|
16
|
+
`sdi_port_cr1000x` (CR1000X) is a disjoint union restricted to *odd* terminals only (SDI-12's own
|
|
17
|
+
half-duplex wiring, `docs/instructions.md`). `checker/instructions.py` calls `check` here; the two
|
|
18
|
+
modules stay separate because this one owns the per-logger *data*, while `instructions.py` is the
|
|
19
|
+
shared entry point another workstream also uses.
|
|
20
|
+
|
|
21
|
+
A `kind` dict:
|
|
22
|
+
- `ranges`: list of `[min, max]` pairs (a single-range kind may instead give `min`/`max` directly,
|
|
23
|
+
kept for the pre-existing CR1000X/CR300/CR350 `diff_chan`/`se_chan` entries).
|
|
24
|
+
- `transform`: `"abs"` (test/show `abs(value)`, burst-mode) or `"none"` (default).
|
|
25
|
+
- `skip_nonpositive`: CR300/CR350 (`docs/loggers.md`) -- a value `<= 0` is deliberately left
|
|
26
|
+
unchecked (real, different, not-yet-modelled validation shape there; never guessed at).
|
|
27
|
+
- `odd_only` + `odd_message`: after the range check passes, the (transformed) value must also be
|
|
28
|
+
odd, else `odd_message` fires instead of `message` (`docs/instructions.md`'s "Invalid start
|
|
29
|
+
channel for Differential Measurement..." / "Invalid SDI-12 port..." findings).
|
|
30
|
+
- `allowed` + `allowed_message`: after the range check passes, the value must also be one of
|
|
31
|
+
`allowed` (`SPIOpen`'s `C1, U1, U5, or U9`, `TimerInput`'s starting channel).
|
|
32
|
+
- `secondary_when_invalid`: CR1000X -- an out-of-range value also gets the `odd_message`/
|
|
33
|
+
`allowed_message` line after `message` (its port converter returns 0 for a bad port, which
|
|
34
|
+
then fails the instruction's own test; CR6 stops at `message`).
|
|
35
|
+
- `message`: the range-check failure text, `{v}` = the displayed (transformed) value.
|
|
36
|
+
|
|
37
|
+
CR6 ports go through one converter (decompiled `FUN_00515ff0`): `C1..C16` (101..116) and
|
|
38
|
+
`U1..U12` (201..212) are valid, `C5..C16` being the same terminals as `U1..U12` (oracle,
|
|
39
|
+
2026-10-01: `PortSet(109,1)`, `SPIOpen(105,...)` compile); CR1000X takes `C1..C8`.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import math
|
|
43
|
+
|
|
44
|
+
from .. import consteval
|
|
45
|
+
from .. import syntax as S
|
|
46
|
+
from ..diagnostics import Diagnostics
|
|
47
|
+
from ..profiles import Profile
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ranges(kind: dict) -> list[tuple[int, int]]:
|
|
51
|
+
if "ranges" in kind:
|
|
52
|
+
return [tuple(r) for r in kind["ranges"]]
|
|
53
|
+
return [(kind["min"], kind["max"])]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _in_range(v: float, kind: dict) -> bool:
|
|
57
|
+
return any(lo <= v <= hi for lo, hi in _ranges(kind))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _cr6_burst_kind(kind: dict) -> bool:
|
|
61
|
+
return kind.get("transform") == "abs" and _ranges(kind) == [(201, 212)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _evaluate(value: float, kind: dict) -> tuple[bool, bool, int]:
|
|
65
|
+
"""(in_range, odd_ok, displayed_value) for a constant `value` against one channel `kind`.
|
|
66
|
+
|
|
67
|
+
`transform: "abs"`: the real compiler's shown value and both tests use the argument's absolute
|
|
68
|
+
value -- burst-mode measurements encode "same channel, repeated" as a negative channel number
|
|
69
|
+
(`docs/loggers.md`, oracle: CR6 `VoltDiff` `DiffChan` -1 (raw -201) compiles, `TCSe` `SEChan`
|
|
70
|
+
-202 compiles). `transform: "none"` (every other kind): the raw value is both tested and shown.
|
|
71
|
+
"""
|
|
72
|
+
v = abs(value) if kind.get("transform") == "abs" else value
|
|
73
|
+
if _cr6_burst_kind(kind) and -200 < value < 0:
|
|
74
|
+
v = value # a small negative is no burst code: tested and shown signed (oracle, CR6)
|
|
75
|
+
if kind.get("port_wrap"):
|
|
76
|
+
# CR6 ports (PortSet/PortGet/CS616, round 9): the value is cast to a 32-bit integer (out of
|
|
77
|
+
# range -> -2147483648, which is what the message shows) and any v >= 201 with v mod 200
|
|
78
|
+
# in 0..12 is accepted besides C1..C16/U1..U12 (400, 1000, 1001, 3012 compile; 200, 213,
|
|
79
|
+
# 413, 1100 do not).
|
|
80
|
+
v = int(v) if -(2**31) <= v < 2**31 else -(2**31)
|
|
81
|
+
in_range = _in_range(v, kind) or (v >= 201 and v % 200 <= 12)
|
|
82
|
+
return in_range, True, v
|
|
83
|
+
in_range = _in_range(v, kind)
|
|
84
|
+
odd_ok = (not kind.get("odd_only")) or (in_range and int(v) % 2 == 1)
|
|
85
|
+
return in_range, odd_ok, int(v)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
CR6_BRIDGES = frozenset({"BRHALF", "BRHALF3W", "BRHALF4W", "BRFULL", "BRFULL6W"})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def check(program: S.Program, profile: Profile, diags: Diagnostics, env: consteval.Env) -> None:
|
|
92
|
+
terminals = profile.terminals
|
|
93
|
+
checked_instructions: dict = terminals.get("instructions", {})
|
|
94
|
+
channels: dict = terminals.get("channels", {})
|
|
95
|
+
if not checked_instructions:
|
|
96
|
+
return
|
|
97
|
+
from .instr_data import _with_inline
|
|
98
|
+
|
|
99
|
+
for stmt in _with_inline(program.statements): # a one-line If's statement too (round 58)
|
|
100
|
+
if not isinstance(stmt, S.Instruction):
|
|
101
|
+
continue
|
|
102
|
+
positions = checked_instructions.get(stmt.keyword.upper())
|
|
103
|
+
if not positions:
|
|
104
|
+
continue
|
|
105
|
+
if (
|
|
106
|
+
stmt.keyword.upper() == "ACPOWER"
|
|
107
|
+
and profile.name == "cr6"
|
|
108
|
+
and _check_acpower(stmt, channels[positions[0][1]], env, diags)
|
|
109
|
+
):
|
|
110
|
+
continue
|
|
111
|
+
for pos, kind_name, *extra in positions:
|
|
112
|
+
# optional third and fourth items: the Reps position and the channels per repetition;
|
|
113
|
+
# the channel span is then checked (rounds 30-31, docs/checker.md#measurement-
|
|
114
|
+
# destination-and-channel-spans-round-31)
|
|
115
|
+
reps = _const_int(stmt, extra[0], env, truncate=True) if extra else None
|
|
116
|
+
width = extra[1] if len(extra) > 1 else 1
|
|
117
|
+
kind = channels[kind_name]
|
|
118
|
+
if reps is not None and profile.name == "cr6" and stmt.keyword.upper() in CR6_BRIDGES:
|
|
119
|
+
reps = min(reps, 1) # start channel only: `instr_measure._check_bridge_span` owns the span
|
|
120
|
+
if reps is not None and "span" in kind:
|
|
121
|
+
_check_arg(stmt, pos, kind, env, diags, reps=reps, width=width)
|
|
122
|
+
else: # CR300/CR350: the last channel used is tested and shown
|
|
123
|
+
_check_arg(stmt, pos, kind, env, diags, max(reps, 1) * width - 1 if reps is not None else 0)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _const_int(stmt: S.Instruction, pos: int, env: consteval.Env, truncate: bool = False) -> int | None:
|
|
127
|
+
"""An integral constant argument; `truncate` takes the integer part of any finite one."""
|
|
128
|
+
if pos > len(stmt.args) or stmt.args[pos - 1] is None or not consteval.is_constant(stmt.args[pos - 1], env):
|
|
129
|
+
return None
|
|
130
|
+
try:
|
|
131
|
+
value = consteval.value_of(stmt.args[pos - 1], env)
|
|
132
|
+
except consteval.NotConstant:
|
|
133
|
+
return None
|
|
134
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool) or not math.isfinite(value):
|
|
135
|
+
return None
|
|
136
|
+
return int(value) if truncate or value == int(value) else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _check_acpower(stmt: S.Instruction, kind: dict, env: consteval.Env, diags: Diagnostics) -> bool:
|
|
140
|
+
"""CR6 `ACPower` with a bad `ChanV` (4th) and/or `ChanI` (7th) and one or two `RepsI` (10th)
|
|
141
|
+
(oracle, 2026-10-10, `tests/fixtures/acpower_channels.json`): a bad voltage channel `v` prints
|
|
142
|
+
`v, v, -v, v`; per rep `k` (one or two) a bad voltage channel then adds `v` (from the second
|
|
143
|
+
rep) and a bad current channel `i` adds `-(i+k)`. True when handled."""
|
|
144
|
+
v, i, reps = (_const_int(stmt, pos, env) for pos in (4, 7, 10))
|
|
145
|
+
if v is None or i is None or reps not in (1, 2) or v < 0:
|
|
146
|
+
return False
|
|
147
|
+
v_bad, i_bad = not _in_range(v, kind), not _in_range(i, kind)
|
|
148
|
+
shown = [v, v, -v, v] if v_bad else []
|
|
149
|
+
for k in range(reps):
|
|
150
|
+
if k and v_bad:
|
|
151
|
+
shown.append(v)
|
|
152
|
+
if i_bad:
|
|
153
|
+
shown.append(-(i + k))
|
|
154
|
+
for value in shown:
|
|
155
|
+
diags.error(stmt.line, kind["message"].format(v=value), stmt.file)
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _check_arg(
|
|
160
|
+
stmt: S.Instruction,
|
|
161
|
+
pos: int,
|
|
162
|
+
kind: dict,
|
|
163
|
+
env: consteval.Env,
|
|
164
|
+
diags: Diagnostics,
|
|
165
|
+
span: int = 0,
|
|
166
|
+
reps: int | None = None,
|
|
167
|
+
width: int = 1,
|
|
168
|
+
) -> None:
|
|
169
|
+
idx = pos - 1
|
|
170
|
+
if idx >= len(stmt.args) or stmt.args[idx] is None:
|
|
171
|
+
return
|
|
172
|
+
arg = stmt.args[idx]
|
|
173
|
+
huge = False
|
|
174
|
+
if kind.get("huge_int_min") or "overflow_add" in kind:
|
|
175
|
+
# CR1000X (oracle, 2026-10-25, round 35): a constant outside int32 becomes INT_MIN; a
|
|
176
|
+
# single-ended channel then reports it, a differential one compiles, as does one whose
|
|
177
|
+
# `|channel| + overflow_add` passes INT_MAX (from 2147483641, any Reps)
|
|
178
|
+
try:
|
|
179
|
+
folded = consteval.value_of(arg, env) if consteval.is_constant(arg, env) else None
|
|
180
|
+
except consteval.NotConstant:
|
|
181
|
+
folded = None
|
|
182
|
+
if isinstance(folded, (int, float)) and not isinstance(folded, bool) and math.isfinite(folded):
|
|
183
|
+
huge = not -(2**31) < int(folded) < 2**31 # (INT_MIN itself too: `abs` keeps it)
|
|
184
|
+
if "overflow_add" in kind and (huge or abs(int(folded)) + kind["overflow_add"] > 2**31 - 1):
|
|
185
|
+
return
|
|
186
|
+
if (isinstance(arg, S.String) or huge) and kind.get("string_int_min" if not huge else "huge_int_min"):
|
|
187
|
+
# a string literal (`PortSet("fuzz",1)`, `"102"`, `""`) folds to INT_MIN (oracle, all
|
|
188
|
+
# four loggers, 2026-10-12)
|
|
189
|
+
value = -2147483648
|
|
190
|
+
in_range, odd_ok, _ = _evaluate(value, kind)
|
|
191
|
+
shown = value # (`abs` of INT_MIN is INT_MIN in C: CR1000X `VoltSe(X,1,mV1000,"abc",...)`)
|
|
192
|
+
if not in_range:
|
|
193
|
+
diags.error(stmt.line, kind["message"].format(v=shown), stmt.file)
|
|
194
|
+
if kind.get("secondary_when_invalid") and (not odd_ok or "allowed" in kind):
|
|
195
|
+
diags.error(
|
|
196
|
+
stmt.line,
|
|
197
|
+
(kind["odd_message"] if not odd_ok else kind["allowed_message"]).format(v=shown),
|
|
198
|
+
stmt.file,
|
|
199
|
+
)
|
|
200
|
+
return
|
|
201
|
+
if not consteval.is_constant(arg, env):
|
|
202
|
+
return # a variable here is already rejected by the generic const-expr parameter (type
|
|
203
|
+
# code 1's own "Variable illegal in Parameter" -- docs/type-codes.md); not our job.
|
|
204
|
+
try:
|
|
205
|
+
value = consteval.value_of(arg, env)
|
|
206
|
+
except consteval.NotConstant:
|
|
207
|
+
return
|
|
208
|
+
if not consteval.is_finite_number(value):
|
|
209
|
+
return
|
|
210
|
+
if kind.get("truncate") and math.isfinite(value):
|
|
211
|
+
value = int(value) # (CR1000X CurrentSE `402.5` is RG2, PulseCount `108.5` C8, round 31)
|
|
212
|
+
if kind.get("skip_nonpositive") and value <= 0:
|
|
213
|
+
# CR300/CR350 (docs/loggers.md): a channel <= 0 is accepted far outside any range this
|
|
214
|
+
# checker would derive with confidence (oracle: DiffChan -4 compiles with a 1..3 range,
|
|
215
|
+
# -100 fails as "Channel 99"; -1 also compiles for an SEChan that never takes negative
|
|
216
|
+
# values per its own CHM doc). Left unchecked rather than guessed, per AGENTS.md's "never
|
|
217
|
+
# falsely reject" priority -- every corpus mismatch this rule fixes is a plain positive
|
|
218
|
+
# channel number written where a different range or terminal name was required.
|
|
219
|
+
return
|
|
220
|
+
if _cr6_burst_kind(kind) and value <= -200 and not -212 <= value <= -201:
|
|
221
|
+
return # CR6 -200 and -213 and below behave irregularly (accepted, odd-start or port messages)
|
|
222
|
+
spec = kind.get("span") if reps is not None else None
|
|
223
|
+
top = max(hi for _, hi in _ranges(kind))
|
|
224
|
+
if spec is not None and not spec.get("start_upper", True) and value > top:
|
|
225
|
+
# CR6: a start channel above U12 is reported as the span (`250`: `Channel 50`, round 31)
|
|
226
|
+
if kind.get("odd_only") and int(value) % 2 != 1:
|
|
227
|
+
diags.error(stmt.line, kind["odd_message"].format(v=int(value)), stmt.file)
|
|
228
|
+
else:
|
|
229
|
+
diags.error(stmt.line, spec["message"].format(v=int(value) + spec.get("shift", 0)), stmt.file)
|
|
230
|
+
return
|
|
231
|
+
in_range, odd_ok, shown = _evaluate(value + span, kind)
|
|
232
|
+
allowed_ok = "allowed" not in kind or (in_range and shown in kind["allowed"])
|
|
233
|
+
if not in_range:
|
|
234
|
+
diags.error(stmt.line, kind["message"].format(v=shown), stmt.file)
|
|
235
|
+
if not kind.get("secondary_when_invalid"):
|
|
236
|
+
return
|
|
237
|
+
if not odd_ok:
|
|
238
|
+
diags.error(stmt.line, kind["odd_message"].format(v=shown), stmt.file)
|
|
239
|
+
elif not allowed_ok:
|
|
240
|
+
diags.error(stmt.line, kind["allowed_message"].format(v=shown), stmt.file)
|
|
241
|
+
elif spec is not None and reps is not None and reps > 1 and (value > 0 or not in_range):
|
|
242
|
+
# the last repetition's start channel must stay in the start's own range; the message
|
|
243
|
+
# shows the first channel past it (`spec`: own `ranges`, `step`, display `shift`). CR300/
|
|
244
|
+
# CR350 PulseCount spans an invalid start from `invalid_start` (round 31)
|
|
245
|
+
start = value if in_range else spec["invalid_start"]
|
|
246
|
+
last = start + (reps - 1) * width * spec.get("step", 1)
|
|
247
|
+
end = next(hi for lo, hi in _ranges(kind) if lo <= start <= hi)
|
|
248
|
+
if last > end or not _in_range(last, spec if "ranges" in spec else kind):
|
|
249
|
+
diags.error(stmt.line, spec["message"].format(v=int(end) + 1 + spec.get("shift", 0)), stmt.file)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""`-u`: `%s declared but not used.` (`docs/cli.md` "-u").
|
|
2
|
+
|
|
3
|
+
Oracle-confirmed this session (`tools/tdf_probe.py <snippet> --extra=-u`, real `CR6Comp.exe` under
|
|
4
|
+
Wine, cross-checked against `cr1000xcomp.exe`/`cr300comp.exe`/`cr350comp.exe` on the same snippet --
|
|
5
|
+
all four loggers print the identical messages in the identical format, so this module has no
|
|
6
|
+
per-logger branching):
|
|
7
|
+
|
|
8
|
+
- Flags a `Public`/`Dim` variable (the whole array/`String`, never a single element -- any
|
|
9
|
+
reference to *any* element marks the whole declaration used) and a `Sub`/`Function` parameter
|
|
10
|
+
that is never referenced anywhere else in the program text, each as `line N: Warning: %s declared
|
|
11
|
+
but not used.`.
|
|
12
|
+
- Also flags a `Sub` **or** `Function` that is never referenced anywhere else in the program, as a
|
|
13
|
+
*bare* `Warning: %s declared but not used.` -- no `line N:` prefix, the same "no line" shape as
|
|
14
|
+
the pre-existing "table declared but never called" warning (`checker/warnings.py`). An initial
|
|
15
|
+
reading of one oracle probe (an uncalled `Function` whose body sets its return value the usual
|
|
16
|
+
way, `MyFunc = value`) looked like an uncalled `Function` is *never* flagged, unlike an uncalled
|
|
17
|
+
`Sub` -- corrected once the real corpus (`ShuntLib.crb`'s `SampleShunt`, uncalled in
|
|
18
|
+
`Main.CR6`/`PowerBudget_tester.CR6`/`Preflight_tester.CR6` because its only call site sits behind
|
|
19
|
+
a false `#If`) turned out to *be* flagged: `MyFunc = value` inside the function's own body is
|
|
20
|
+
itself a reference to `MyFunc` (an ordinary `Assignment.target`, walked like any other), which is
|
|
21
|
+
what suppressed the warning in the first probe, not the `Function`/`Sub` distinction; a second
|
|
22
|
+
probe with `Return value` instead of self-assignment confirmed an uncalled `Function` *is*
|
|
23
|
+
flagged identically to an uncalled `Sub` once nothing else references its name.
|
|
24
|
+
- **Not** flagged: `Const`/`ConstTable` fields and an `Alias` name. Aliasing merges the alias name
|
|
25
|
+
and the underlying declared variable into one usage identity: a reference to the alias name
|
|
26
|
+
elsewhere in the program counts as using the original variable (oracle: `Public X` / `Alias X =
|
|
27
|
+
Y` / `Y = 1` -- `X` is *not* flagged), but the `Alias`/`Units`/`ReadOnly` statement's own mention
|
|
28
|
+
of a name is never itself a use (oracle: `Public X` / `Alias X = Y` alone, `Y` never referenced
|
|
29
|
+
again -- `X` *is* flagged; likewise a bare `Units X = Label` or `ReadOnly X` with no other
|
|
30
|
+
reference to `X` still flags it). A variable referenced only as a `DataTable` output source (a
|
|
31
|
+
`Sample`/`Average`/... argument) counts as used.
|
|
32
|
+
- The whole check is suppressed by *any* compile error anywhere in the file, not just the ones
|
|
33
|
+
after a given declaration (oracle: a single unrelated error anywhere removes every `-u` warning).
|
|
34
|
+
`compiler.compile_file` only calls `check` after the ordinary checker has run and found no
|
|
35
|
+
errors; this module does not re-check that itself.
|
|
36
|
+
|
|
37
|
+
Scope/limits: this project's syntax tree keeps block structure flat (`syntax.py`'s own docstring)
|
|
38
|
+
and does not model lexical scope -- like `checker/symbols.py`'s `declared_names`, a name here is
|
|
39
|
+
"used" if referenced *anywhere* in the program, including inside a different `Sub`/`Function` than
|
|
40
|
+
the one that declares it. Not independently oracle-tested against real per-scope shadowing (not
|
|
41
|
+
observed in this project's corpus). `Static` declarations were not investigated (rejected as
|
|
42
|
+
invalid syntax by the CR6 grammar in every oracle probe tried, and unused in `crbasic/`).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from .. import syntax as S
|
|
46
|
+
from ..diagnostics import Diagnostics
|
|
47
|
+
|
|
48
|
+
# Instructions that mention a variable's name as metadata, not as a real reference (oracle-
|
|
49
|
+
# confirmed for all three: `Alias X = Y`, `Units X = Label`, `ReadOnly X` never count as "using"
|
|
50
|
+
# `X` on their own).
|
|
51
|
+
_METADATA_ONLY = frozenset({"ALIAS", "UNITS", "READONLY"})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _base_name(expr: S.Expr | None) -> str | None:
|
|
55
|
+
"""The identifier a bare `Name` or single-element `Index` (`Arr(3)`) expression names, for
|
|
56
|
+
resolving an `Alias` statement's own operands -- `None` for anything else (a swath/complex
|
|
57
|
+
alias target, `docs/tdf-rules.md` §11 open question, is left unmerged rather than guessed at).
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(expr, S.Name):
|
|
60
|
+
return expr.name
|
|
61
|
+
if isinstance(expr, S.Index):
|
|
62
|
+
return expr.name
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _walk_expr(expr: S.Expr | None, names: set[str]) -> None:
|
|
67
|
+
if expr is None:
|
|
68
|
+
return
|
|
69
|
+
if isinstance(expr, S.Name):
|
|
70
|
+
names.add(expr.name.upper())
|
|
71
|
+
elif isinstance(expr, S.Index):
|
|
72
|
+
names.add(expr.name.upper())
|
|
73
|
+
for a in expr.args:
|
|
74
|
+
_walk_expr(a, names)
|
|
75
|
+
elif isinstance(expr, S.Member):
|
|
76
|
+
_walk_expr(expr.base, names)
|
|
77
|
+
for a in expr.args or ():
|
|
78
|
+
_walk_expr(a, names)
|
|
79
|
+
elif isinstance(expr, S.Unary):
|
|
80
|
+
_walk_expr(expr.operand, names)
|
|
81
|
+
elif isinstance(expr, S.Binary):
|
|
82
|
+
_walk_expr(expr.left, names)
|
|
83
|
+
_walk_expr(expr.right, names)
|
|
84
|
+
elif isinstance(expr, S.ArrayLiteral):
|
|
85
|
+
for item in expr.items:
|
|
86
|
+
_walk_expr(item, names)
|
|
87
|
+
# Number/String: no name to collect.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _walk_stmt(stmt: S.Stmt, names: set[str]) -> None:
|
|
91
|
+
"""Every name a statement *references* (not declares) -- feeds the "used anywhere" set that
|
|
92
|
+
every declared Public/Dim/param/Sub name is checked against. `Declaration`'s own `VarDecl.name`
|
|
93
|
+
and `SubDef`'s own `name`/`Param.name` are never walked here (they are declarations, callers
|
|
94
|
+
collect those separately); a `VarDecl`'s `dims`/`init`/`string_size` and a `Param`'s `default`/
|
|
95
|
+
`string_size` genuinely can reference some *other* variable, so those are walked.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(stmt, S.Declaration):
|
|
98
|
+
for v in stmt.vars:
|
|
99
|
+
for d in v.dims:
|
|
100
|
+
_walk_expr(d, names)
|
|
101
|
+
for e in v.init or ():
|
|
102
|
+
_walk_expr(e, names)
|
|
103
|
+
_walk_expr(v.string_size, names)
|
|
104
|
+
elif isinstance(stmt, S.ConstDecl):
|
|
105
|
+
_walk_expr(stmt.value, names)
|
|
106
|
+
elif isinstance(stmt, S.Assignment):
|
|
107
|
+
_walk_expr(stmt.target, names)
|
|
108
|
+
_walk_expr(stmt.value, names)
|
|
109
|
+
elif isinstance(stmt, S.Instruction):
|
|
110
|
+
if stmt.keyword.upper() in _METADATA_ONLY:
|
|
111
|
+
return
|
|
112
|
+
for a in stmt.args:
|
|
113
|
+
_walk_expr(a, names)
|
|
114
|
+
elif isinstance(stmt, S.If):
|
|
115
|
+
_walk_expr(stmt.cond, names)
|
|
116
|
+
for s2 in stmt.inline or ():
|
|
117
|
+
_walk_stmt(s2, names)
|
|
118
|
+
for s2 in stmt.inline_else or ():
|
|
119
|
+
_walk_stmt(s2, names)
|
|
120
|
+
elif isinstance(stmt, S.ElseIf):
|
|
121
|
+
_walk_expr(stmt.cond, names)
|
|
122
|
+
elif isinstance(stmt, S.For):
|
|
123
|
+
_walk_expr(stmt.var, names)
|
|
124
|
+
_walk_expr(stmt.start, names)
|
|
125
|
+
_walk_expr(stmt.end, names)
|
|
126
|
+
_walk_expr(stmt.step, names)
|
|
127
|
+
elif isinstance(stmt, S.DoLoop):
|
|
128
|
+
_walk_expr(stmt.cond, names)
|
|
129
|
+
elif isinstance(stmt, S.Case):
|
|
130
|
+
for item in stmt.items:
|
|
131
|
+
for part in item:
|
|
132
|
+
if isinstance(part, S.Expr):
|
|
133
|
+
_walk_expr(part, names)
|
|
134
|
+
elif isinstance(stmt, S.Call):
|
|
135
|
+
names.add(stmt.name.upper()) # `Call MySub(...)` / bare `MySub(...)`: a real call.
|
|
136
|
+
for a in stmt.args:
|
|
137
|
+
_walk_expr(a, names)
|
|
138
|
+
elif isinstance(stmt, S.SubDef):
|
|
139
|
+
for p in stmt.params:
|
|
140
|
+
_walk_expr(p.default, names)
|
|
141
|
+
_walk_expr(p.string_size, names)
|
|
142
|
+
# If/ElseIf/For/DoLoop/Case/Label/Unparsed themselves: nothing else to collect.
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def check(program: S.Program, diags: Diagnostics) -> None:
|
|
146
|
+
"""Add every `-u` warning for `program` to `diags`. Caller's responsibility (`compiler.py`):
|
|
147
|
+
only call this when `not diags.errors` (oracle-confirmed gate, see this module's docstring).
|
|
148
|
+
"""
|
|
149
|
+
variables: list[tuple[str, int, str, str]] = [] # (NAME upper, line, display, file)
|
|
150
|
+
params: list[tuple[str, int, str, str]] = []
|
|
151
|
+
subs: list[tuple[str, str, str]] = [] # (NAME upper, display, file)
|
|
152
|
+
alias_map: dict[str, str] = {} # ALIAS-TARGET upper -> ORIGINAL-BASE upper
|
|
153
|
+
|
|
154
|
+
for stmt in program.statements:
|
|
155
|
+
if isinstance(stmt, S.Declaration) and stmt.kind.upper() in ("PUBLIC", "DIM"):
|
|
156
|
+
for v in stmt.vars:
|
|
157
|
+
variables.append((v.name.upper(), stmt.line, v.name, stmt.file))
|
|
158
|
+
elif isinstance(stmt, S.SubDef):
|
|
159
|
+
for p in stmt.params:
|
|
160
|
+
params.append((p.name.upper(), stmt.line, p.name, stmt.file))
|
|
161
|
+
subs.append((stmt.name.upper(), stmt.name, stmt.file))
|
|
162
|
+
elif isinstance(stmt, S.Instruction) and stmt.keyword.upper() == "ALIAS" and stmt.args:
|
|
163
|
+
original = _base_name(stmt.args[0])
|
|
164
|
+
if original:
|
|
165
|
+
for target in stmt.args[1:]:
|
|
166
|
+
name = _base_name(target)
|
|
167
|
+
if name:
|
|
168
|
+
alias_map[name.upper()] = original.upper()
|
|
169
|
+
|
|
170
|
+
used: set[str] = set()
|
|
171
|
+
for stmt in program.statements:
|
|
172
|
+
_walk_stmt(stmt, used)
|
|
173
|
+
for name in list(used):
|
|
174
|
+
mapped = alias_map.get(name)
|
|
175
|
+
if mapped:
|
|
176
|
+
used.add(mapped)
|
|
177
|
+
|
|
178
|
+
for upper, line, display, file in variables:
|
|
179
|
+
if upper not in used:
|
|
180
|
+
diags.warning(line, f"{display} declared but not used.", file)
|
|
181
|
+
for upper, line, display, file in params:
|
|
182
|
+
if upper not in used:
|
|
183
|
+
diags.warning(line, f"{display} declared but not used.", file)
|
|
184
|
+
for upper, display, file in subs:
|
|
185
|
+
if upper not in used:
|
|
186
|
+
diags.warning(0, f"{display} declared but not used.", file)
|