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,678 @@
1
+ """`TimeIntoInterval`/`IfTime` argument checks, made by the parser (pass 1) on all four loggers.
2
+
3
+ Decompiled `FUN_00494780`: with constant `TintoInt`, `Interval` and `Units` (not month), both
4
+ values become whole seconds plus microseconds. A negative `TintoInt` wraps into the interval. Then:
5
+
6
+ - `Interval (%d) must be > 0 and TimeInto Interval (%d) must be < Interval.` (the raw values)
7
+ unless `0 <= TintoInt < Interval`;
8
+ - inside a `Scan`, `Scan interval must be integrally divisible into interval.` unless both are
9
+ multiples of the scan interval (100 uSec units, or whole seconds once the interval passes
10
+ 3600 s, where sub-second parts give `Illegal sub seconds in time into and/or interval when
11
+ interval > 3600 secs.`).
12
+
13
+ Oracle probes, 2026-09-28 (`docs/checker.md` "TimeIntoInterval arguments").
14
+ """
15
+
16
+ import math
17
+ import re
18
+ import struct
19
+
20
+ from .. import consteval
21
+ from .. import syntax as S
22
+ from ..diagnostics import Diagnostics
23
+ from . import instr_rules
24
+
25
+ _FUNCTIONS = frozenset({"TIMEINTOINTERVAL", "IFTIME"})
26
+ # Functions whose integer-typed constant arguments draw `Parameter must be integral.`, with the
27
+ # argument positions checked (`docs/checker.md` "Integral parameters").
28
+ _INTEGRAL_ARGS = {"TIMEINTOINTERVAL": (0, 1, 2), "IFTIME": (0, 1, 2), "SERIALOPEN": (1,), "TIMER": (0,)}
29
+ _MONTH = 6
30
+ # The trailing `TimeOut` of `HTTPPut` (9th argument) and `FTPClient` (11th): see `timeout_gate`.
31
+ _TIMEOUT_ARG = {"HTTPPUT": 8, "FTPCLIENT": 10}
32
+ _MODE_RE = re.compile(r"(?i)\b(IfTime|TimeIntoInterval|Timer|SerialOpen|HTTPPut|FTPClient)\b\s*\(")
33
+ _LITERAL_RE = re.compile(r"\"[^\"]*\"|'.*$")
34
+ NOT_INTEGRAL = "Parameter must be integral."
35
+ _INT_MIN, _INT_MAX = -(2**31), 2**31 - 1
36
+ _F32_MAX = 3.4028234663852886e38
37
+
38
+
39
+ def gate_text(stmt) -> str | None:
40
+ """The text `timeout_gate` reads for `stmt` in a flattened (`_with_inline`) statement list: a
41
+ one-line `If` gives only its condition, its bodies follow as statements of their own, so the
42
+ mode advances once per source occurrence (oracle, round 19, all four loggers)."""
43
+ text = getattr(stmt, "text", None)
44
+ if isinstance(stmt, S.If) and stmt.inline is not None and text:
45
+ m = re.search(r"(?i)\bThen\b", _LITERAL_RE.sub('""', text))
46
+ return _LITERAL_RE.sub('""', text)[: m.start()] if m else text
47
+ return text
48
+
49
+
50
+ def timeout_gate(seen: bool, text: str | None) -> tuple[bool | None, bool]:
51
+ """`(gate, seen)` for one statement. `seen` is the parser's persistent integer-argument mode:
52
+ `IfTime`/`TimeIntoInterval` set it and nothing restores it until a `Timer` or `SerialOpen`
53
+ (oracle, round 17, CR6 and CR350: an earlier `IfTime` in a `Sub`, another `Scan` or earlier in
54
+ the same statement; `Timer`/`SerialOpen` between them switches it off). `gate` is the mode at
55
+ the statement's first `HTTPPut`/`FTPClient` (`True`/`False`), `None` when an `IfTime` follows
56
+ that call in the same statement (not modelled). Gates the `TimeOut` rule (round 17 notes in
57
+ `docs/instructions.md`)."""
58
+ code = _LITERAL_RE.sub('""', text or "")
59
+ mode, gate, queried = seen, False, False
60
+ for m in _MODE_RE.finditer(code):
61
+ word = m.group(1).upper()
62
+ if word in ("HTTPPUT", "FTPCLIENT"):
63
+ if not queried:
64
+ gate, queried = mode, True
65
+ elif word in ("IFTIME", "TIMEINTOINTERVAL"):
66
+ if queried and not mode:
67
+ gate = None
68
+ mode = True
69
+ else:
70
+ mode = False
71
+ return gate, mode
72
+
73
+
74
+ def check(program: S.Program, profile, diags: Diagnostics, env: consteval.Env) -> None:
75
+ from . import expressions, instr_data
76
+
77
+ wide = wide_constants(program, profile)
78
+ known = expressions._known_names(program, profile)
79
+ positions = {id(st): i for i, st in enumerate(program.statements)}
80
+ in_table = blocked_table = False
81
+ scan: tuple[int, int] | None = None
82
+ seen = False
83
+ for stmt in program.statements:
84
+ gate, seen = timeout_gate(seen, getattr(stmt, "text", None))
85
+ _check_timeout_value(stmt, diags, env, profile, wide, program)
86
+ if gate:
87
+ _check_timeout(stmt, diags, env, profile, wide)
88
+ kw = stmt.keyword.upper() if isinstance(stmt, S.Instruction) else ""
89
+ if isinstance(stmt, S.Instruction) and (kw == "DATATABLE"):
90
+ in_table = True
91
+ blocked_table = any(expressions._undefined_names(a, known) for a in stmt.args[1:])
92
+ elif kw == "ENDTABLE":
93
+ in_table = blocked_table = False
94
+ if isinstance(stmt, S.Instruction) and (kw == "SCAN"):
95
+ scan = _scan_step(stmt, env)
96
+ continue
97
+ if kw in ("NEXTSCAN", "SUBSCAN", "NEXTSUBSCAN") or isinstance(stmt, S.SubDef):
98
+ scan = None
99
+ if not profile.single_pass:
100
+ for call in _calls(stmt, {"FTPCLIENT"}):
101
+ if (option := _put_get_option(call, env)) is not None:
102
+ diags.error(stmt.line, f"Invalid PutGetOption: {option}", stmt.file)
103
+ for call in _calls(stmt, _INTEGRAL_ARGS):
104
+ if isinstance(call, S.Call) and call.explicit:
105
+ continue
106
+ if call.name.upper() == "TIMER":
107
+ owner, before = _timer_context(stmt, call, known, profile.single_pass)
108
+ table_only = (
109
+ isinstance(owner, S.Instruction) and owner.keyword.upper() in instr_data.TABLE_ONLY_INSTRUCTIONS
110
+ )
111
+ if (
112
+ before
113
+ or blocked_table
114
+ or (table_only and not in_table)
115
+ or (not profile.single_pass and _timer_shadowed(program, stmt, positions, diags))
116
+ ):
117
+ continue
118
+ bad = [
119
+ i
120
+ for i in _INTEGRAL_ARGS[call.name.upper()][: len(call.args)]
121
+ if not_integral(call.args[i], env, profile, wide) or _nonfinite_literal(call.args[i], env)
122
+ ]
123
+ for _ in bad:
124
+ diags.error(stmt.line, NOT_INTEGRAL, stmt.file)
125
+ if not bad and call.name.upper() in _FUNCTIONS:
126
+ _check_call(call, scan, stmt, diags, env, any(d.line < stmt.line for d in diags.errors))
127
+
128
+
129
+ def _put_get_option(call, env: consteval.Env) -> int | None:
130
+ """CR6/CR1000X: an `FTPClient` constant PutGetOption outside -29..29 after truncation, as the
131
+ Long the message prints (above the Long range -2147483648, above float32 2147483647; below the
132
+ Long range it compiles); None when valid or not a numeric constant (oracle, rounds 65 and 67,
133
+ docs/instructions.md#ftpclient-putgetoption)."""
134
+ if len(call.args) < 6:
135
+ return None
136
+ try:
137
+ value = consteval.value_of(call.args[5], env)
138
+ except consteval.NotConstant:
139
+ return None
140
+ if (
141
+ isinstance(value, (bool, str))
142
+ or not isinstance(value, (int, float))
143
+ or (isinstance(value, float) and not math.isfinite(value))
144
+ ):
145
+ return None # (a huge integer literal is a Python int: `math.isfinite` would overflow)
146
+ if value < _INT_MIN:
147
+ return None # below the Long range: compiles (`-2147483649`, `-1e39`; oracle, round 67)
148
+ if value > _F32_MAX:
149
+ option = _INT_MAX # beyond float32 (+inf): 2147483647 (oracle, round 67)
150
+ else:
151
+ option = int(value) if value <= _INT_MAX else _INT_MIN
152
+ return option if abs(option) >= 30 else None
153
+
154
+
155
+ def _timer_context(stmt, call, known, single_pass):
156
+ """Owner and an earlier unresolved name in the expression that contains this call."""
157
+ from . import expressions, symbols
158
+
159
+ if stmt is call:
160
+ return stmt, False
161
+ before = False
162
+ for expr in expressions._stmt_exprs(stmt):
163
+ if not (single_pass and isinstance(stmt, S.Call)):
164
+ before = False
165
+ for node in symbols._name_nodes(expr):
166
+ if node is call:
167
+ return stmt, before
168
+ before |= node.name.upper() not in known or id(node) in getattr(known, "out_of_scope", ())
169
+ for inner in (getattr(stmt, "inline", None) or []) + (getattr(stmt, "inline_else", None) or []):
170
+ if any(c is call for c in _calls(inner, {"TIMER"})):
171
+ return _timer_context(inner, call, known, single_pass)
172
+ return stmt, False
173
+
174
+
175
+ def _timer_shadowed(program, stmt, positions, diags):
176
+ """CR6/CR1000X resolve earlier visible storage named Timer before the built-in."""
177
+ from . import symbols
178
+
179
+ for decl in symbols.scoped_view(program, stmt).statements:
180
+ if positions[id(decl)] > positions[id(stmt)]:
181
+ continue
182
+ if isinstance(decl, S.SubDef) and any(p.name.upper() == "TIMER" for p in decl.params):
183
+ return True
184
+ if isinstance(decl, S.Declaration) and decl.kind == "DIM" and any(v.name.upper() == "TIMER" for v in decl.vars):
185
+ return True
186
+ if (
187
+ isinstance(decl, S.Instruction)
188
+ and decl.keyword.upper() == "ALIAS"
189
+ and not any(d.line == decl.line and d.file == decl.file for d in diags.errors)
190
+ and any(isinstance(a, (S.Name, S.Index)) and a.name.upper() == "TIMER" for a in decl.args[1:])
191
+ ):
192
+ return True
193
+ return False
194
+
195
+
196
+ def _nonfinite_literal(expr: S.Expr | None, env) -> bool:
197
+ # NAN/INF are expression literals outside consteval's symbol table. The integral-argument
198
+ # resolver reads them as such even beside aliases or parameters of that name (round 46).
199
+ while isinstance(expr, S.Unary) and expr.op in ("+", "-"):
200
+ expr = expr.operand
201
+ return isinstance(expr, S.Name) and expr.name.upper() in ("NAN", "INF") and expr.name.upper() not in env.consts
202
+
203
+
204
+ def _check_timeout_value(stmt: S.Stmt, diags: Diagnostics, env, profile, wide, program) -> None:
205
+ """A constant integer `TimeOut` whose 32-bit product `10 * TimeOut` is negative is `Parameter
206
+ TimeOut cannot be negative` on all four loggers, gated or not (oracle, round 19, probes
207
+ `scratch/r19/W8`). Only the forms in `timeout_value` are modelled. CR6/CR1000X: no such line when
208
+ another argument of the same call is an undefined name (`Invalid symbol in expression` alone).
209
+ CR300/CR350: the line follows the same-line `... is not defined.` lines (`order_timeout_negative`).
210
+ After an already reported error arithmetic is not folded: `fold_timeout_expressions`."""
211
+ for call in _calls(stmt, _TIMEOUT_ARG):
212
+ if _negative_timeout(call, env, profile, wide, program):
213
+ diags.error(stmt.line, _TIMEOUT_NEGATIVE, stmt.file, stmt=stmt)
214
+
215
+
216
+ def _negative_timeout(call, env, profile, wide, program) -> bool:
217
+ """Whether `call` draws `Parameter TimeOut cannot be negative` when nothing was reported before."""
218
+ if isinstance(call, S.Call) and call.explicit:
219
+ return False
220
+ i = _TIMEOUT_ARG[call.name.upper()]
221
+ if i >= len(call.args) or call.args[i] is None:
222
+ return False
223
+ value = timeout_value(call.args[i], env, profile, wide)
224
+ if value is None or value[0] != "int" or _signed32(10 * value[1]) >= 0:
225
+ return False
226
+ return profile.single_pass or not _undefined_sibling(call, i, program, profile)
227
+
228
+
229
+ def _undefined_sibling(call: S.Index, i: int, program: S.Program, profile) -> bool:
230
+ """Whether another argument of `call` (not position `i`) names an undefined symbol."""
231
+ from . import expressions # lazy: `expressions` imports this module's package
232
+
233
+ known = expressions._known_names(program, profile)
234
+ return any(a is not None and expressions._undefined_names(a, known) for j, a in enumerate(call.args) if j != i)
235
+
236
+
237
+ def _needs_integer_fold(arg) -> bool:
238
+ """Arithmetic needs the parser's error-free fold; a unary plus alone does not.
239
+
240
+ See `docs/instructions.md`, round 23. The value classifier bounds the supported operators
241
+ and operands; runtime expressions and unobserved forms are left to their existing checks.
242
+ """
243
+ while isinstance(arg, S.Unary) and arg.op in ("-", "+"):
244
+ if arg.op == "-":
245
+ return True
246
+ arg = arg.operand
247
+ return isinstance(arg, S.Binary) and arg.op in ("+", "-", "*")
248
+
249
+
250
+ def fold_timeout_expressions(program: S.Program, profile, diags: Diagnostics, env, reported) -> None:
251
+ """The compiler folds integer arithmetic only while its reported-error count is zero (warnings
252
+ do not count, `:` and newlines do not reset it). After any reported error, a supported arithmetic
253
+ TimeOut is `Parameter must be a constant integer: <statement>.` instead of the sign check,
254
+ whatever its value; in the gated state (`timeout_gate`) it is silent. Oracle rounds 21 and 23,
255
+ fixtures `r21_negfold.json` and `r23_timeout_expressions.json`. `program` is the `_with_inline`
256
+ list. What counts as reported
257
+ before the call: an error on an earlier line, an error of an earlier statement of the same line
258
+ (any other error of the line when the call has an earlier segment), a previous `TimeOut` finding of
259
+ this rule, and on CR300/CR350 an undefined name in an earlier argument of the call. CR6/CR1000X
260
+ count only `reported`, the pass-1 errors (a name left to pass 2 does not count). A converted
261
+ finding goes after the other errors of its line.
262
+ """
263
+ from . import expressions # lazy: `expressions` imports this module's package
264
+
265
+ stmts = program.statements
266
+ neg_lines: dict = {}
267
+ for d in diags.items:
268
+ if d.severity == "error" and d.message == _TIMEOUT_NEGATIVE:
269
+ neg_lines.setdefault((d.file, d.line), []).append(d)
270
+ wide = wide_constants(program, profile)
271
+ lines: dict = {}
272
+ first: dict = {}
273
+ for i, st in enumerate(stmts):
274
+ lines.setdefault(st.file or "", []).append((st.line, i))
275
+ first.setdefault((st.file or "", st.line), i)
276
+ for v in lines.values():
277
+ v.sort()
278
+ import bisect
279
+
280
+ def position(d) -> int:
281
+ entries = lines.get(d.file or "")
282
+ if not entries:
283
+ return -1
284
+ k = bisect.bisect_left(entries, (d.line, -1))
285
+ return entries[k][1] if k < len(entries) else len(stmts)
286
+
287
+ known = None
288
+ seen = False
289
+ family_error = False
290
+ added: list = []
291
+ for k, stmt in enumerate(stmts):
292
+ gate, seen = timeout_gate(seen, gate_text(stmt))
293
+ inline = {
294
+ id(c)
295
+ for inner in (getattr(stmt, "inline", None) or []) + (getattr(stmt, "inline_else", None) or [])
296
+ for c in _calls(inner, _TIMEOUT_ARG)
297
+ }
298
+ for call in _calls(stmt, _TIMEOUT_ARG):
299
+ if id(call) in inline or (isinstance(call, S.Call) and call.explicit):
300
+ continue
301
+ i = _TIMEOUT_ARG[call.name.upper()]
302
+ if i >= len(call.args) or call.args[i] is None:
303
+ continue
304
+ arg = call.args[i]
305
+ key = (stmt.file, stmt.line)
306
+ own = None
307
+ if _negative_timeout(call, env, profile, wide, program):
308
+ own = (neg_lines.get(key) or [None]).pop(0)
309
+ value = timeout_value(arg, env, profile, wide)
310
+ if _needs_integer_fold(arg) and value is not None and value[0] == "int":
311
+ line_first = first[(stmt.file or "", stmt.line)]
312
+ counted = [
313
+ d
314
+ for d in diags.items
315
+ if d.severity == "error"
316
+ and d.line
317
+ and d.message != _TIMEOUT_NEGATIVE
318
+ and not any(d is a for a in added)
319
+ and (profile.single_pass or any(d is r for r in reported))
320
+ ]
321
+ before = (
322
+ family_error
323
+ or any(position(d) < line_first for d in counted)
324
+ or (k > line_first and any(position(d) == line_first for d in counted))
325
+ )
326
+ if not before and profile.single_pass:
327
+ known = known or expressions._known_names(program, profile)
328
+ before = any(a is not None and expressions._undefined_names(a, known) for a in call.args[:i])
329
+ if before and gate is not None:
330
+ if own is not None:
331
+ diags.items.remove(own)
332
+ if not gate:
333
+ diags.error(
334
+ stmt.line,
335
+ expressions._arity_message(
336
+ "constint", stmt.text, profile, stmt.at_eol, whole_statement=True
337
+ ),
338
+ stmt.file,
339
+ )
340
+ added.append(diags.items[-1])
341
+ family_error = True
342
+ continue
343
+ if own is not None:
344
+ family_error = True
345
+
346
+
347
+ def order_timeout_negative(diags: Diagnostics, profile, program: S.Program) -> None:
348
+ """CR300/CR350 only: a `Parameter TimeOut cannot be negative` line is printed after the
349
+ `... is not defined.` lines of its own call and of the earlier statements of its line, before
350
+ those of later ones (oracle, round 19: `HTTPPut(...,NoSuchVar,8,214748365)` gives `NoSuchVar is
351
+ not defined.` then the TimeOut line; round 21: `HTTPPut(...,-1) : Result(4)=Nope+1` the other way
352
+ round). Moved after the last such line."""
353
+ if not profile.single_pass:
354
+ return
355
+ items = diags.items
356
+ for d in [d for d in items if d.severity == "error" and d.message == _TIMEOUT_NEGATIVE]:
357
+ at = next(i for i, o in enumerate(items) if o is d)
358
+ same = [st for st in program.statements if (st.file, st.line) == (d.file, d.line)]
359
+ mine = next((n for n, st in enumerate(same) if id(st) == d.stmt), len(same))
360
+ after = []
361
+ for j, o in enumerate(items):
362
+ if (
363
+ o is d
364
+ or o.severity != "error"
365
+ or (o.file, o.line) != (d.file, d.line)
366
+ or not o.message.endswith(" is not defined.")
367
+ ):
368
+ continue
369
+ name = re.compile(r"(?i)\b" + re.escape(o.message[: -len(" is not defined.")]) + r"\b")
370
+ owner = next((n for n, st in enumerate(same) if name.search(st.text or "")), 0)
371
+ if owner <= mine:
372
+ after.append(j)
373
+ if after and max(after) > at:
374
+ items.pop(at)
375
+ items.insert(max(after), d)
376
+
377
+
378
+ def _check_timeout(stmt: S.Stmt, diags: Diagnostics, env, profile, wide) -> None:
379
+ """After an earlier `IfTime`/`TimeIntoInterval`, a string `TimeOut` is `Warning: Parameter will
380
+ truncate to the nearest integer.`; a float-typed constant or `NAN` is `Parameter must be
381
+ integral.` (not a constant-integer error; oracle, all four loggers, round 17)."""
382
+ for call in _calls(stmt, _TIMEOUT_ARG):
383
+ if isinstance(call, S.Call) and call.explicit:
384
+ continue
385
+ i = _TIMEOUT_ARG[call.name.upper()]
386
+ if i >= len(call.args) or call.args[i] is None:
387
+ continue
388
+ arg = call.args[i]
389
+ if isinstance(arg, S.String):
390
+ diags.warning(stmt.line, "Parameter will truncate to the nearest integer.", stmt.file)
391
+ elif (
392
+ (isinstance(arg, S.Name) and arg.name.upper() == "NAN")
393
+ or (not_integral(arg, env, profile, wide) and (not profile.single_pass or _float_typed(arg, env, wide)))
394
+ or (timeout_value(arg, env, profile, wide) == ("wide", 0))
395
+ ):
396
+ diags.error(stmt.line, NOT_INTEGRAL, stmt.file)
397
+
398
+
399
+ def wide_constants(program: S.Program, profile) -> frozenset[str]:
400
+ """`Const` names declared as one positive numeric literal beyond the converted range: they
401
+ carry the literal's float type into every expression that uses them (oracle, 2026-10-02:
402
+ `Const KB = 999999999999` -> `KB`, `-KB` and `KB-1` all draw the error, while the computed
403
+ `Const KC = 2147483647+1` and `Const KM = -2147483649` wrap silently)."""
404
+ return frozenset(
405
+ st.name.upper()
406
+ for st in program.statements
407
+ if isinstance(st, S.ConstDecl)
408
+ and isinstance(st.value, S.Number)
409
+ and not st.value.text.startswith("&")
410
+ and _out_of_range(st.value.value, profile)
411
+ )
412
+
413
+
414
+ def _out_of_range(value, profile) -> bool:
415
+ if profile.name in ("cr300", "cr350"):
416
+ rounded = struct.unpack("<f", struct.pack("<f", float(value)))[0] if abs(value) < 3.4e38 else float("inf")
417
+ return not _INT_MIN <= rounded <= 2**31
418
+ return not _INT_MIN <= value <= _INT_MAX
419
+
420
+
421
+ def not_integral(expr: S.Expr | None, env: consteval.Env, profile, wide=frozenset()) -> bool:
422
+ """Whether a constant argument of an integer-typed function parameter draws `Parameter must be
423
+ integral.` (oracle, 2026-10-02): the expression is float-typed (a literal with a decimal point
424
+ or exponent, a `/` or `^`, a named constant whose value is not whole, or arithmetic with any
425
+ of those) or its value is outside the converted range: a 32-bit integer on CR6/CR1000X, a
426
+ 32-bit float rounded into [-2^31, 2^31] on CR300/CR350. A named constant otherwise counts as
427
+ an integer (`Const K = 60.0` passes). Hex and binary literals wrap to 32 bits and are never
428
+ flagged. Variables and function calls are left alone."""
429
+ if expr is None or not _plain(expr):
430
+ return False
431
+ try:
432
+ value = consteval.value_of(expr, env)
433
+ except consteval.NotConstant:
434
+ return False
435
+ if isinstance(value, (str, bool)) or not isinstance(value, (int, float)):
436
+ return False
437
+ if _float_typed(expr, env, wide) or value != int(value):
438
+ return True
439
+ literal = expr.operand if isinstance(expr, S.Unary) and expr.op == "-" else expr
440
+ if not isinstance(literal, S.Number) or literal.text.startswith("&"):
441
+ return False # (a computed or named value wraps or saturates silently, rule not fitted)
442
+ return _out_of_range(value, profile)
443
+
444
+
445
+ def _plain(expr: S.Expr | None) -> bool:
446
+ if isinstance(expr, (S.Number, S.Name)):
447
+ return True
448
+ if isinstance(expr, S.Unary):
449
+ return expr.op in ("-", "+") and _plain(expr.operand)
450
+ if isinstance(expr, S.Binary):
451
+ return expr.op in ("+", "-", "*", "/", "^", "\\", "MOD") and _plain(expr.left) and _plain(expr.right)
452
+ return False
453
+
454
+
455
+ def _float_typed(expr: S.Expr | None, env: consteval.Env, wide) -> bool:
456
+ if isinstance(expr, S.Number):
457
+ text = expr.text.upper()
458
+ return not text.startswith("&") and ("." in text or "E" in text)
459
+ if isinstance(expr, S.Name):
460
+ if expr.name.upper() in wide:
461
+ return True
462
+ try:
463
+ value = consteval.value_of(expr, env)
464
+ except consteval.NotConstant:
465
+ return False
466
+ return isinstance(value, float) and value != int(value)
467
+ if isinstance(expr, S.Unary):
468
+ return _float_typed(expr.operand, env, wide)
469
+ if isinstance(expr, S.Binary):
470
+ return expr.op in ("/", "^") or _float_typed(expr.left, env, wide) or _float_typed(expr.right, env, wide)
471
+ return False
472
+
473
+
474
+ _TIMEOUT_NEGATIVE = "Parameter TimeOut cannot be negative"
475
+
476
+
477
+ def _signed32(x: int) -> int:
478
+ x &= 0xFFFFFFFF
479
+ return x - 2**32 if x >= 2**31 else x
480
+
481
+
482
+ def timeout_value(arg, env, profile, wide=frozenset()):
483
+ """Classify a constant `TimeOut` argument: `("wide", 0)` when it is float-typed (a float literal,
484
+ or a decimal integer literal past the logger's 32-bit range, see `_decimal`), `("int", v)` for an
485
+ integer constant whose 32-bit value is `v`, or `None` when not modelled. Integer `+`, `-`,
486
+ `*` and unary signs on named constants are folded with 32-bit wrapping; other operators and
487
+ out-of-range `Const` values on CR6/CR1000X remain outside this classifier.
488
+ """
489
+ if isinstance(arg, S.Unary) and arg.op in ("-", "+"):
490
+ signs, lit = [], arg
491
+ while isinstance(lit, S.Unary) and lit.op in ("-", "+"):
492
+ signs.append(lit.op)
493
+ lit = lit.operand
494
+ sign = -1 if signs.count("-") % 2 else 1
495
+ if isinstance(lit, S.Number):
496
+ return _timeout_literal(lit, sign, profile)
497
+ value = timeout_value(lit, env, profile, wide)
498
+ if value is not None and value[0] == "int":
499
+ return ("int", _signed32(sign * value[1]))
500
+ return value
501
+ if isinstance(arg, S.Binary) and arg.op in ("+", "-", "*"):
502
+ left = timeout_value(arg.left, env, profile, wide)
503
+ right = timeout_value(arg.right, env, profile, wide)
504
+ if left is None or right is None or left[0] != "int" or right[0] != "int":
505
+ return None
506
+ a, b = left[1], right[1]
507
+ value = a + b if arg.op == "+" else a - b if arg.op == "-" else a * b
508
+ return ("int", _signed32(value))
509
+ if isinstance(arg, S.Number):
510
+ return _timeout_literal(arg, 1, profile)
511
+ if isinstance(arg, S.Name):
512
+ name = arg.name.upper()
513
+ if name in ("TRUE", "FALSE"):
514
+ return ("int", -1 if name == "TRUE" else 0)
515
+ if name in wide:
516
+ return ("wide", 0)
517
+ if env is None:
518
+ return None
519
+ try:
520
+ value = consteval.value_of(arg, env)
521
+ except consteval.NotConstant:
522
+ return None
523
+ if isinstance(value, bool) or not isinstance(value, int):
524
+ return None
525
+ if _INT_MIN <= value <= _INT_MAX:
526
+ return ("int", value)
527
+ return _decimal(abs(value), -1 if value < 0 else 1, profile, literal=False)
528
+ return None
529
+
530
+
531
+ def _timeout_literal(lit: S.Number, sign: int, profile):
532
+ text = lit.text.upper()
533
+ if text.startswith(("&H", "&B")):
534
+ digits = text[2:]
535
+ base, limit, alphabet = (16, 8, "0123456789ABCDEF") if text[1] == "H" else (2, 32, "01")
536
+ if not digits or len(digits) > limit or any(c not in alphabet for c in digits):
537
+ return None
538
+ return ("int", _signed32(sign * _signed32(int(digits, base))))
539
+ if text.startswith("&"):
540
+ return None
541
+ if "." in text or "E" in text:
542
+ return ("wide", 0) # a float literal
543
+ if not text.isdigit():
544
+ return None
545
+ return _decimal(int(text), sign, profile, literal=True)
546
+
547
+
548
+ def _decimal(magnitude: int, sign: int, profile, literal: bool):
549
+ """A decimal magnitude with its sign. CR300/CR350 (single pass): the float32-rounded magnitude
550
+ past 2147483648 is a float, otherwise the digits saturate at 2147483647 before the sign.
551
+ CR6/CR1000X: past 2147483647 is a float literal (except the exact literal -2147483648, integer
552
+ INT_MIN), while an out-of-range `Const` value is not modelled (`None`)."""
553
+ if profile.single_pass:
554
+ rounded = struct.unpack("<f", struct.pack("<f", float(magnitude)))[0]
555
+ if rounded > 2**31:
556
+ return ("wide", 0)
557
+ return ("int", sign * min(magnitude, _INT_MAX))
558
+ if magnitude > _INT_MAX:
559
+ if not literal:
560
+ return None
561
+ if sign < 0 and magnitude == 2**31:
562
+ return ("int", _INT_MIN)
563
+ return ("wide", 0)
564
+ return ("int", sign * magnitude)
565
+
566
+
567
+ def _check_call(call: S.Index | S.Call, scan, stmt: S.Stmt, diags: Diagnostics, env, after_error: bool = False) -> None:
568
+ values = [v for a in call.args[:3] if (v := _int(a, env)) is not None]
569
+ if len(call.args) != 3 or len(values) != 3 or values[2] == _MONTH or _INT_MIN in values[:2]:
570
+ return
571
+ if after_error and min(values[:2]) < 0:
572
+ return # a negative argument is only reported while the file is still clean
573
+ tinto_raw, interval_raw, unit = values
574
+ # an unknown unit scales both to 0, so the check always fails (oracle, round 55)
575
+ per_unit = instr_rules._US_PER_UNIT.get(unit, 0)
576
+ t_sec, t_us = _split(tinto_raw * per_unit)
577
+ i_sec, i_us = _split(interval_raw * per_unit)
578
+ if t_us < 0:
579
+ t_sec, t_us = i_sec - 1 + t_sec, t_us + 1_000_000
580
+ elif t_sec < 0:
581
+ t_sec += i_sec
582
+ if i_sec < 0 or i_us < 0 or t_sec < 0 or i_sec < t_sec or (t_sec == i_sec and i_us <= t_us):
583
+ diags.error(
584
+ stmt.line,
585
+ f"Interval ({interval_raw}) must be > 0 and TimeInto Interval ({tinto_raw}) must be < Interval.",
586
+ stmt.file,
587
+ )
588
+ if scan is None:
589
+ return
590
+ if i_sec < 3601:
591
+ interval, tinto = i_sec * 10000 + _cdiv(i_us, 100), t_sec * 10000 + _cdiv(t_us, 100)
592
+ step = _wrap32(scan[0] * 10000 + _cdiv(scan[1], 100))
593
+ else:
594
+ interval, tinto, step = i_sec, t_sec, scan[0]
595
+ if i_us or t_us:
596
+ diags.error(
597
+ stmt.line, "Illegal sub seconds in time into and/or interval when interval > 3600 secs.", stmt.file
598
+ )
599
+ if step and (_cmod(interval, step) or _cmod(tinto, step)):
600
+ diags.error(stmt.line, "Scan interval must be integrally divisible into interval.", stmt.file)
601
+
602
+
603
+ def _scan_step(stmt: S.Instruction, env: consteval.Env) -> tuple[int, int] | None:
604
+ """Seconds and microseconds the compiler keeps of a constant `Scan` interval, valid or not
605
+ (`instr_rules.scan_split`; oracle, all four loggers, 2026-10-10: `Scan("fuzz",Sec,...)` is
606
+ -2^31 s, whose step in 100 us wraps to 0 and so passes `IfTime(0,5,Min)` but not `(0,24,Hr)`)."""
607
+ return instr_rules.scan_split(stmt, env)
608
+
609
+
610
+ def _wrap32(n: int) -> int:
611
+ n &= 0xFFFFFFFF
612
+ return n - 2**32 if n >= 2**31 else n
613
+
614
+
615
+ def _split(total_us: int) -> tuple[int, int]:
616
+ """Whole seconds and microseconds, both truncated toward zero like the compiler's C code."""
617
+ sec = _cdiv(total_us, 1_000_000)
618
+ return sec, total_us - sec * 1_000_000
619
+
620
+
621
+ def _cdiv(a: int, b: int) -> int:
622
+ q = abs(a) // abs(b)
623
+ return q if (a >= 0) == (b > 0) else -q
624
+
625
+
626
+ def _cmod(a: int, b: int) -> int:
627
+ return a - b * _cdiv(a, b)
628
+
629
+
630
+ def _int(expr: S.Expr | None, env: consteval.Env) -> int | None:
631
+ if expr is None or not consteval.is_constant(expr, env):
632
+ return None
633
+ try:
634
+ value = consteval.value_of(expr, env)
635
+ except consteval.NotConstant:
636
+ return None
637
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or value != int(value):
638
+ return None
639
+ if isinstance(expr, S.Number) and expr.text.startswith("&"):
640
+ if int(value) > 0xFFFFFFFF:
641
+ return None # (real: silent for more than 8 hex digits; rule not fitted)
642
+ value = int(value) & 0xFFFFFFFF
643
+ return value - 2**32 if value >= 2**31 else value
644
+ inner = expr.operand if isinstance(expr, S.Unary) and expr.op == "-" else expr
645
+ if env.profile.name in ("cr300", "cr350") and isinstance(inner, S.Number):
646
+ return max(-_INT_MAX, min(_INT_MAX, int(value))) # a single literal saturates
647
+ return int(value) if _INT_MIN <= value <= _INT_MAX else None # computed: wraps, silent
648
+
649
+
650
+ def _calls(stmt: S.Stmt, names=_FUNCTIONS):
651
+ """Every call of `names` in `stmt`, including a one-line `If`'s statements."""
652
+ exprs: list[S.Expr | None] = []
653
+ if isinstance(stmt, (S.If, S.ElseIf, S.DoLoop)):
654
+ exprs.append(stmt.cond)
655
+ if isinstance(stmt, S.Call) and stmt.name.upper() in names:
656
+ yield stmt
657
+ if isinstance(stmt, S.If):
658
+ for inner in (stmt.inline or []) + (stmt.inline_else or []):
659
+ yield from _calls(inner, names)
660
+ elif isinstance(stmt, S.Assignment):
661
+ exprs += [stmt.target, stmt.value]
662
+ elif isinstance(stmt, (S.Instruction, S.Call)):
663
+ exprs += list(stmt.args)
664
+ elif isinstance(stmt, S.For):
665
+ exprs += [stmt.start, stmt.end, stmt.step]
666
+ stack = [e for e in exprs if e is not None]
667
+ while stack:
668
+ expr = stack.pop(0)
669
+ if isinstance(expr, S.Index):
670
+ if expr.name.upper() in names:
671
+ yield expr
672
+ stack += [a for a in expr.args if a is not None]
673
+ elif isinstance(expr, S.Unary) and expr.operand is not None:
674
+ stack.append(expr.operand)
675
+ elif isinstance(expr, S.Binary):
676
+ stack += [expr.left, expr.right]
677
+ elif isinstance(expr, S.Member):
678
+ stack += [a for a in (expr.args or []) if a is not None]