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
@@ -0,0 +1,580 @@
1
+ """Measurement-time budget of a `Scan` (CR6; delays, ports and pulse counts also CR1000X).
2
+
3
+ Every instruction adds a number of 100 microsecond units to the open `Scan` when it is compiled;
4
+ `NextScan` compares the sum with the interval (`docs/instructions.md`, "Scan time budget").
5
+ `tools/trace_times.py` records the real additions; the models here reproduce them (CR6, 2026-10-10,
6
+ `tests/fixtures/scan_time.json`).
7
+
8
+ Units of 100 us throughout. Instructions the model does not know add an unknown extra, so the
9
+ total of a scan holding one is a lower bound (`partial`).
10
+ """
11
+
12
+ import math
13
+ import re
14
+ import struct
15
+ from dataclasses import dataclass, field
16
+ from typing import cast
17
+
18
+ from .. import consteval
19
+ from .. import syntax as S
20
+ from ..profiles import Profile
21
+
22
+ _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}
23
+ UNIT_FACTOR = {1: 10, 2: 10_000, 3: 600_000, 4: 36_000_000, 5: 864_000_000} # 100 us per unit
24
+ UNIT_NAMES = {0: "uSec", 1: "mSec", 2: "Sec", 3: "Min", 4: "Hr", 5: "Day"}
25
+ SCAN_UNITS = (1, 2, 3)
26
+
27
+
28
+ def delay_us(value: float, unit: int) -> int:
29
+ """The unsigned 32-bit microsecond count of a constant `Delay(0,...)` interval: `value * unit`
30
+ modulo 2^32; outside the Long range the interval is a Double whose low word is read as a
31
+ float32, and a non-finite one counts as 2^31 (oracle, CR6/CR1000X, round 57,
32
+ docs/checker.md#task-memory-overflow-of-long-delays-round-57)."""
33
+ if not -(2**31) <= value < 2**31:
34
+ value = struct.unpack("<f", struct.pack("<d", float(value))[:4])[0]
35
+ if not math.isfinite(value):
36
+ return 2**31
37
+ return int(value * _US_PER_UNIT[unit]) % 2**32
38
+
39
+
40
+ def excite_delay_us(value: float) -> int:
41
+ """`ExciteV`'s `Delay`: the Long conversion as unsigned 32 bits (same section)."""
42
+ return consteval.to_long(value) % 2**32
43
+
44
+
45
+ def task_memory_overflow(us: int) -> bool:
46
+ """`FUN_00522b50` splits a delay into 43000 us loops with a signed 16-bit count; a negative
47
+ count unrolls it past the 5119 task words (same section)."""
48
+ return bool((us // 43000) & 0x8000)
49
+
50
+
51
+ def amount(excess: int, unit: int) -> int:
52
+ """`FUN_0046c5c0`: an excess in 100 us units as a count of `unit`, rounded up (uSec: x100)."""
53
+ return excess * 100 if unit == 0 else -(-excess // UNIT_FACTOR[unit])
54
+
55
+
56
+ _OPENERS = frozenset({"SELECT", "WHILE"})
57
+ _CLOSERS = frozenset({"ENDIF", "ENDSELECT", "WEND"})
58
+ # measurement ranges (FUN_00514690): code -> (gain index, open-detect "C" variant)
59
+ _RANGES = {
60
+ 800: (0, False),
61
+ 802: (1, False),
62
+ 804: (2, False),
63
+ 810: (3, False),
64
+ 820: (0, True),
65
+ 822: (1, True),
66
+ 824: (2, True),
67
+ 830: (3, True),
68
+ }
69
+
70
+
71
+ def _f32(x: float) -> float:
72
+ try:
73
+ return struct.unpack("f", struct.pack("f", x))[0]
74
+ except OverflowError: # (a C float cast overflows to infinity: fN1 `1E300`, round 31)
75
+ return math.copysign(math.inf, x)
76
+
77
+
78
+ def i32(x: int) -> int:
79
+ """C `int` arithmetic: wrapped to 32 bits."""
80
+ return (x + 2**31) % 2**32 - 2**31
81
+
82
+
83
+ def _cint(x: float) -> int:
84
+ """x87 float -> int: truncated; out of range gives -2147483648."""
85
+ return int(x) if -(2**31) <= x < 2**31 else -(2**31)
86
+
87
+
88
+ def _cdiv(a: int, b: int) -> int:
89
+ """C integer division (truncates toward zero)."""
90
+ q = abs(a) // abs(b)
91
+ return q if (a < 0) == (b < 0) else -q
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class Model:
96
+ """The constants of one compiler's time accounting (from its measurement code generator)."""
97
+
98
+ overhead: int # first analog measurement of a scan
99
+ refund: int # taken back when less than 50 ms of slack remains
100
+ integ_num: float # integration code = int(integ_num / fN1 + 0.5)
101
+ integ_zero: int # ... for fN1 = 0
102
+ default_settle: int # documentation only
103
+ round_up: int # added before the final division by 100
104
+ busy_extra: int # added to the longest integration time (units) for the calibration gap
105
+
106
+ def integ_code(self, fnotch: float) -> int | None:
107
+ if _f32(fnotch) == 0: # (`1E-50` is a float zero, round 31)
108
+ return self.integ_zero
109
+ if not math.isfinite(fnotch) or fnotch <= 0:
110
+ return None
111
+ code = int(_f32(_f32(self.integ_num) / _f32(fnotch)) + 0.5)
112
+ if code > 0xFFFF:
113
+ return None
114
+ return code or 1
115
+
116
+ def integ_time(self, k: int) -> int: # microseconds-ish, before settling
117
+ raise NotImplementedError
118
+
119
+ def settle_code(self, settling: int) -> int:
120
+ raise NotImplementedError
121
+
122
+ def settle_units(self, code: int) -> int:
123
+ raise NotImplementedError
124
+
125
+ def open_detect_code(self) -> int:
126
+ raise NotImplementedError
127
+
128
+ def fixed_code(self, constant: int) -> int:
129
+ """Settling code of a constant the compiler itself passes (PanelTemp 900, Battery 200)."""
130
+ raise NotImplementedError
131
+
132
+
133
+ class _CR6(Model):
134
+ def integ_time(self, k):
135
+ return (k << 5) // 3 + 1
136
+
137
+ def settle_code(self, settling):
138
+ return _cint(_f32(_f32(_f32(i32((settling or 500) + 810)) * 3.0) / 32.0) + 0.5)
139
+
140
+ def settle_units(self, code):
141
+ return i32(_cint(_f32(_f32(code * 32.0) / 3.0)) + 5)
142
+
143
+ def open_detect_code(self):
144
+ return 5
145
+
146
+ def fixed_code(self, constant):
147
+ return int(_f32(_f32(constant * 3.0) / 32.0) + 0.5)
148
+
149
+
150
+ class _CR1000X(Model):
151
+ def integ_time(self, k):
152
+ return k * 32 + 180
153
+
154
+ def settle_code(self, settling):
155
+ return _cint(_f32(_f32(settling or 450) / 32.0) + 0.5)
156
+
157
+ def settle_units(self, code):
158
+ return i32(_cint(_f32(code * 32.0)) + 5)
159
+
160
+ def open_detect_code(self):
161
+ return 2
162
+
163
+ def fixed_code(self, constant):
164
+ return int(_f32(constant / 32.0) + 0.5)
165
+
166
+
167
+ MODELS = {
168
+ "cr6": _CR6(78, 77, 93750.0, 24, 500, 0x28, 13),
169
+ "cr1000x": _CR1000X(16, 15, 31250.0, 8, 450, 0x50, 17),
170
+ }
171
+
172
+
173
+ def analog_units(
174
+ m: Model,
175
+ reps: int,
176
+ gain: int,
177
+ fnotch: float,
178
+ settling: int,
179
+ single_ended: bool,
180
+ meas_off: bool = False,
181
+ reverse: bool = False,
182
+ excite: bool = False,
183
+ open_detect: bool = False,
184
+ groups: int = 1,
185
+ ) -> int | None:
186
+ """Time of one analog measurement call. `meas_off` (single-ended) adds one more measurement,
187
+ `reverse` (differential) and `excite` double the total, autorange (gain 3) settles twice, the
188
+ open-detect ranges settle longer and add 50 per rep. Any settling time counts as converted
189
+ (not clamped by its range check), in 32-bit C arithmetic (round 31)."""
190
+ k = m.integ_code(fnotch)
191
+ if k is None or not 1 <= reps <= 100_000:
192
+ return None
193
+ integ = m.integ_time(k)
194
+ settle = m.settle_units(m.settle_code(settling) + (m.open_detect_code() if open_detect else 0))
195
+ total = i32((settle + integ) * reps)
196
+ if gain == 3:
197
+ total = i32(total + reps * settle)
198
+ if excite:
199
+ total = i32(total * 2)
200
+ total = i32(total * groups)
201
+ if meas_off and single_ended:
202
+ total = i32(total + integ + settle)
203
+ if reverse and not single_ended:
204
+ total = i32(total * 2)
205
+ if open_detect:
206
+ total = i32(total + reps * 0x32)
207
+ return _cdiv(i32(total + m.round_up), 100) + 1
208
+
209
+
210
+ def panel_units(m: Model, fnotch: float) -> int | None:
211
+ """`PanelTemp(Dest, fN1)`: two measurements with a fixed settling argument (900)."""
212
+ k = m.integ_code(fnotch)
213
+ if k is None:
214
+ return None
215
+ return (m.settle_units(m.fixed_code(900)) + m.integ_time(k) + m.round_up) // 100 + 1
216
+
217
+
218
+ def battery_units(m: Model) -> int:
219
+ """CR1000X `Battery`: one analog call with integration code 10 and a fixed settling of 200."""
220
+ return (m.settle_units(m.fixed_code(200)) + m.integ_time(10) + m.round_up) // 100 + 1
221
+
222
+
223
+ @dataclass
224
+ class Budget:
225
+ scan: S.Instruction
226
+ end: S.Instruction
227
+ value: int # Scan interval argument
228
+ unit: int # its Units code
229
+ interval: int # 100 us units
230
+ msg_unit: int # the unit the messages count in (a SubScan can leave its own)
231
+ total: int = 0 # after the analog adjustment
232
+ analog: bool = False
233
+ pulse_only: bool = True # nothing but PulseCount adds time (such a scan never warns)
234
+ max_integ: int = 0 # longest integration time (units) of an analog call
235
+ partial: bool = False
236
+ sub_short: list = field(default_factory=list) # (NextSubScan statement, amount, unit)
237
+
238
+
239
+ def _value(expr: S.Expr | None, env: consteval.Env) -> object | None:
240
+ if expr is None or not consteval.is_constant(expr, env):
241
+ return None
242
+ try:
243
+ return consteval.value_of(expr, env)
244
+ except consteval.NotConstant:
245
+ return None
246
+
247
+
248
+ def _num(expr: S.Expr | None, env: consteval.Env) -> float | None:
249
+ v = _value(expr, env)
250
+ return float(v) if isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) else None
251
+
252
+
253
+ def _long(expr: S.Expr | None, env: consteval.Env) -> int | None:
254
+ v = _value(expr, env)
255
+ if isinstance(v, bool):
256
+ return -1 if v else 0
257
+ return consteval.to_long(v) if isinstance(v, (int, float)) else None
258
+
259
+
260
+ def _settling(expr: S.Expr | None, env: consteval.Env) -> int | None:
261
+ """A settling time as the compiler converts it (a string is -2147483648, round 31; `INF`
262
+ times as a float overflow does, `1E39`)."""
263
+ if isinstance(expr, S.Name) and expr.name.upper() == "INF" and "INF" not in env.consts:
264
+ return consteval.to_long(math.inf)
265
+ v = _value(expr, env)
266
+ return consteval.to_long(v) if isinstance(v, str) else _long(expr, env)
267
+
268
+
269
+ # keyword -> (reps position, range position, flag position, settle position, fnotch position,
270
+ # single-ended)
271
+ _ANALOG = {
272
+ "VOLTSE": (2, 3, 5, 6, 7, True),
273
+ "VOLTDIFF": (2, 3, 5, 6, 7, False),
274
+ "TCSE": (2, 3, 7, 8, 9, True),
275
+ "TCDIFF": (2, 3, 7, 8, 9, False),
276
+ "CURRENTSE": (2, 3, 5, 6, 7, True),
277
+ }
278
+ # bridges (round 31): keyword -> (ranges, channel, MeasPEx, RevEx, RevDiff, settle, fnotch positions,
279
+ # single-ended, measurement groups)
280
+ _BRIDGE = {
281
+ "BRHALF": ((3,), 4, 6, 8, None, 9, 10, True, 1),
282
+ "BRHALF3W": ((3,), 4, 6, 8, None, 9, 10, True, 2),
283
+ "BRFULL": ((3,), 4, 6, 8, 9, 10, 11, False, 1),
284
+ "BRHALF4W": ((3, 4), 5, 7, 9, 10, 11, 12, False, 2),
285
+ "BRFULL6W": ((3, 4), 5, 7, 9, 10, 11, 12, False, 2),
286
+ }
287
+ _PLAIN = ((0, False), (1, False), (2, False)) # fixed ranges without open detect
288
+
289
+
290
+ def _bridge_units(m: Model, args: list, env: consteval.Env, spec: tuple) -> tuple[int, bool, int] | None:
291
+ """One analog call per MeasPEx group of reps (one call when MeasPEx equals Reps); each doubles
292
+ per RevEx, RevDiff (differential) and second measurement group. Autorange and open-detect
293
+ ranges of the two-group bridges, burst channels and Reps above 100000 are not modelled
294
+ (round 31, traced CR6, docs/checker.md "Bridge channel spans and settling-time timing")."""
295
+ ranges, cp, mp, xp, dp, sp, ip, single, groups = spec
296
+ if len(args) < ip:
297
+ return None
298
+ reps, chan, meas, revex = (_long(args[i - 1], env) for i in (2, cp, mp, xp))
299
+ revdiff = _long(args[dp - 1], env) if dp else 0
300
+ rngs = [_long(args[i - 1], env) for i in ranges]
301
+ settling, fn = _settling(args[sp - 1], env), _num(args[ip - 1], env)
302
+ if (
303
+ reps is None
304
+ or chan is None
305
+ or meas is None
306
+ or revex is None
307
+ or revdiff is None
308
+ or settling is None
309
+ or fn is None
310
+ ):
311
+ return None
312
+ if None in rngs:
313
+ return None
314
+ rngs = cast(list[int], rngs)
315
+ if (
316
+ chan <= 0
317
+ or not 1 <= reps <= 100_000
318
+ or any(r not in _RANGES for r in rngs)
319
+ or (groups > 1 and any(_RANGES[r] not in _PLAIN for r in rngs))
320
+ ):
321
+ return None
322
+ meas = max(meas, 1)
323
+ chunks = [reps] if meas == reps else [meas] * (reps // meas) + ([reps % meas] if reps % meas else [])
324
+ total = 0
325
+ for n in chunks:
326
+ units = analog_units(
327
+ m,
328
+ n,
329
+ _RANGES[rngs[0]][0],
330
+ fn,
331
+ settling,
332
+ single,
333
+ excite=(revex & 0xFF) != 0,
334
+ reverse=(revdiff & 0xFF) != 0,
335
+ open_detect=_RANGES[rngs[0]][1],
336
+ groups=groups,
337
+ )
338
+ if units is None:
339
+ return None
340
+ total = i32(total + units)
341
+ code = m.integ_code(fn)
342
+ if code is None:
343
+ return None
344
+ return total, True, m.integ_time(code) // 100
345
+
346
+
347
+ def instruction_units(stmt: S.Instruction, env: consteval.Env, profile: Profile) -> tuple[int, bool, int] | None:
348
+ """`(units, analog, integration time in units)` the instruction adds, or None when not modelled / not
349
+ readable."""
350
+ kw = stmt.keyword.upper()
351
+ args = stmt.args
352
+ if kw == "DELAY":
353
+ if len(args) != 3:
354
+ return None
355
+ option, value, unit = (_num(a, env) for a in args)
356
+ if option is None or int(option) != 0 or value is None or unit is None or int(unit) not in _US_PER_UNIT:
357
+ return None
358
+ if 0 <= value < 1:
359
+ return None
360
+ micro = delay_us(value, int(unit))
361
+ if micro == 0 or task_memory_overflow(micro):
362
+ return None
363
+ if micro >= 2**31: # rounded up instead (an empirical fit, round 57; mechanism not derived)
364
+ return -(-micro // 100), False, 0
365
+ return (2 * (micro // 2) + 100) // 100, False, 0
366
+ if kw == "EXCITEV":
367
+ delay = _long(args[2], env) if len(args) == 3 else None
368
+ if delay is None:
369
+ return None
370
+ delay = excite_delay_us(delay)
371
+ if task_memory_overflow(max(delay - 10, 0)):
372
+ return None
373
+ return ((delay + 100) % 2**32) // 100, False, 0
374
+ if kw == "PORTSET":
375
+ return 1, False, 0
376
+ if kw in ("SW12", "PULSECOUNT"):
377
+ return 1, False, 0
378
+ m = MODELS.get(profile.name)
379
+ if m is None:
380
+ return None
381
+ if kw == "PORTGET" or (kw == "BATTERY" and profile.name == "cr6"):
382
+ return 0, False, 0
383
+ if kw == "BATTERY":
384
+ return battery_units(m), True, 0 # only the floor (oracle, CR1000X Scan(3,mSec))
385
+ if kw == "PANELTEMP" and len(args) == 2:
386
+ fn = _num(args[1], env)
387
+ units = panel_units(m, fn) if fn is not None else None
388
+ return (2 * units, True, m.integ_time(m.integ_code(fn)) // 100) if units and fn is not None else None
389
+ if kw in _ANALOG:
390
+ rp, gp, fp, sp, ip, single = _ANALOG[kw]
391
+ if len(args) < ip:
392
+ return None
393
+ reps, rng, flag = (_long(args[i - 1], env) for i in (rp, gp, fp))
394
+ settling = _settling(args[sp - 1], env)
395
+ fn = _num(args[ip - 1], env)
396
+ if (reps is None or rng is None or flag is None or settling is None or fn is None) or rng not in _RANGES:
397
+ return None
398
+ gain, open_detect = _RANGES[rng]
399
+ cr6_current = kw == "CURRENTSE" and profile.name == "cr6" # MeasOff and "C" ignored
400
+ on = (flag & 0xFF) != 0 and not cr6_current
401
+ open_detect = open_detect and not cr6_current
402
+ flags = dict(meas_off=on and single, reverse=on and not single, open_detect=open_detect)
403
+ units = analog_units(m, reps, gain, fn, settling, single, **flags)
404
+ return (units, True, m.integ_time(m.integ_code(fn)) // 100) if units is not None else None
405
+ if kw in _BRIDGE:
406
+ return _bridge_units(m, args, env, _BRIDGE[kw])
407
+ if kw in ("THERM107", "THERM108", "THERM109") and len(args) >= 6:
408
+ reps, settling = _long(args[1], env), _long(args[4], env)
409
+ fn = _num(args[5], env)
410
+ if (reps is None or settling is None or fn is None) or not 1 <= reps <= 255 or not 0 <= settling < 6_000_000:
411
+ return None
412
+ units = analog_units(m, reps, 0, fn, settling, True, excite=True)
413
+ return (units, True, m.integ_time(m.integ_code(fn)) // 100) if units else None
414
+ if kw == "CS616" and len(args) >= 2:
415
+ reps = _long(args[1], env)
416
+ return (reps * 5 + 2, False, 0) if reps is not None and 1 <= reps <= 255 else None
417
+ return None
418
+
419
+
420
+ def budgets(program: S.Program, env: consteval.Env, profile: Profile, include_zero: bool = False):
421
+ """One `Budget` per main-sequence `Scan` with a constant mSec/Sec/Min interval, in order."""
422
+ from .instr_rules import _scan_raw
423
+
424
+ model = MODELS.get(profile.name) or MODELS["cr6"]
425
+ slow = False
426
+ cur: Budget | None = None
427
+ frames: list[
428
+ tuple[int, int, int, int | None, int] | None
429
+ ] = [] # open SubScans: (start, count, interval, unit, saved unit)
430
+ total = 0
431
+ for stmt in program.statements:
432
+ kw = stmt.keyword.upper() if isinstance(stmt, S.Instruction) else ""
433
+ text = stmt.text or ""
434
+ if kw == "NEXT" and re.match(r"(?i)\s*Next\s+Scan\b", text):
435
+ kw = "NEXTSCAN"
436
+ elif kw == "NEXT" and re.match(r"(?i)\s*Next\s+SubScan\b", text):
437
+ kw = "NEXTSUBSCAN"
438
+ if kw == "SLOWSEQUENCE":
439
+ slow = True
440
+ elif isinstance(stmt, S.Instruction) and (kw == "SCAN" and not slow):
441
+ raw = _scan_raw(stmt, env)
442
+ if include_zero and len(stmt.args) > 1:
443
+ v, u = _value(stmt.args[0], env), _value(stmt.args[1], env)
444
+ if (
445
+ u in (2, 3)
446
+ and not isinstance(u, bool)
447
+ and isinstance(v, (int, float))
448
+ and not isinstance(v, bool)
449
+ and (0 <= v < 1 or v > 2147483647)
450
+ ):
451
+ raw = (0, u)
452
+ if raw and raw[1] in SCAN_UNITS:
453
+ value, unit = raw
454
+ cur = Budget(stmt, stmt, value, unit, value * _US_PER_UNIT[unit] // 100, unit)
455
+ else:
456
+ cur = None
457
+ frames, total = [], 0
458
+ elif cur is None:
459
+ continue
460
+ elif isinstance(stmt, S.Instruction) and (kw == "NEXTSCAN"):
461
+ if frames:
462
+ cur.partial = True
463
+ cur.end = stmt
464
+ cur.total = total
465
+ if cur.analog:
466
+ cur.total = i32(cur.total + model.overhead)
467
+ if i32(cur.interval - cur.total) < 500:
468
+ cur.total = i32(cur.total - model.refund)
469
+ yield cur
470
+ cur = None
471
+ elif isinstance(stmt, S.Instruction) and (kw == "SUBSCAN"):
472
+ args = [_value(a, env) for a in stmt.args]
473
+ count = args[2] if len(args) == 3 else None
474
+ ok = isinstance(count, int) and not isinstance(count, bool) and count > 0
475
+ step = 0
476
+ if ok and args[0] != 0:
477
+ u = _US_PER_UNIT.get(args[1]) if isinstance(args[1], int) else None
478
+ if u is None or not consteval.is_finite_number(args[0]) or args[0] <= 0:
479
+ ok = False
480
+ else:
481
+ step = int(args[0]) * u // 100
482
+ if not ok:
483
+ cur.partial = True
484
+ sub_unit = (
485
+ args[1]
486
+ if ok and isinstance(args[1], int) and not isinstance(args[1], bool) and args[1] in UNIT_NAMES
487
+ else None
488
+ )
489
+ if ok and sub_unit is None:
490
+ cur.partial = True
491
+ frames.append((total, cast(int, count), step, sub_unit, cur.msg_unit) if ok else None)
492
+ if sub_unit is not None:
493
+ cur.msg_unit = sub_unit
494
+ elif kw == "NEXTSUBSCAN":
495
+ frame = frames.pop() if frames else None
496
+ if frame is None:
497
+ continue
498
+ start, count, step, _sub_unit, saved = frame
499
+ body = total - start
500
+ restore = True
501
+ if count < 2 or step == 0 or body == 0:
502
+ total += (count - 1) * body
503
+ elif step < body:
504
+ cur.sub_short.append((stmt, amount(body - step, cur.msg_unit), cur.msg_unit))
505
+ total += (count - 1) * body
506
+ elif step - body > 1:
507
+ total += (count - 1) * step
508
+ restore = False # the real compiler returns before restoring the unit
509
+ else:
510
+ total += (count - 1) * body
511
+ if restore:
512
+ cur.msg_unit = saved
513
+ elif isinstance(stmt, S.Instruction):
514
+ got = instruction_units(stmt, env, profile)
515
+ if got is None:
516
+ if kw in _KNOWN_TIMED:
517
+ cur.partial = True
518
+ cur.analog = cur.analog or kw in _ANALOG_KINDS
519
+ else:
520
+ total = i32(total + got[0])
521
+ if kw != "PULSECOUNT":
522
+ cur.pulse_only = False
523
+ cur.analog = cur.analog or got[1]
524
+ cur.max_integ = max(cur.max_integ, got[2])
525
+
526
+
527
+ # of those, the ones that take the analog-measurement overhead
528
+ _ANALOG_KINDS = frozenset(
529
+ {
530
+ "VOLTSE",
531
+ "VOLTDIFF",
532
+ "TCSE",
533
+ "TCDIFF",
534
+ "PANELTEMP",
535
+ "THERM107",
536
+ "THERM108",
537
+ "THERM109",
538
+ "BRHALF",
539
+ "BRHALF3W",
540
+ "BRHALF4W",
541
+ "BRFULL",
542
+ "BRFULL6W",
543
+ "RESISTANCE",
544
+ "CURRENTSE",
545
+ }
546
+ )
547
+ # instructions that add time but are not modelled (a scan holding one has a lower-bound total)
548
+ _KNOWN_TIMED = frozenset(
549
+ {
550
+ "VOLTSE",
551
+ "VOLTDIFF",
552
+ "TCSE",
553
+ "TCDIFF",
554
+ "PANELTEMP",
555
+ "THERM107",
556
+ "THERM108",
557
+ "THERM109",
558
+ "CS616",
559
+ "DELAY",
560
+ "BRHALF",
561
+ "BRHALF3W",
562
+ "BRHALF4W",
563
+ "BRFULL",
564
+ "BRFULL6W",
565
+ "RESISTANCE",
566
+ "CURRENTSE",
567
+ "PERIODAVG",
568
+ "AM25T",
569
+ "THERMISTOR",
570
+ "VIBRATINGWIRE",
571
+ "EXCITEV",
572
+ "EXCITEI",
573
+ "CALIBRATE",
574
+ "PANELTEMP2",
575
+ "RESISTANCE3W",
576
+ "RESISTANCE2W",
577
+ "TCPSYC",
578
+ "MUXSELECT",
579
+ }
580
+ )