opencrbasic 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. opencrbasic/__init__.py +3 -0
  2. opencrbasic/__main__.py +5 -0
  3. opencrbasic/analysis.py +184 -0
  4. opencrbasic/bindings.py +1034 -0
  5. opencrbasic/catalog.py +173 -0
  6. opencrbasic/checkconfig.py +144 -0
  7. opencrbasic/checker/__init__.py +977 -0
  8. opencrbasic/checker/cr3xx.py +3118 -0
  9. opencrbasic/checker/ess.py +81 -0
  10. opencrbasic/checker/expressions.py +3581 -0
  11. opencrbasic/checker/instr_data.py +1802 -0
  12. opencrbasic/checker/instr_measure.py +1547 -0
  13. opencrbasic/checker/instr_rules.py +3540 -0
  14. opencrbasic/checker/instructions.py +17 -0
  15. opencrbasic/checker/intervals.py +678 -0
  16. opencrbasic/checker/longconv.py +133 -0
  17. opencrbasic/checker/scan_time.py +580 -0
  18. opencrbasic/checker/storage.py +456 -0
  19. opencrbasic/checker/structs.py +240 -0
  20. opencrbasic/checker/structure.py +6213 -0
  21. opencrbasic/checker/symbols.py +609 -0
  22. opencrbasic/checker/terminals.py +249 -0
  23. opencrbasic/checker/unused.py +186 -0
  24. opencrbasic/checker/var_types.py +564 -0
  25. opencrbasic/checker/warnings.py +661 -0
  26. opencrbasic/checks.py +491 -0
  27. opencrbasic/cli.py +391 -0
  28. opencrbasic/commands.py +128 -0
  29. opencrbasic/compiler.py +320 -0
  30. opencrbasic/consteval.py +327 -0
  31. opencrbasic/data/background_calibration.json +4 -0
  32. opencrbasic/data/before_beginprog.json +99 -0
  33. opencrbasic/data/cr1000x/background_calibration.json +4 -0
  34. opencrbasic/data/cr1000x/before_beginprog.json +138 -0
  35. opencrbasic/data/cr1000x/ess_variables.json +1087 -0
  36. opencrbasic/data/cr1000x/fnotch.json +29 -0
  37. opencrbasic/data/cr1000x/functions.json +1337 -0
  38. opencrbasic/data/cr1000x/instructions.json +4836 -0
  39. opencrbasic/data/cr1000x/measure.json +121 -0
  40. opencrbasic/data/cr1000x/mode.json +111 -0
  41. opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
  42. opencrbasic/data/cr1000x/settings_fields.json +321 -0
  43. opencrbasic/data/cr1000x/status_members.json +318 -0
  44. opencrbasic/data/cr1000x/symbols.json +872 -0
  45. opencrbasic/data/cr1000x/terminals.json +225 -0
  46. opencrbasic/data/cr300/before_beginprog.json +42 -0
  47. opencrbasic/data/cr300/ess_variables.json +1087 -0
  48. opencrbasic/data/cr300/fnotch.json +22 -0
  49. opencrbasic/data/cr300/functions.json +1261 -0
  50. opencrbasic/data/cr300/instructions.json +2837 -0
  51. opencrbasic/data/cr300/measure.json +33 -0
  52. opencrbasic/data/cr300/mode.json +121 -0
  53. opencrbasic/data/cr300/settings_fields.json +259 -0
  54. opencrbasic/data/cr300/status_members.json +303 -0
  55. opencrbasic/data/cr300/symbols.json +584 -0
  56. opencrbasic/data/cr300/terminals.json +193 -0
  57. opencrbasic/data/cr350/before_beginprog.json +44 -0
  58. opencrbasic/data/cr350/ess_variables.json +1087 -0
  59. opencrbasic/data/cr350/fnotch.json +22 -0
  60. opencrbasic/data/cr350/functions.json +1262 -0
  61. opencrbasic/data/cr350/instructions.json +2848 -0
  62. opencrbasic/data/cr350/measure.json +35 -0
  63. opencrbasic/data/cr350/mode.json +121 -0
  64. opencrbasic/data/cr350/settings_fields.json +292 -0
  65. opencrbasic/data/cr350/status_members.json +303 -0
  66. opencrbasic/data/cr350/symbols.json +572 -0
  67. opencrbasic/data/cr350/terminals.json +190 -0
  68. opencrbasic/data/ess_variables.json +1087 -0
  69. opencrbasic/data/fnotch.json +34 -0
  70. opencrbasic/data/functions.json +1337 -0
  71. opencrbasic/data/instructions.json +4965 -0
  72. opencrbasic/data/measure.json +123 -0
  73. opencrbasic/data/mode.json +121 -0
  74. opencrbasic/data/notices_cr6.json +890 -0
  75. opencrbasic/data/pipeline_tasks.json +84 -0
  76. opencrbasic/data/settings_fields.json +322 -0
  77. opencrbasic/data/status_members.json +319 -0
  78. opencrbasic/data/status_table.json +684 -0
  79. opencrbasic/data/symbols.json +902 -0
  80. opencrbasic/data/terminals.json +195 -0
  81. opencrbasic/data/type_codes.json +249 -0
  82. opencrbasic/diagnostics.py +196 -0
  83. opencrbasic/editing.py +827 -0
  84. opencrbasic/formatting.py +310 -0
  85. opencrbasic/helpdocs.py +677 -0
  86. opencrbasic/hover.py +667 -0
  87. opencrbasic/inlay.py +169 -0
  88. opencrbasic/lexer.py +183 -0
  89. opencrbasic/lsp/__init__.py +5 -0
  90. opencrbasic/lsp/__main__.py +7 -0
  91. opencrbasic/lsp/config.py +202 -0
  92. opencrbasic/lsp/features.py +602 -0
  93. opencrbasic/lsp/server.py +741 -0
  94. opencrbasic/lsp/session.py +1189 -0
  95. opencrbasic/lsp/worker.py +128 -0
  96. opencrbasic/mode.py +143 -0
  97. opencrbasic/navigation.py +401 -0
  98. opencrbasic/parser.py +3264 -0
  99. opencrbasic/positions.py +274 -0
  100. opencrbasic/preprocess.py +269 -0
  101. opencrbasic/profiles.py +266 -0
  102. opencrbasic/provenance.py +17 -0
  103. opencrbasic/quickfix.py +331 -0
  104. opencrbasic/rename.py +662 -0
  105. opencrbasic/signature.py +33 -0
  106. opencrbasic/source.py +600 -0
  107. opencrbasic/sources.py +125 -0
  108. opencrbasic/syntax.py +337 -0
  109. opencrbasic/tables.py +2173 -0
  110. opencrbasic/tables_hook.py +14 -0
  111. opencrbasic/tdf.py +142 -0
  112. opencrbasic/timing.py +565 -0
  113. opencrbasic-1.0.0.dist-info/METADATA +223 -0
  114. opencrbasic-1.0.0.dist-info/RECORD +117 -0
  115. opencrbasic-1.0.0.dist-info/WHEEL +4 -0
  116. opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
  117. opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
opencrbasic/timing.py ADDED
@@ -0,0 +1,565 @@
1
+ """`-t`: the `[Times]` section CR6Comp writes to the message file -- one `line N: <label>` per
2
+ timed statement. Every rule and its evidence is in `docs/cli.md` "Timing output (-t)"; coverage is
3
+ only what oracle probing established, anything else (non-constant arguments, other instructions)
4
+ prints no entry.
5
+ """
6
+
7
+ import re
8
+ from typing import TypeGuard, cast
9
+
10
+ from . import consteval, mode
11
+ from . import syntax as S
12
+ from .profiles import Profile
13
+
14
+
15
+ def _const(expr: S.Expr | None, env: consteval.Env) -> object | None:
16
+ try:
17
+ return consteval.value_of(expr, env)
18
+ except consteval.NotConstant:
19
+ return None
20
+
21
+
22
+ def _num(v: object | None) -> float | None:
23
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
24
+ return float(v)
25
+ return None
26
+
27
+
28
+ def _round_half_up(x: float) -> int:
29
+ # C's usual `(int)(x + 0.5)` for a non-negative `x` -- avoids Python's round-half-to-even,
30
+ # which would silently disagree with the exe at an exact `.5` (docs/cli.md "-t").
31
+ return int(x + 0.5)
32
+
33
+
34
+ def _flatten(stmts):
35
+ for stmt in stmts:
36
+ yield stmt
37
+ if isinstance(stmt, S.If):
38
+ yield from _flatten(stmt.inline or [])
39
+ yield from _flatten(stmt.inline_else or [])
40
+
41
+
42
+ def times_lines(program: S.Program, profile: Profile, main_file: str) -> list[str]:
43
+ """The `[Times]` section's entries (without the `[Times]` header itself), in program order.
44
+
45
+ `main_file` is accepted for backwards compatibility but no longer used to filter statements
46
+ (see the module docstring's last paragraph); kept so callers do not need to change.
47
+ """
48
+ del main_file
49
+ env = consteval.Env(profile, dict(program.consts))
50
+ try:
51
+ pipeline = (
52
+ mode.decide(
53
+ program.statements,
54
+ profile,
55
+ lambda e: consteval.is_constant(e, env),
56
+ lambda e: consteval.value_of(e, env),
57
+ )
58
+ == "Pipeline"
59
+ )
60
+ except Exception:
61
+ pipeline = False
62
+ lines: list[str] = []
63
+ scan_count = 0
64
+ inside = False # within a Scan(...)'s own span (see module docstring)
65
+ in_sub = False
66
+ prev_kw = ""
67
+ for stmt in _flatten(program.statements):
68
+ if isinstance(stmt, S.SubDef):
69
+ in_sub = True
70
+ continue
71
+ if not isinstance(stmt, S.Instruction):
72
+ continue
73
+ kw = stmt.keyword
74
+ if S.ends_routine(stmt):
75
+ in_sub = False
76
+ if kw == "SCAN":
77
+ if prev_kw != "SLOWSEQUENCE":
78
+ if scan_count:
79
+ lines.append("")
80
+ lines.append(f"line {stmt.line}: Scan: {scan_count}" + (" (Subroutine)" if in_sub else ""))
81
+ scan_count += 1
82
+ inside = True
83
+ else:
84
+ inside = False
85
+ prev_kw = kw
86
+ continue
87
+ if kw == "NEXTSCAN":
88
+ inside = False
89
+ prev_kw = kw
90
+ continue
91
+ prev_kw = kw
92
+ if not inside:
93
+ continue
94
+ model = _model(profile)
95
+ if kw == "PORTSET" and len(stmt.args) == 2:
96
+ lines.append(f"line {stmt.line}: PortSet = 100")
97
+ elif kw == "SW12" and model != "cr3xx":
98
+ lines.append(f"line {stmt.line}: Sw12 = 100")
99
+ elif kw == "SWVX" and model != "cr6":
100
+ lines.append(f"line {stmt.line}: SwVx = 100")
101
+ elif kw == "BATTERY" and model == "cr1000x":
102
+ lines.append(f"line {stmt.line}: Battery = 800")
103
+ elif kw == "PANELTEMP" and model != "cr3xx":
104
+ _paneltemp(stmt, env, lines, 12 if model == "cr1000x" else 10)
105
+ elif kw in ("DELAY", "MUXSELECT") and model == "cr3xx":
106
+ pass
107
+ elif kw == "DELAY":
108
+ _delay(stmt, env, lines, pipeline)
109
+ elif kw in _MEASURE:
110
+ _measure(stmt, env, lines)
111
+ elif kw in _THERM:
112
+ _therm(stmt, env, lines)
113
+ elif kw in _BRIDGE:
114
+ _bridge(stmt, env, lines)
115
+ elif kw == "AM25T":
116
+ _am25t(stmt, env, lines)
117
+ elif kw in _CLOSED:
118
+ _closed(stmt, env, lines, pipeline)
119
+ return lines
120
+
121
+
122
+ def _model(profile: Profile) -> str:
123
+ """Which timing cost model a logger follows: the CR6, the CR1000X, or the CR300/CR350."""
124
+ return {"cr6": "cr6", "cr1000x": "cr1000x"}.get(profile.name, "cr3xx")
125
+
126
+
127
+ def _paneltemp(stmt: S.Instruction, env: consteval.Env, lines: list[str], base: int = 10) -> None:
128
+ if len(stmt.args) < 2:
129
+ return
130
+ timeout = _num(_const(stmt.args[1], env))
131
+ if not timeout or timeout <= 0:
132
+ return
133
+ cost = (base + _round_half_up(10000.0 / timeout)) * 100
134
+ lines.append(f"line {stmt.line}: PanelTemp = {cost}")
135
+ lines.append(f"line {stmt.line}: PanelTemp = {cost}")
136
+
137
+
138
+ _UNIT_US = {"USEC": 1, "MSEC": 1000, "SEC": 1000000, "MIN": 60000000}
139
+
140
+
141
+ def _signed32(x: int) -> int:
142
+ x &= 0xFFFFFFFF
143
+ return x - (1 << 32) if x >= 1 << 31 else x
144
+
145
+
146
+ def _floor100(x: int) -> int:
147
+ """`x` rounded down to a multiple of 100 in unsigned 32-bit arithmetic, shown signed (a Delay
148
+ of -5 mSec prints -4996; docs/cli.md)."""
149
+ return _signed32((x & 0xFFFFFFFF) // 100 * 100)
150
+
151
+
152
+ def _trunc100(x: int) -> int:
153
+ """Measurement costs: the sum wraps to signed 32 bits, then is truncated toward zero to a
154
+ multiple of 100 (a 1,000,000-rep VoltSe prints -109967100; docs/cli.md)."""
155
+ v = _signed32(x)
156
+ return v // 100 * 100 if v >= 0 else -(-v // 100 * 100)
157
+
158
+
159
+ def _delay(stmt: S.Instruction, env: consteval.Env, lines: list[str], pipeline: bool) -> None:
160
+ # Timed only for RepDelay == 0 in PipelineMode; units case-insensitive (docs/cli.md).
161
+ if len(stmt.args) < 3 or not pipeline:
162
+ return
163
+ value = _num(_const(stmt.args[1], env))
164
+ units = stmt.args[2]
165
+ if _num(_const(stmt.args[0], env)) != 0 or not value:
166
+ return
167
+ if not isinstance(units, S.Name) or units.name.upper() not in _UNIT_US:
168
+ return
169
+ cost = _floor100(int(value * _UNIT_US[units.name.upper()]) + 100)
170
+ lines.append(f"line {stmt.line}: Delay = {cost}")
171
+
172
+
173
+ # (reps, range, channel, flag, settle, integ) argument positions; `flag_adds` is True when the
174
+ # flag (MeasOff / RevDiff) adds one measurement per rep to a single-ended / doubles a differential.
175
+ _MEASURE = {
176
+ "VOLTSE": (1, 2, 3, 4, 5, 6, False),
177
+ "VOLTDIFF": (1, 2, 3, 4, 5, 6, True),
178
+ "TCSE": (1, 2, 3, 6, 7, 8, False),
179
+ "TCDIFF": (1, 2, 3, 6, 7, 8, True),
180
+ }
181
+ _PLAIN_RANGES = {800, 802, 804}
182
+ _C_RANGES = {820, 822, 824}
183
+ _AUTO_RANGES = {810, 830} # AutoRange, AutoRangeC
184
+
185
+
186
+ def _measure(stmt: S.Instruction, env: consteval.Env, lines: list[str]) -> None:
187
+ """VoltSe/VoltDiff/TCSe/TCDiff (docs/cli.md "Voltage and thermocouple cost"). Not modelled
188
+ (no entry): AutoRange, non-constant arguments, negative settle/integ."""
189
+ ri, gi, ci, fi, si, ii, doubling = _MEASURE[stmt.keyword]
190
+ if len(stmt.args) <= ii:
191
+ return
192
+ model = _model(env.profile)
193
+ if model != "cr6":
194
+ reps, rng, flag, settle, integ = (_num(_const(stmt.args[k], env)) for k in (ri, gi, fi, si, ii))
195
+ if (
196
+ (reps is None or rng is None or flag is None or settle is None or integ is None)
197
+ or reps < 1
198
+ or settle < 0
199
+ or integ < 0
200
+ ):
201
+ return
202
+ chan_n = _num(_const(stmt.args[ci], env))
203
+ if chan_n is None:
204
+ return
205
+ reps = int(reps)
206
+ if model == "cr3xx":
207
+ cost = (
208
+ _c3_cost(1 + (1 if flag else 0), reps, settle)
209
+ if doubling
210
+ else _c3_cost(1, reps, settle, 1 if flag else 0)
211
+ )
212
+ else:
213
+ if rng not in _PLAIN_RANGES and rng not in _C_RANGES and rng not in _AUTO_RANGES:
214
+ return
215
+ n = reps * (2 if flag else 1) if doubling else reps + (1 if flag else 0)
216
+ c = rng in _C_RANGES or rng == 830
217
+ valid = _x1000_chan(chan_n, doubling)
218
+ if valid is None:
219
+ return
220
+ auto = (n if doubling else reps) if rng in _AUTO_RANGES else 0
221
+ cost = _x1000_cost(n, settle, integ, c, reps, auto, c and bool(valid))
222
+ lines.append(f"line {stmt.line}: {_LABEL[stmt.keyword]} = {cost}")
223
+ return
224
+ reps, rng, flag, settle, integ = (_num(_const(stmt.args[k], env)) for k in (ri, gi, fi, si, ii))
225
+ chan = stmt.args[ci]
226
+ um = re.fullmatch(r"U(\d+)", chan.name.upper()) if isinstance(chan, S.Name) else None
227
+ u_chan = um is not None and (not doubling or int(um.group(1)) % 2 == 1) # even U is no differential pair
228
+ if um is not None and not u_chan:
229
+ chan = S.Number(cast(S.Name, chan).line, value=0)
230
+ if not u_chan and _num(_const(chan, env)) is None:
231
+ return
232
+ if (
233
+ (reps is None or rng is None or flag is None or settle is None or integ is None)
234
+ or reps < 1
235
+ or settle < 0
236
+ or integ < 0
237
+ ):
238
+ return
239
+ auto = rng in _AUTO_RANGES
240
+ if rng not in _PLAIN_RANGES and rng not in _C_RANGES and not auto:
241
+ return
242
+ n = reps * (2 if flag else 1) if doubling else reps + (1 if flag else 0)
243
+ extra = 0
244
+ thirds_u = 0
245
+ if rng in _C_RANGES or rng == 830:
246
+ extra = 50 * reps
247
+ if u_chan:
248
+ thirds_u = 160
249
+ cost = _meas_cost(n, settle, integ, extra, thirds_u, (n if doubling else reps) if auto else 0)
250
+ lines.append(f"line {stmt.line}: {_LABEL[stmt.keyword]} = {cost}")
251
+
252
+
253
+ # CR1000X (docs/cli.md "CR1000X and CR300/CR350 timing"): a measurement pass costs 185 + 32*ks +
254
+ # 32*m (us), settle quantum 32 us (ks = (settle+16)//32, 14 when 0), integration m =
255
+ # round(31250/integ) (8 when 0); C ranges add 64 per pass and 50 per rep; AutoRange adds one
256
+ # settling pass (5 + settle part + 64 if C) per measurement.
257
+ def _x1000_parts(settle: float, integ: float) -> tuple[int, int]:
258
+ ks = 14 if settle == 0 else int(settle + 16) // 32
259
+ m = 8 if integ == 0 else max(1, int(31250 / integ + 0.5))
260
+ return 32 * ks, 32 * m
261
+
262
+
263
+ def _x1000_cost(
264
+ n: int, settle: float, integ: float, c: bool = False, c_reps: int = 0, auto_n: int = 0, u: bool = False
265
+ ) -> int:
266
+ """`c`: a C range; `u`: that C range on a valid channel (SE 1..16, differential 1..8), the
267
+ only case the 64 us per pass applies."""
268
+ s_part, x_part = _x1000_parts(settle, integ)
269
+ cost = 180 + n * (185 + (64 if u else 0) + s_part + x_part) + (50 * c_reps if c else 0)
270
+ cost += auto_n * (5 + s_part + (64 if u else 0))
271
+ return _trunc100(cost)
272
+
273
+
274
+ def _x1000_chan(chan: float | None, differential: bool) -> bool | None:
275
+ """Whether `chan` is a valid channel on the CR1000X; None where the cost is not modelled
276
+ (negative channels, channel 0 of a differential instruction)."""
277
+ if chan is None or chan < 0 or (differential and chan == 0):
278
+ return None
279
+ return 1 <= chan <= (8 if differential else 16)
280
+
281
+
282
+ # CR300/CR350: a pass costs 51136 + settle (500 when 0); the fixed part is 50 + 60*m for m passes
283
+ # per rep (RevDiff, RevEx, double-measurement forms); a MeasOff pass is 51021 + settle with no
284
+ # fixed part of its own. Range and integration time have no effect.
285
+ _C3_PASS = 51136
286
+ _C3_OFFSET_PASS = 51021
287
+
288
+
289
+ def _c3_fixed(m: int) -> int:
290
+ return 50 + 60 * m
291
+
292
+
293
+ def _c3_cost(m: int, reps: int, settle: float, offset_pass: int = 0) -> int:
294
+ s = int(settle) or 500
295
+ return _trunc100(_c3_fixed(m) + reps * m * (_C3_PASS + s) + offset_pass * (_C3_OFFSET_PASS + s))
296
+
297
+
298
+ def _meas_cost(n: float, settle: float, integ: float, extra: float = 0, thirds_u: int = 0, auto_reps: float = 0) -> int:
299
+ ks = int((24 * (settle or 500) + 127) // 256)
300
+ kx = 24 if integ == 0 else max(1, int((24e6 / integ + 128) // 256))
301
+ settle_part = -(-(32 * ks + thirds_u) // 3)
302
+ # AutoRange adds one settle-only pass per rep, 1 us cheaper than a measurement's (docs/cli.md).
303
+ return _trunc100(int(140 + extra + n * (816 + settle_part + 32 * kx // 3) + auto_reps * (815 + settle_part)))
304
+
305
+
306
+ # Therm107/108/109(Dest, Reps, SEChan, VxChan, Settle, Integ, Mult, Offset): two measurements per
307
+ # rep, plain range, no channel dependence (docs/cli.md "Thermistor cost").
308
+ _THERM = {"THERM107": "Therm107", "THERM108": "Therm108", "THERM109": "Therm109"}
309
+
310
+
311
+ def _therm(stmt: S.Instruction, env: consteval.Env, lines: list[str]) -> None:
312
+ a = _args(stmt, env, 1, 4, 5)
313
+ if a is None or a[0] < 1 or a[1] < 0 or a[2] < 0:
314
+ return
315
+ model = _model(env.profile)
316
+ n = (2 * int(a[0])) % 512 # the thermistor measurement count wraps at 512 (2 * Reps; docs/cli.md)
317
+ if n == 0:
318
+ return # the real compiler crashes (divide by zero) for Reps = 256, 512, ...
319
+ if model == "cr3xx":
320
+ cost = _c3_cost(2, n // 2, a[1])
321
+ elif model == "cr1000x":
322
+ cost = _x1000_cost(n, a[1], a[2])
323
+ else:
324
+ cost = _meas_cost(n, a[1], a[2])
325
+ lines.append(f"line {stmt.line}: {_THERM[stmt.keyword]} = {cost}")
326
+
327
+
328
+ _LABEL = {"VOLTSE": "VoltSe", "VOLTDIFF": "VoltDiff", "TCSE": "TCSe", "TCDIFF": "TCDiff"}
329
+
330
+
331
+ # Bridge instructions: (label, first range, second range or None, channel, MeasPEx, RevDiff or
332
+ # None, RevEx, settle, integ, base measurements per rep, single-ended, no range dependence,
333
+ # entry pairs). Argument positions after `Dest` (docs/cli.md "Bridge cost").
334
+ _BRIDGE = {
335
+ "BRHALF": ("BrHalf", 2, None, 3, 5, None, 7, 8, 9, 1, True, False),
336
+ "BRHALF3W": ("BrHalf3W", 2, None, 3, 5, None, 7, 8, 9, 2, True, False),
337
+ "BRHALF4W": ("BrHalf4W", 2, 3, 4, 6, 8, 9, 10, 11, 2, False, False),
338
+ "BRFULL": ("BrFull", 2, None, 3, 5, 7, 8, 9, 10, 1, False, False),
339
+ "BRFULL6W": ("BrFull6W", 2, 3, 4, 6, 8, 9, 10, 11, 2, False, False),
340
+ "RESISTANCE": ("Resistance", 2, None, 3, 5, 7, 8, 9, 10, 1, False, True),
341
+ }
342
+
343
+
344
+ def _bridge(stmt: S.Instruction, env: consteval.Env, lines: list[str]) -> None:
345
+ label, g1, g2, ci, mpi, rdi, rei, si, ii, base, single, resistance = _BRIDGE[stmt.keyword]
346
+ if len(stmt.args) <= ii:
347
+ return
348
+ idx = [1, g1, mpi, rei, si, ii] + ([g2] if g2 else []) + ([rdi] if rdi else [])
349
+ vals = [_num(_const(stmt.args[k], env)) for k in idx]
350
+ if None in vals:
351
+ return
352
+ vals = cast(list[float], vals)
353
+ reps, rng, mp, rev_ex, settle, integ = vals[:6]
354
+ rest = vals[6:]
355
+ rng2 = rest.pop(0) if g2 else rng
356
+ rev_diff = rest.pop(0) if rdi else 0
357
+ chan = stmt.args[ci]
358
+ if not _chan_is_u(chan) and _num(_const(chan, env)) is None:
359
+ return
360
+ if reps < 1 or settle < 0 or integ < 0:
361
+ return
362
+ model = _model(env.profile)
363
+ if model != "cr6":
364
+ valid = _x1000_chan(_num(_const(chan, env)), not single) if model == "cr1000x" else None
365
+ if resistance or (model == "cr1000x" and valid is None):
366
+ return
367
+ per = base * (2 if rev_diff else 1) * (2 if rev_ex else 1)
368
+ group = max(int(mp), 1)
369
+ left = int(reps)
370
+ entries = 0
371
+ while left > 0 and (model == "cr1000x" or entries < 3):
372
+ g = min(group, left)
373
+ left -= g
374
+ entries += 1
375
+ if model == "cr3xx":
376
+ cost = _trunc100(_c3_fixed(per) + g * per * (_C3_PASS + (int(settle) or 500)))
377
+ else:
378
+ for r in (rng, rng2):
379
+ if r not in _PLAIN_RANGES and r not in _C_RANGES and r not in _AUTO_RANGES:
380
+ return
381
+ c = any(r in _C_RANGES or r == 830 for r in (rng, rng2))
382
+ auto = per * g if (g2 is None and rng in _AUTO_RANGES) else 0
383
+ cost = _x1000_cost(g * per, settle, integ, c, g * base, auto, c and bool(valid))
384
+ lines.append(f"line {stmt.line}: {label} = {cost}")
385
+ return
386
+ c_range = False
387
+ for r in (rng, rng2):
388
+ if r not in _PLAIN_RANGES and r not in _C_RANGES:
389
+ return
390
+ c_range |= r in _C_RANGES
391
+ if resistance:
392
+ c_range = False
393
+ if mp < 1:
394
+ return
395
+ u = 0
396
+ if (
397
+ c_range
398
+ and _chan_is_u(chan)
399
+ and (single or int(cast(re.Match[str], re.fullmatch(r"U(\d+)", chan.name.upper())).group(1)) % 2 == 1)
400
+ ):
401
+ u = 160
402
+ per = base * (2 if rev_diff else 1) * (2 if rev_ex else 1)
403
+ group = max(int(mp), 1)
404
+ left = int(reps)
405
+ while left > 0:
406
+ g = min(group, left)
407
+ left -= g
408
+ if resistance:
409
+ lines.append(f"line {stmt.line}: {label} = {_meas_cost(per, settle, integ)}")
410
+ extra = 50 * g * base if c_range else 0
411
+ lines.append(f"line {stmt.line}: {label} = {_meas_cost(g * per, settle, integ, extra, u)}")
412
+
413
+
414
+ def _am25t(stmt: S.Instruction, env: consteval.Env, lines: list[str]) -> None:
415
+ """AM25T(Dest, Reps, Range, DiffChan, MuxChan, TCType, Tref, ClkPort, ResPort, VxChan, RevDiff,
416
+ Settle, Integ, Mult, Offset) (docs/cli.md "AM25T cost")."""
417
+ a = _args(stmt, env, 1, 2, 10, 11, 12)
418
+ if a is None or len(stmt.args) < 15:
419
+ return
420
+ reps, rng, rev, settle, integ = a
421
+ chan = stmt.args[3]
422
+ cm = re.fullmatch(r"U(\d+)", chan.name.upper()) if isinstance(chan, S.Name) else None
423
+ cnum = int(cm.group(1)) if cm else _num(_const(chan, env))
424
+ if cnum is None or reps < 1 or settle < 0 or integ < 0:
425
+ return
426
+ auto = rng in _AUTO_RANGES
427
+ c_range = rng in _C_RANGES or rng == 830
428
+ model = _model(env.profile)
429
+ if model != "cr6":
430
+ if cm is not None or not (c_range or auto or rng in _PLAIN_RANGES or model == "cr3xx"):
431
+ return
432
+ m = 2 if rev else 1
433
+ reps = int(reps)
434
+ if model == "cr3xx":
435
+ first = _trunc100(_c3_fixed(m) + m * (_C3_PASS + (int(settle) or 500)))
436
+ total = _trunc100(_c3_fixed(m) + reps * m * (_C3_PASS + 80 + (int(settle) or 500)))
437
+ out = [first] * 3 + [500, total]
438
+ else:
439
+ s_part, x_part = _x1000_parts(settle, integ)
440
+ first = _trunc100(180 + m * (185 + s_part + x_part) + (50 if c_range else 0))
441
+ total = _trunc100(
442
+ 180 + reps * (m * (265 + s_part + x_part) + (50 if c_range else 0) + (m * (85 + s_part) if auto else 0))
443
+ )
444
+ out = [first] * 3 + [300, total]
445
+ out.append(400 + 200 * int(cnum))
446
+ lines.extend(f"line {stmt.line}: AM25T = {v}" for v in out)
447
+ return
448
+ if not (c_range or auto or rng in _PLAIN_RANGES):
449
+ return
450
+ n = 2 if rev else 1
451
+ ks = int((24 * (settle or 500) + 127) // 256)
452
+ kx = 24 if integ == 0 else max(1, int((24e6 / integ + 128) // 256))
453
+ settle_part = -(-32 * ks // 3)
454
+ per = 816 + settle_part + 32 * kx // 3
455
+ out = []
456
+ if _chan_is_u(stmt.args[9]):
457
+ first = _trunc100(140 + n * per + (50 if c_range else 0))
458
+ out = [first] * 3 + [300]
459
+ total = 140 + int(reps) * (n * (per + 80) + (n * (815 + settle_part + 80) if auto else 0) + (50 if c_range else 0))
460
+ out += [_trunc100(total), (40000 if cm else 0) + 400 + 200 * int(cnum)]
461
+ lines.extend(f"line {stmt.line}: AM25T = {v}" for v in out)
462
+
463
+
464
+ def _args(stmt: S.Instruction, env: consteval.Env, *idx: int) -> list[float] | None:
465
+ if len(stmt.args) <= max(idx):
466
+ return None
467
+ vals = [_num(_const(stmt.args[i], env)) for i in idx]
468
+ return None if None in vals else cast(list[float], vals)
469
+
470
+
471
+ def _chan_is_u(expr: S.Expr | None) -> TypeGuard[S.Name]:
472
+ return isinstance(expr, S.Name) and re.fullmatch(r"U\d+", expr.name.upper()) is not None
473
+
474
+
475
+ def _closed(stmt: S.Instruction, env: consteval.Env, lines: list[str], pipeline: bool) -> None:
476
+ """Instructions whose cost is a closed form of one or two constant arguments
477
+ (docs/cli.md "Closed forms"); no entry when an argument is not constant."""
478
+ kw = stmt.keyword
479
+ out: list[int] = []
480
+ label = _CLOSED[kw]
481
+ if kw == "PULSEPORT":
482
+ a = _args(stmt, env, 1)
483
+ out = [_floor100(2 * max(int(a[0]), 0) + 100)] if a else []
484
+ elif kw == "EXCITEV":
485
+ a = _args(stmt, env, 2)
486
+ out = [_floor100(max(int(a[0]), 0) + 100)] if a else []
487
+ elif kw == "PULSECOUNT":
488
+ out = [100] if _args(stmt, env, 1) else []
489
+ elif kw == "CALIBRATE":
490
+ model = _model(env.profile)
491
+ out = {"cr6": [2000, 7700] * 3, "cr1000x": [1500, 5700] * 3}.get(model, [])
492
+ elif kw == "CS616":
493
+ a = _args(stmt, env, 1)
494
+ out = [200 + 500 * int(a[0])] if a else []
495
+ elif kw == "PERIODAVG":
496
+ a = _args(stmt, env, 1, 5 if env.profile.name in ("cr300", "cr350") else 7)
497
+ out = [_signed32(int(a[0]) * int(a[1]) * 1000 + 200)] if a else []
498
+ elif kw == "MUXSELECT":
499
+ a = _args(stmt, env, 2, 3, 4)
500
+ if a and pipeline:
501
+ base = 100 if a[2] == 0 else 13000
502
+ out = [_floor100(base + int(a[1]) * (2000 * int(a[0]) + 50))]
503
+ elif kw == "ACPOWER":
504
+ out = _acpower(stmt, env) if _model(env.profile) == "cr6" else _acpower_x1000(stmt, env)
505
+ lines.extend(f"line {stmt.line}: {label} = {v}" for v in out)
506
+
507
+
508
+ def _acpower(stmt: S.Instruction, env: consteval.Env) -> list[int]:
509
+ # LineFrq 50/60 are special; any other whole-number frequency (a fraction is truncated) follows
510
+ # the closed form of docs/cli.md "ACPower"; below 1 Hz the real exe reports an error.
511
+ a = _args(stmt, env, 1, 2, 9)
512
+ if not a or len(stmt.args) < 10 or not 1 <= a[1] < 1e6:
513
+ return []
514
+ for k in (3, 6):
515
+ if not _chan_is_u(stmt.args[k]) and _num(_const(stmt.args[k], env)) is None:
516
+ return []
517
+ f = int(a[1])
518
+ if f in (50, 60):
519
+ u_cost, n_cost = {50: (41100, 278300), 60: (34500, 231900)}[f]
520
+ head = 61200 if f == 50 else 51200
521
+ else:
522
+ q = 1000 // f
523
+ u_cost, n_cost = (1003 * q + 2171) // 100 * 100, (2021 * q + 2160) // 100 * 100
524
+ head = 1200 + 1000 * (2000 // f)
525
+ volt, cur = (u_cost if _chan_is_u(stmt.args[k]) else n_cost for k in (3, 6))
526
+ cfg = int(a[0])
527
+ seq = {2: [volt, cur, cur], 3: [volt, cur] * 3, 1: [volt] + [cur] * max(int(a[2]), 0)}.get(cfg, [volt, cur])
528
+ return [1000] + [v for x in seq for v in (head, x)]
529
+
530
+
531
+ def _acpower_x1000(stmt: S.Instruction, env: consteval.Env) -> list[int]:
532
+ """CR1000X ACPower: numeric channels only, first entry 400; LineFrq 50/60 are table values,
533
+ 2..20 follow 992*q + c (q = 1000//f; c in [1536,1600) by the data, 1550 used)."""
534
+ if _model(env.profile) != "cr1000x":
535
+ return []
536
+ a = _args(stmt, env, 1, 2, 9)
537
+ if not a or len(stmt.args) < 10:
538
+ return []
539
+ for k in (3, 6):
540
+ if _num(_const(stmt.args[k], env)) is None:
541
+ return []
542
+ f = int(a[1])
543
+ if f == 50:
544
+ head, x = 61200, 40500
545
+ elif f == 60:
546
+ head, x = 51200, 33800
547
+ elif 2 <= a[1] <= 20:
548
+ head, x = 1200 + 1000 * (2000 // f), (992 * (1000 // f) + 1550) // 100 * 100
549
+ else:
550
+ return []
551
+ cfg = int(a[0])
552
+ seq = {2: 3, 3: 6, 1: 1 + max(int(a[2]), 0)}.get(cfg, 2)
553
+ return [400] + [v for _ in range(seq) for v in (head, x)]
554
+
555
+
556
+ _CLOSED = {
557
+ "PULSEPORT": "PulsePort",
558
+ "EXCITEV": "ExciteV",
559
+ "PULSECOUNT": "Pulsecount",
560
+ "CALIBRATE": "Calibrate",
561
+ "CS616": "CS616",
562
+ "PERIODAVG": "PeriodAvg",
563
+ "MUXSELECT": "MuxSelect",
564
+ "ACPOWER": "ACPower",
565
+ }