opencrbasic 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencrbasic/__init__.py +3 -0
- opencrbasic/__main__.py +5 -0
- opencrbasic/analysis.py +184 -0
- opencrbasic/bindings.py +1034 -0
- opencrbasic/catalog.py +173 -0
- opencrbasic/checkconfig.py +144 -0
- opencrbasic/checker/__init__.py +977 -0
- opencrbasic/checker/cr3xx.py +3118 -0
- opencrbasic/checker/ess.py +81 -0
- opencrbasic/checker/expressions.py +3581 -0
- opencrbasic/checker/instr_data.py +1802 -0
- opencrbasic/checker/instr_measure.py +1547 -0
- opencrbasic/checker/instr_rules.py +3540 -0
- opencrbasic/checker/instructions.py +17 -0
- opencrbasic/checker/intervals.py +678 -0
- opencrbasic/checker/longconv.py +133 -0
- opencrbasic/checker/scan_time.py +580 -0
- opencrbasic/checker/storage.py +456 -0
- opencrbasic/checker/structs.py +240 -0
- opencrbasic/checker/structure.py +6213 -0
- opencrbasic/checker/symbols.py +609 -0
- opencrbasic/checker/terminals.py +249 -0
- opencrbasic/checker/unused.py +186 -0
- opencrbasic/checker/var_types.py +564 -0
- opencrbasic/checker/warnings.py +661 -0
- opencrbasic/checks.py +491 -0
- opencrbasic/cli.py +391 -0
- opencrbasic/commands.py +128 -0
- opencrbasic/compiler.py +320 -0
- opencrbasic/consteval.py +327 -0
- opencrbasic/data/background_calibration.json +4 -0
- opencrbasic/data/before_beginprog.json +99 -0
- opencrbasic/data/cr1000x/background_calibration.json +4 -0
- opencrbasic/data/cr1000x/before_beginprog.json +138 -0
- opencrbasic/data/cr1000x/ess_variables.json +1087 -0
- opencrbasic/data/cr1000x/fnotch.json +29 -0
- opencrbasic/data/cr1000x/functions.json +1337 -0
- opencrbasic/data/cr1000x/instructions.json +4836 -0
- opencrbasic/data/cr1000x/measure.json +121 -0
- opencrbasic/data/cr1000x/mode.json +111 -0
- opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
- opencrbasic/data/cr1000x/settings_fields.json +321 -0
- opencrbasic/data/cr1000x/status_members.json +318 -0
- opencrbasic/data/cr1000x/symbols.json +872 -0
- opencrbasic/data/cr1000x/terminals.json +225 -0
- opencrbasic/data/cr300/before_beginprog.json +42 -0
- opencrbasic/data/cr300/ess_variables.json +1087 -0
- opencrbasic/data/cr300/fnotch.json +22 -0
- opencrbasic/data/cr300/functions.json +1261 -0
- opencrbasic/data/cr300/instructions.json +2837 -0
- opencrbasic/data/cr300/measure.json +33 -0
- opencrbasic/data/cr300/mode.json +121 -0
- opencrbasic/data/cr300/settings_fields.json +259 -0
- opencrbasic/data/cr300/status_members.json +303 -0
- opencrbasic/data/cr300/symbols.json +584 -0
- opencrbasic/data/cr300/terminals.json +193 -0
- opencrbasic/data/cr350/before_beginprog.json +44 -0
- opencrbasic/data/cr350/ess_variables.json +1087 -0
- opencrbasic/data/cr350/fnotch.json +22 -0
- opencrbasic/data/cr350/functions.json +1262 -0
- opencrbasic/data/cr350/instructions.json +2848 -0
- opencrbasic/data/cr350/measure.json +35 -0
- opencrbasic/data/cr350/mode.json +121 -0
- opencrbasic/data/cr350/settings_fields.json +292 -0
- opencrbasic/data/cr350/status_members.json +303 -0
- opencrbasic/data/cr350/symbols.json +572 -0
- opencrbasic/data/cr350/terminals.json +190 -0
- opencrbasic/data/ess_variables.json +1087 -0
- opencrbasic/data/fnotch.json +34 -0
- opencrbasic/data/functions.json +1337 -0
- opencrbasic/data/instructions.json +4965 -0
- opencrbasic/data/measure.json +123 -0
- opencrbasic/data/mode.json +121 -0
- opencrbasic/data/notices_cr6.json +890 -0
- opencrbasic/data/pipeline_tasks.json +84 -0
- opencrbasic/data/settings_fields.json +322 -0
- opencrbasic/data/status_members.json +319 -0
- opencrbasic/data/status_table.json +684 -0
- opencrbasic/data/symbols.json +902 -0
- opencrbasic/data/terminals.json +195 -0
- opencrbasic/data/type_codes.json +249 -0
- opencrbasic/diagnostics.py +196 -0
- opencrbasic/editing.py +827 -0
- opencrbasic/formatting.py +310 -0
- opencrbasic/helpdocs.py +677 -0
- opencrbasic/hover.py +667 -0
- opencrbasic/inlay.py +169 -0
- opencrbasic/lexer.py +183 -0
- opencrbasic/lsp/__init__.py +5 -0
- opencrbasic/lsp/__main__.py +7 -0
- opencrbasic/lsp/config.py +202 -0
- opencrbasic/lsp/features.py +602 -0
- opencrbasic/lsp/server.py +741 -0
- opencrbasic/lsp/session.py +1189 -0
- opencrbasic/lsp/worker.py +128 -0
- opencrbasic/mode.py +143 -0
- opencrbasic/navigation.py +401 -0
- opencrbasic/parser.py +3264 -0
- opencrbasic/positions.py +274 -0
- opencrbasic/preprocess.py +269 -0
- opencrbasic/profiles.py +266 -0
- opencrbasic/provenance.py +17 -0
- opencrbasic/quickfix.py +331 -0
- opencrbasic/rename.py +662 -0
- opencrbasic/signature.py +33 -0
- opencrbasic/source.py +600 -0
- opencrbasic/sources.py +125 -0
- opencrbasic/syntax.py +337 -0
- opencrbasic/tables.py +2173 -0
- opencrbasic/tables_hook.py +14 -0
- opencrbasic/tdf.py +142 -0
- opencrbasic/timing.py +565 -0
- opencrbasic-1.0.0.dist-info/METADATA +223 -0
- opencrbasic-1.0.0.dist-info/RECORD +117 -0
- opencrbasic-1.0.0.dist-info/WHEEL +4 -0
- opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
- opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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]
|