Barmaja-PyArduino 2.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.
- Barmaja_PyArduino/BARMAJA_PyArduino.ico +0 -0
- Barmaja_PyArduino/__init__.py +528 -0
- Barmaja_PyArduino/__init__.pyi +331 -0
- Barmaja_PyArduino/__main__.py +2 -0
- Barmaja_PyArduino/adapters/__init__.py +35 -0
- Barmaja_PyArduino/api_registry.py +459 -0
- Barmaja_PyArduino/backend/__init__.py +3 -0
- Barmaja_PyArduino/backend/uno_cpp.py +1680 -0
- Barmaja_PyArduino/board.py +80 -0
- Barmaja_PyArduino/cli.py +223 -0
- Barmaja_PyArduino/compiler.py +1721 -0
- Barmaja_PyArduino/console.py +217 -0
- Barmaja_PyArduino/device_packs.py +367 -0
- Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/src/Barmaja_Button.h +18 -0
- Barmaja_PyArduino/device_packs_builtin/Button/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/Button/pack.json +40 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/src/Barmaja_ClockDS3231.h +20 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/pack.json +136 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/src/Barmaja_DistanceVL53L0X.h +12 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/pack.json +48 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/src/Barmaja_LCDParallel.h +17 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/pack.json +137 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/src/Barmaja_LoadCellHX711.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/example.py +11 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/pack.json +74 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/src/Barmaja_NeoPixelStrip.h +15 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/example.py +13 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/pack.json +114 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/src/Barmaja_OLEDText.h +21 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/example.py +9 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/pack.json +103 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/src/Barmaja_PressureBMP280.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/pack.json +92 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/src/Barmaja_RotaryEncoder.h +18 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/pack.json +44 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/src/Barmaja_ServoDriverPCA9685.h +15 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/pack.json +117 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/src/Barmaja_ShiftRegister595.h +14 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/example.py +9 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/pack.json +83 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/src/Barmaja_TemperatureDS18B20.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/pack.json +63 -0
- Barmaja_PyArduino/doctor.py +406 -0
- Barmaja_PyArduino/environment.py +595 -0
- Barmaja_PyArduino/errors.py +166 -0
- Barmaja_PyArduino/flow.py +754 -0
- Barmaja_PyArduino/frontend.py +611 -0
- Barmaja_PyArduino/installer.py +151 -0
- Barmaja_PyArduino/ir.py +660 -0
- Barmaja_PyArduino/library_adder.py +440 -0
- Barmaja_PyArduino/live.py +859 -0
- Barmaja_PyArduino/lowering.py +596 -0
- Barmaja_PyArduino/numeric.py +748 -0
- Barmaja_PyArduino/pack_builder.py +544 -0
- Barmaja_PyArduino/py.typed +0 -0
- Barmaja_PyArduino/serial_monitor.py +172 -0
- Barmaja_PyArduino/source.py +184 -0
- Barmaja_PyArduino/stubgen.py +148 -0
- Barmaja_PyArduino/symbols.py +646 -0
- Barmaja_PyArduino/target.py +1631 -0
- Barmaja_PyArduino/toolchain.py +506 -0
- Barmaja_PyArduino/typesys.py +917 -0
- Barmaja_PyArduino/workflow.py +213 -0
- barmaja_pyarduino-2.0.0.dist-info/METADATA +39 -0
- barmaja_pyarduino-2.0.0.dist-info/RECORD +87 -0
- barmaja_pyarduino-2.0.0.dist-info/WHEEL +5 -0
- barmaja_pyarduino-2.0.0.dist-info/licenses/LICENSE.txt +7 -0
- barmaja_pyarduino-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .errors import NumericRangeError
|
|
7
|
+
from .symbols import SymbolId, SymbolKind
|
|
8
|
+
from .typesys import TypedModule, TypeKind
|
|
9
|
+
|
|
10
|
+
INT32_MIN = -(2**31)
|
|
11
|
+
INT32_MAX = 2**31 - 1
|
|
12
|
+
UINT32_MAX = 2**32 - 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class NumericRange:
|
|
17
|
+
minimum: int | None = None
|
|
18
|
+
maximum: int | None = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def known(self) -> bool:
|
|
22
|
+
return self.minimum is not None and self.maximum is not None
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def exact(cls, value: int) -> "NumericRange":
|
|
26
|
+
return cls(value, value)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def int32(cls) -> "NumericRange":
|
|
30
|
+
return cls(INT32_MIN, INT32_MAX)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def time_u32(cls) -> "NumericRange":
|
|
34
|
+
return cls(0, UINT32_MAX)
|
|
35
|
+
|
|
36
|
+
def union(self, other: "NumericRange") -> "NumericRange":
|
|
37
|
+
if not self.known or not other.known:
|
|
38
|
+
return NumericRange()
|
|
39
|
+
return NumericRange(min(self.minimum, other.minimum), max(self.maximum, other.maximum))
|
|
40
|
+
|
|
41
|
+
def within(self, minimum: int, maximum: int) -> bool:
|
|
42
|
+
return self.known and self.minimum >= minimum and self.maximum <= maximum
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
UNKNOWN_RANGE = NumericRange()
|
|
46
|
+
INT32_RANGE = NumericRange.int32()
|
|
47
|
+
TIME_U32_RANGE = NumericRange.time_u32()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class NumericAnalysis:
|
|
52
|
+
typed: TypedModule
|
|
53
|
+
symbol_ranges: dict[SymbolId, NumericRange]
|
|
54
|
+
expression_ranges: dict[int, NumericRange]
|
|
55
|
+
checked_int_expressions: frozenset[int]
|
|
56
|
+
checked_pwm_arguments: frozenset[int]
|
|
57
|
+
checked_time_thresholds: frozenset[int]
|
|
58
|
+
|
|
59
|
+
def range_for_expr(self, node: ast.AST) -> NumericRange:
|
|
60
|
+
return self.expression_ranges.get(id(node), UNKNOWN_RANGE)
|
|
61
|
+
|
|
62
|
+
def range_for_symbol_id(self, symbol_id: SymbolId) -> NumericRange:
|
|
63
|
+
return self.symbol_ranges.get(symbol_id, UNKNOWN_RANGE)
|
|
64
|
+
|
|
65
|
+
def int_expr_needs_runtime_check(self, node: ast.AST) -> bool:
|
|
66
|
+
return id(node) in self.checked_int_expressions
|
|
67
|
+
|
|
68
|
+
def pwm_arg_needs_runtime_check(self, node: ast.AST) -> bool:
|
|
69
|
+
return id(node) in self.checked_pwm_arguments
|
|
70
|
+
|
|
71
|
+
def time_threshold_needs_runtime_check(self, node: ast.AST) -> bool:
|
|
72
|
+
return id(node) in self.checked_time_thresholds
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NumericAnalyzer:
|
|
76
|
+
"""Target-aware Step-8 numeric range analysis for the Uno.
|
|
77
|
+
|
|
78
|
+
The analysis is deliberately conservative. A range fact is used only when
|
|
79
|
+
it is proven for every assignment represented by the supported subset.
|
|
80
|
+
Unknown dynamic Int arithmetic is lowered through checked runtime helpers.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, typed: TypedModule) -> None:
|
|
84
|
+
self.typed = typed
|
|
85
|
+
self.resolved = typed.resolved
|
|
86
|
+
self.tree = self.resolved.frontend.tree
|
|
87
|
+
self.symbol_ranges: dict[SymbolId, NumericRange] = {}
|
|
88
|
+
self.expression_ranges: dict[int, NumericRange] = {}
|
|
89
|
+
self.checked_int_expressions: set[int] = set()
|
|
90
|
+
self.checked_pwm_arguments: set[int] = set()
|
|
91
|
+
self.checked_time_thresholds: set[int] = set()
|
|
92
|
+
self._parents: dict[int, ast.AST] = {}
|
|
93
|
+
for parent in ast.walk(self.tree):
|
|
94
|
+
for child in ast.iter_child_nodes(parent):
|
|
95
|
+
self._parents[id(child)] = parent
|
|
96
|
+
|
|
97
|
+
def analyze(self) -> NumericAnalysis:
|
|
98
|
+
self._validate_integer_literals()
|
|
99
|
+
|
|
100
|
+
# A few fixed-point passes are enough for straight-line assignments,
|
|
101
|
+
# function parameters, and return propagation. Self-updating state is
|
|
102
|
+
# intentionally widened to the full Int domain.
|
|
103
|
+
for _ in range(16):
|
|
104
|
+
before = dict(self.symbol_ranges)
|
|
105
|
+
self.expression_ranges.clear()
|
|
106
|
+
self._scan_module(update_symbols=True)
|
|
107
|
+
if self.symbol_ranges == before:
|
|
108
|
+
break
|
|
109
|
+
else:
|
|
110
|
+
# Not converged: some ranges are still growing through an indirect
|
|
111
|
+
# cycle (e.g. `y = x * 2; x = y` in loop()) or a long backward chain.
|
|
112
|
+
# A partially iterated range is an UNDER-approximation, and trusting it
|
|
113
|
+
# would emit unchecked arithmetic that silently wraps on the Uno. Widen
|
|
114
|
+
# every still-changing symbol to its full domain until stable; each
|
|
115
|
+
# symbol can be widened at most once, so this terminates.
|
|
116
|
+
while True:
|
|
117
|
+
before = dict(self.symbol_ranges)
|
|
118
|
+
self.expression_ranges.clear()
|
|
119
|
+
self._scan_module(update_symbols=True)
|
|
120
|
+
changed = [sid for sid, fact in self.symbol_ranges.items() if before.get(sid) != fact]
|
|
121
|
+
if not changed:
|
|
122
|
+
break
|
|
123
|
+
for sid in changed:
|
|
124
|
+
typ = self.typed.symbol_types.get(sid)
|
|
125
|
+
self.symbol_ranges[sid] = TIME_U32_RANGE if typ is not None and typ.kind == TypeKind.TIME_U32 else INT32_RANGE
|
|
126
|
+
|
|
127
|
+
# Final pass records stable expression facts and runtime-check needs.
|
|
128
|
+
self.expression_ranges.clear()
|
|
129
|
+
self.checked_int_expressions.clear()
|
|
130
|
+
self.checked_pwm_arguments.clear()
|
|
131
|
+
self.checked_time_thresholds.clear()
|
|
132
|
+
self._scan_module(update_symbols=False)
|
|
133
|
+
self._validate_time_comparisons()
|
|
134
|
+
self._validate_pwm_boundaries()
|
|
135
|
+
|
|
136
|
+
return NumericAnalysis(
|
|
137
|
+
typed=self.typed,
|
|
138
|
+
symbol_ranges=dict(self.symbol_ranges),
|
|
139
|
+
expression_ranges=dict(self.expression_ranges),
|
|
140
|
+
checked_int_expressions=frozenset(self.checked_int_expressions),
|
|
141
|
+
checked_pwm_arguments=frozenset(self.checked_pwm_arguments),
|
|
142
|
+
checked_time_thresholds=frozenset(self.checked_time_thresholds),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# ---------- literal domain ----------
|
|
146
|
+
def _validate_integer_literals(self) -> None:
|
|
147
|
+
for node in ast.walk(self.tree):
|
|
148
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, float):
|
|
149
|
+
self._check_float_literal(node)
|
|
150
|
+
continue
|
|
151
|
+
if not isinstance(node, ast.Constant) or isinstance(node.value, bool) or not isinstance(node.value, int):
|
|
152
|
+
continue
|
|
153
|
+
parent = self._parents.get(id(node))
|
|
154
|
+
# Python parses -2147483648 as UnaryOp(USub, Constant(2147483648)).
|
|
155
|
+
if (
|
|
156
|
+
node.value == 2**31
|
|
157
|
+
and isinstance(parent, ast.UnaryOp)
|
|
158
|
+
and isinstance(parent.op, ast.USub)
|
|
159
|
+
and parent.operand is node
|
|
160
|
+
):
|
|
161
|
+
continue
|
|
162
|
+
if not (INT32_MIN <= node.value <= INT32_MAX):
|
|
163
|
+
raise NumericRangeError(
|
|
164
|
+
f"Integer literal {node.value} is outside the Barmaja V1 Int range "
|
|
165
|
+
f"[{INT32_MIN}, {INT32_MAX}].",
|
|
166
|
+
line=getattr(node, "lineno", None),
|
|
167
|
+
column=(getattr(node, "col_offset", 0) + 1) if hasattr(node, "col_offset") else None,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _check_float_literal(node: ast.Constant) -> None:
|
|
172
|
+
"""Float literals must be finite Arduino float32 values (about 1.2e-38 .. 3.4e38)."""
|
|
173
|
+
import math
|
|
174
|
+
import struct
|
|
175
|
+
value = node.value
|
|
176
|
+
problem = None
|
|
177
|
+
if not math.isfinite(value):
|
|
178
|
+
problem = "is not a finite number"
|
|
179
|
+
else:
|
|
180
|
+
try:
|
|
181
|
+
as_f32 = struct.unpack("<f", struct.pack("<f", value))[0]
|
|
182
|
+
except OverflowError:
|
|
183
|
+
as_f32 = math.inf
|
|
184
|
+
if math.isinf(as_f32):
|
|
185
|
+
problem = "is too large for an Arduino float (max about 3.4e38)"
|
|
186
|
+
elif value != 0.0 and abs(as_f32) < 1.1754943508222875e-38:
|
|
187
|
+
problem = "is too small for an Arduino float (min about 1.2e-38)"
|
|
188
|
+
if problem:
|
|
189
|
+
raise NumericRangeError(
|
|
190
|
+
f"Float literal {value!r} {problem}.",
|
|
191
|
+
line=getattr(node, "lineno", None),
|
|
192
|
+
column=(getattr(node, "col_offset", 0) + 1) if hasattr(node, "col_offset") else None,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# ---------- module / statement scan ----------
|
|
196
|
+
def _scan_module(self, *, update_symbols: bool) -> None:
|
|
197
|
+
for stmt in self.tree.body:
|
|
198
|
+
if isinstance(stmt, ast.FunctionDef):
|
|
199
|
+
continue
|
|
200
|
+
self._scan_stmt(stmt, update_symbols=update_symbols)
|
|
201
|
+
for stmt in self.tree.body:
|
|
202
|
+
if isinstance(stmt, ast.FunctionDef):
|
|
203
|
+
self._scan_function(stmt, update_symbols=update_symbols)
|
|
204
|
+
|
|
205
|
+
def _scan_function(self, fn: ast.FunctionDef, *, update_symbols: bool) -> None:
|
|
206
|
+
# Parameter ranges derive from all call sites.
|
|
207
|
+
sig = self.typed.function_signatures[fn.name]
|
|
208
|
+
if update_symbols:
|
|
209
|
+
calls = [
|
|
210
|
+
n for n in ast.walk(self.tree)
|
|
211
|
+
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == fn.name
|
|
212
|
+
]
|
|
213
|
+
for index, param in enumerate(sig.parameters):
|
|
214
|
+
ranges = []
|
|
215
|
+
for call in calls:
|
|
216
|
+
if index < len(call.args):
|
|
217
|
+
ranges.append(self._expr_range(call.args[index], record_checks=False))
|
|
218
|
+
if ranges:
|
|
219
|
+
merged = ranges[0]
|
|
220
|
+
for item in ranges[1:]:
|
|
221
|
+
merged = merged.union(item)
|
|
222
|
+
self._merge_symbol(param.symbol_id, merged)
|
|
223
|
+
for stmt in fn.body:
|
|
224
|
+
self._scan_stmt(stmt, update_symbols=update_symbols)
|
|
225
|
+
|
|
226
|
+
def _scan_stmt(self, stmt: ast.stmt, *, update_symbols: bool) -> None:
|
|
227
|
+
if isinstance(stmt, (ast.ImportFrom, ast.Import, ast.Pass, ast.Global, ast.Break, ast.Continue)):
|
|
228
|
+
return
|
|
229
|
+
if isinstance(stmt, ast.Expr):
|
|
230
|
+
self._expr_range(stmt.value)
|
|
231
|
+
return
|
|
232
|
+
if isinstance(stmt, ast.Assign):
|
|
233
|
+
value_range = self._expr_range(stmt.value)
|
|
234
|
+
for target in stmt.targets:
|
|
235
|
+
if isinstance(target, ast.Subscript):
|
|
236
|
+
# values[i * k] = x: the store index is an Int expression too.
|
|
237
|
+
self._expr_range(target.slice)
|
|
238
|
+
self._assign_target(target, value_range, stmt.value, update_symbols)
|
|
239
|
+
return
|
|
240
|
+
if isinstance(stmt, ast.AugAssign):
|
|
241
|
+
target_range = self._expr_range(stmt.target)
|
|
242
|
+
value_range = self._expr_range(stmt.value)
|
|
243
|
+
if isinstance(stmt.target, ast.Name):
|
|
244
|
+
sym = self.resolved.symbol_for_name(stmt.target)
|
|
245
|
+
typ = self.typed.symbol_types.get(sym.symbol_id)
|
|
246
|
+
if typ and typ.kind == TypeKind.INT and isinstance(stmt.op, (ast.Add, ast.Sub, ast.Mult)):
|
|
247
|
+
candidate = self._int_augop_range(stmt.op, target_range, value_range)
|
|
248
|
+
if candidate is not None:
|
|
249
|
+
lo, hi = candidate
|
|
250
|
+
if (
|
|
251
|
+
lo < INT32_MIN or hi > INT32_MAX
|
|
252
|
+
) and target_range.known and value_range.known and target_range.minimum == target_range.maximum and value_range.minimum == value_range.maximum:
|
|
253
|
+
raise self._overflow(stmt, f"constant augmented integer result [{lo}, {hi}]")
|
|
254
|
+
if update_symbols and typ and typ.kind == TypeKind.INT:
|
|
255
|
+
# Augmented state can execute repeatedly (especially in loop()),
|
|
256
|
+
# so keep the post-operation symbol conservative after validating
|
|
257
|
+
# any deterministic first-operation overflow above.
|
|
258
|
+
self.symbol_ranges[sym.symbol_id] = INT32_RANGE
|
|
259
|
+
return
|
|
260
|
+
if isinstance(stmt, ast.If):
|
|
261
|
+
self._expr_range(stmt.test)
|
|
262
|
+
for child in stmt.body:
|
|
263
|
+
self._scan_stmt(child, update_symbols=update_symbols)
|
|
264
|
+
for child in stmt.orelse:
|
|
265
|
+
self._scan_stmt(child, update_symbols=update_symbols)
|
|
266
|
+
return
|
|
267
|
+
if isinstance(stmt, ast.While):
|
|
268
|
+
self._expr_range(stmt.test)
|
|
269
|
+
for child in stmt.body:
|
|
270
|
+
self._scan_stmt(child, update_symbols=update_symbols)
|
|
271
|
+
return
|
|
272
|
+
if isinstance(stmt, ast.For):
|
|
273
|
+
iter_range = self._range_target_fact(stmt.iter)
|
|
274
|
+
if update_symbols and isinstance(stmt.target, ast.Name):
|
|
275
|
+
sym = self.resolved.symbol_for_name(stmt.target)
|
|
276
|
+
self._merge_symbol(sym.symbol_id, iter_range)
|
|
277
|
+
if isinstance(stmt.iter, ast.Call):
|
|
278
|
+
for arg in stmt.iter.args:
|
|
279
|
+
self._expr_range(arg)
|
|
280
|
+
for child in stmt.body:
|
|
281
|
+
self._scan_stmt(child, update_symbols=update_symbols)
|
|
282
|
+
return
|
|
283
|
+
if isinstance(stmt, ast.Return):
|
|
284
|
+
if stmt.value is not None:
|
|
285
|
+
self._expr_range(stmt.value)
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
def _assign_target(self, target: ast.AST, value_range: NumericRange, value: ast.AST, update_symbols: bool) -> None:
|
|
289
|
+
if isinstance(target, ast.Name):
|
|
290
|
+
if not update_symbols:
|
|
291
|
+
return
|
|
292
|
+
sym = self.resolved.symbol_for_name(target)
|
|
293
|
+
typ = self.typed.symbol_types.get(sym.symbol_id)
|
|
294
|
+
if typ is None or typ.kind not in {TypeKind.INT, TypeKind.TIME_U32}:
|
|
295
|
+
return
|
|
296
|
+
# Self-updating assignment can grow across runtime iterations; do
|
|
297
|
+
# not infer a falsely narrow bound from a few compiler passes.
|
|
298
|
+
if self._expr_reads_symbol(value, sym.symbol_id):
|
|
299
|
+
fact = TIME_U32_RANGE if typ.kind == TypeKind.TIME_U32 else INT32_RANGE
|
|
300
|
+
self.symbol_ranges[sym.symbol_id] = fact
|
|
301
|
+
return
|
|
302
|
+
self._merge_symbol(sym.symbol_id, value_range)
|
|
303
|
+
return
|
|
304
|
+
if isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (ast.Tuple, ast.List)):
|
|
305
|
+
for t, v in zip(target.elts, value.elts):
|
|
306
|
+
self._assign_target(t, self._expr_range(v), v, update_symbols)
|
|
307
|
+
|
|
308
|
+
def _expr_reads_symbol(self, node: ast.AST, symbol_id: SymbolId) -> bool:
|
|
309
|
+
for child in ast.walk(node):
|
|
310
|
+
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load):
|
|
311
|
+
try:
|
|
312
|
+
if self.resolved.symbol_for_name(child).symbol_id == symbol_id:
|
|
313
|
+
return True
|
|
314
|
+
except KeyError:
|
|
315
|
+
pass
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
def _merge_symbol(self, symbol_id: SymbolId, incoming: NumericRange) -> None:
|
|
319
|
+
if not incoming.known:
|
|
320
|
+
typ = self.typed.symbol_types.get(symbol_id)
|
|
321
|
+
if typ and typ.kind == TypeKind.TIME_U32:
|
|
322
|
+
incoming = TIME_U32_RANGE
|
|
323
|
+
elif typ and typ.kind == TypeKind.INT:
|
|
324
|
+
incoming = INT32_RANGE
|
|
325
|
+
else:
|
|
326
|
+
return
|
|
327
|
+
current = self.symbol_ranges.get(symbol_id)
|
|
328
|
+
self.symbol_ranges[symbol_id] = incoming if current is None else current.union(incoming)
|
|
329
|
+
|
|
330
|
+
# ---------- expression ranges ----------
|
|
331
|
+
def _expr_range(self, node: ast.AST, *, record_checks: bool = True) -> NumericRange:
|
|
332
|
+
result = self._expr_range_uncached(node, record_checks=record_checks)
|
|
333
|
+
self.expression_ranges[id(node)] = result
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
def _expr_range_uncached(self, node: ast.AST, *, record_checks: bool) -> NumericRange:
|
|
337
|
+
typ = self.typed.expression_types.get(id(node))
|
|
338
|
+
|
|
339
|
+
if isinstance(node, ast.Constant):
|
|
340
|
+
if isinstance(node.value, bool):
|
|
341
|
+
return NumericRange.exact(int(node.value))
|
|
342
|
+
if isinstance(node.value, int):
|
|
343
|
+
return NumericRange.exact(node.value)
|
|
344
|
+
return UNKNOWN_RANGE
|
|
345
|
+
|
|
346
|
+
if isinstance(node, ast.Name):
|
|
347
|
+
try:
|
|
348
|
+
sym = self.resolved.symbol_for_name(node)
|
|
349
|
+
except KeyError:
|
|
350
|
+
return UNKNOWN_RANGE
|
|
351
|
+
if sym.kind == SymbolKind.API:
|
|
352
|
+
constants = {
|
|
353
|
+
"LOW": 0, "HIGH": 1,
|
|
354
|
+
"A0": 14, "A1": 15, "A2": 16, "A3": 17, "A4": 18, "A5": 19,
|
|
355
|
+
}
|
|
356
|
+
if node.id in constants:
|
|
357
|
+
return NumericRange.exact(constants[node.id])
|
|
358
|
+
return UNKNOWN_RANGE
|
|
359
|
+
if typ and typ.kind == TypeKind.TIME_U32:
|
|
360
|
+
return self.symbol_ranges.get(sym.symbol_id, TIME_U32_RANGE)
|
|
361
|
+
if typ and typ.kind == TypeKind.INT:
|
|
362
|
+
return self.symbol_ranges.get(sym.symbol_id, INT32_RANGE)
|
|
363
|
+
return UNKNOWN_RANGE
|
|
364
|
+
|
|
365
|
+
if isinstance(node, ast.UnaryOp):
|
|
366
|
+
if isinstance(node.op, ast.USub):
|
|
367
|
+
if isinstance(node.operand, ast.Constant) and isinstance(node.operand.value, int) and not isinstance(node.operand.value, bool):
|
|
368
|
+
value = -node.operand.value
|
|
369
|
+
if not (INT32_MIN <= value <= INT32_MAX):
|
|
370
|
+
raise self._overflow(node, f"Unary '-' result {value}")
|
|
371
|
+
return NumericRange.exact(value)
|
|
372
|
+
operand = self._expr_range(node.operand, record_checks=record_checks)
|
|
373
|
+
if typ and typ.kind == TypeKind.INT:
|
|
374
|
+
if operand.known:
|
|
375
|
+
if operand.minimum == operand.maximum == INT32_MIN:
|
|
376
|
+
raise self._overflow(node, "Unary '-' result 2147483648")
|
|
377
|
+
if operand.minimum <= INT32_MIN <= operand.maximum:
|
|
378
|
+
if record_checks:
|
|
379
|
+
self.checked_int_expressions.add(id(node))
|
|
380
|
+
return INT32_RANGE
|
|
381
|
+
return NumericRange(-operand.maximum, -operand.minimum)
|
|
382
|
+
if record_checks:
|
|
383
|
+
self.checked_int_expressions.add(id(node))
|
|
384
|
+
return INT32_RANGE
|
|
385
|
+
self._expr_range(node.operand, record_checks=record_checks)
|
|
386
|
+
return UNKNOWN_RANGE
|
|
387
|
+
|
|
388
|
+
if isinstance(node, ast.BinOp):
|
|
389
|
+
left = self._expr_range(node.left, record_checks=record_checks)
|
|
390
|
+
right = self._expr_range(node.right, record_checks=record_checks)
|
|
391
|
+
if typ and typ.kind == TypeKind.TIME_U32 and isinstance(node.op, ast.Sub):
|
|
392
|
+
return TIME_U32_RANGE
|
|
393
|
+
if not typ or typ.kind != TypeKind.INT:
|
|
394
|
+
return UNKNOWN_RANGE
|
|
395
|
+
if isinstance(node.op, (ast.Add, ast.Sub, ast.Mult)):
|
|
396
|
+
candidate = self._int_binop_range(node, left, right)
|
|
397
|
+
if candidate is None:
|
|
398
|
+
if record_checks:
|
|
399
|
+
self.checked_int_expressions.add(id(node))
|
|
400
|
+
return INT32_RANGE
|
|
401
|
+
lo, hi = candidate
|
|
402
|
+
if lo < INT32_MIN or hi > INT32_MAX:
|
|
403
|
+
if left.known and right.known and left.minimum == left.maximum and right.minimum == right.maximum:
|
|
404
|
+
raise self._overflow(node, f"constant integer result [{lo}, {hi}]")
|
|
405
|
+
if record_checks:
|
|
406
|
+
self.checked_int_expressions.add(id(node))
|
|
407
|
+
return INT32_RANGE
|
|
408
|
+
return NumericRange(lo, hi)
|
|
409
|
+
# Division/modulo semantics and their runtime checks are Step 10.
|
|
410
|
+
return INT32_RANGE
|
|
411
|
+
|
|
412
|
+
if isinstance(node, ast.IfExp):
|
|
413
|
+
self._expr_range(node.test, record_checks=record_checks)
|
|
414
|
+
a = self._expr_range(node.body, record_checks=record_checks)
|
|
415
|
+
b = self._expr_range(node.orelse, record_checks=record_checks)
|
|
416
|
+
return a.union(b)
|
|
417
|
+
|
|
418
|
+
if isinstance(node, ast.BoolOp):
|
|
419
|
+
for value in node.values:
|
|
420
|
+
self._expr_range(value, record_checks=record_checks)
|
|
421
|
+
return NumericRange(0, 1)
|
|
422
|
+
|
|
423
|
+
if isinstance(node, ast.Compare):
|
|
424
|
+
self._expr_range(node.left, record_checks=record_checks)
|
|
425
|
+
for value in node.comparators:
|
|
426
|
+
self._expr_range(value, record_checks=record_checks)
|
|
427
|
+
return NumericRange(0, 1)
|
|
428
|
+
|
|
429
|
+
if isinstance(node, ast.Call):
|
|
430
|
+
for arg in node.args:
|
|
431
|
+
self._expr_range(arg, record_checks=record_checks)
|
|
432
|
+
return self._call_range(node)
|
|
433
|
+
|
|
434
|
+
if isinstance(node, ast.JoinedStr):
|
|
435
|
+
for part in node.values:
|
|
436
|
+
if isinstance(part, ast.FormattedValue):
|
|
437
|
+
self._expr_range(part.value, record_checks=record_checks)
|
|
438
|
+
return UNKNOWN_RANGE
|
|
439
|
+
|
|
440
|
+
if isinstance(node, ast.Subscript):
|
|
441
|
+
# The index expression must be analyzed like any other Int expression:
|
|
442
|
+
# skipping it left arithmetic such as values[(a * b) % n] UNCHECKED, so an
|
|
443
|
+
# Int32 overflow silently wrapped and selected the wrong element.
|
|
444
|
+
self._expr_range(node.value, record_checks=record_checks)
|
|
445
|
+
self._expr_range(node.slice, record_checks=record_checks)
|
|
446
|
+
if typ and typ.kind == TypeKind.INT:
|
|
447
|
+
return INT32_RANGE
|
|
448
|
+
return UNKNOWN_RANGE
|
|
449
|
+
|
|
450
|
+
if isinstance(node, ast.List):
|
|
451
|
+
# Elements of a list literal are ordinary expressions (checked arithmetic).
|
|
452
|
+
for item in node.elts:
|
|
453
|
+
self._expr_range(item, record_checks=record_checks)
|
|
454
|
+
return UNKNOWN_RANGE
|
|
455
|
+
|
|
456
|
+
# Closed-subset safety net: never leave a nested expression unanalyzed,
|
|
457
|
+
# because "not analyzed" would mean "not checked" for its Int arithmetic.
|
|
458
|
+
for child in ast.iter_child_nodes(node):
|
|
459
|
+
if isinstance(child, ast.expr):
|
|
460
|
+
self._expr_range(child, record_checks=record_checks)
|
|
461
|
+
return UNKNOWN_RANGE
|
|
462
|
+
|
|
463
|
+
def _int_binop_range(self, node: ast.BinOp, left: NumericRange, right: NumericRange) -> tuple[int, int] | None:
|
|
464
|
+
return self._int_augop_range(node.op, left, right)
|
|
465
|
+
|
|
466
|
+
@staticmethod
|
|
467
|
+
def _int_augop_range(op: ast.operator, left: NumericRange, right: NumericRange) -> tuple[int, int] | None:
|
|
468
|
+
if not left.known or not right.known:
|
|
469
|
+
return None
|
|
470
|
+
a, b, c, d = left.minimum, left.maximum, right.minimum, right.maximum
|
|
471
|
+
if isinstance(op, ast.Add):
|
|
472
|
+
return a + c, b + d
|
|
473
|
+
if isinstance(op, ast.Sub):
|
|
474
|
+
return a - d, b - c
|
|
475
|
+
if isinstance(op, ast.Mult):
|
|
476
|
+
values = (a * c, a * d, b * c, b * d)
|
|
477
|
+
return min(values), max(values)
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
def _call_range(self, node: ast.Call) -> NumericRange:
|
|
481
|
+
if isinstance(node.func, ast.Name):
|
|
482
|
+
name = node.func.id
|
|
483
|
+
if name == "analogRead":
|
|
484
|
+
return NumericRange(0, 1023)
|
|
485
|
+
if name == "digitalRead":
|
|
486
|
+
return NumericRange(0, 1)
|
|
487
|
+
if name == "millis":
|
|
488
|
+
return TIME_U32_RANGE
|
|
489
|
+
if name == "bool":
|
|
490
|
+
return NumericRange(0, 1)
|
|
491
|
+
if name == "abs" and node.args:
|
|
492
|
+
arg = self.expression_ranges.get(id(node.args[0]), UNKNOWN_RANGE)
|
|
493
|
+
arg_type = self.typed.expression_types.get(id(node.args[0]))
|
|
494
|
+
if arg_type and arg_type.kind == TypeKind.INT and arg.known:
|
|
495
|
+
if arg.minimum == arg.maximum == INT32_MIN:
|
|
496
|
+
raise self._overflow(node, "abs() result 2147483648")
|
|
497
|
+
if arg.minimum <= INT32_MIN <= arg.maximum:
|
|
498
|
+
return INT32_RANGE
|
|
499
|
+
return NumericRange(0, max(abs(arg.minimum), abs(arg.maximum)))
|
|
500
|
+
return INT32_RANGE
|
|
501
|
+
if name in {"min", "max"} and node.args:
|
|
502
|
+
facts = [self.expression_ranges.get(id(x), UNKNOWN_RANGE) for x in node.args]
|
|
503
|
+
if all(x.known for x in facts):
|
|
504
|
+
if name == "min":
|
|
505
|
+
return NumericRange(min(x.minimum for x in facts), min(x.maximum for x in facts))
|
|
506
|
+
return NumericRange(max(x.minimum for x in facts), max(x.maximum for x in facts))
|
|
507
|
+
if name == "int" and node.args:
|
|
508
|
+
arg_type = self.typed.expression_types.get(id(node.args[0]))
|
|
509
|
+
if arg_type and arg_type.kind == TypeKind.INT:
|
|
510
|
+
return self.expression_ranges.get(id(node.args[0]), INT32_RANGE)
|
|
511
|
+
if arg_type and arg_type.kind == TypeKind.FLOAT:
|
|
512
|
+
value = self._literal_float32(node.args[0])
|
|
513
|
+
if value is not None:
|
|
514
|
+
converted = int(value)
|
|
515
|
+
if not (INT32_MIN <= converted <= INT32_MAX):
|
|
516
|
+
raise self._overflow(node, f"int() result {converted}")
|
|
517
|
+
return NumericRange.exact(converted)
|
|
518
|
+
return INT32_RANGE
|
|
519
|
+
if name == "round" and node.args:
|
|
520
|
+
arg_type = self.typed.expression_types.get(id(node.args[0]))
|
|
521
|
+
# round(Int) is identity; round(Int, ndigits) is exactly
|
|
522
|
+
# constant-foldable when both inputs are literals/range-exact.
|
|
523
|
+
if arg_type and arg_type.kind == TypeKind.INT:
|
|
524
|
+
arg = self.expression_ranges.get(id(node.args[0]), UNKNOWN_RANGE)
|
|
525
|
+
if arg.known and arg.minimum == arg.maximum:
|
|
526
|
+
value = arg.minimum
|
|
527
|
+
ndigits = self._literal_int(node.args[1]) if len(node.args) == 2 else None
|
|
528
|
+
result = value if len(node.args) == 1 else round(value, ndigits)
|
|
529
|
+
if not (INT32_MIN <= result <= INT32_MAX):
|
|
530
|
+
raise self._overflow(node, f"round() result {result}")
|
|
531
|
+
return NumericRange.exact(result)
|
|
532
|
+
# One-argument round(Float) returns Int. Literal floats are
|
|
533
|
+
# first narrowed to the target float32 value, matching Uno.
|
|
534
|
+
if arg_type and arg_type.kind == TypeKind.FLOAT and len(node.args) == 1:
|
|
535
|
+
value = self._literal_float32(node.args[0])
|
|
536
|
+
if value is not None:
|
|
537
|
+
result = round(value)
|
|
538
|
+
if not (INT32_MIN <= result <= INT32_MAX):
|
|
539
|
+
raise self._overflow(node, f"round() result {result}")
|
|
540
|
+
return NumericRange.exact(result)
|
|
541
|
+
return INT32_RANGE
|
|
542
|
+
if name == "sum" and node.args:
|
|
543
|
+
arg_type = self.typed.expression_types.get(id(node.args[0]))
|
|
544
|
+
if arg_type and arg_type.kind == TypeKind.FIXED_LIST and arg_type.element_type and arg_type.element_type.kind == TypeKind.INT:
|
|
545
|
+
values = self._constant_fixed_int_list(node.args[0])
|
|
546
|
+
if values is not None:
|
|
547
|
+
total = 0
|
|
548
|
+
for value in values:
|
|
549
|
+
total += value
|
|
550
|
+
if not (INT32_MIN <= total <= INT32_MAX):
|
|
551
|
+
raise self._overflow(node, f"sum() intermediate result {total}")
|
|
552
|
+
return NumericRange.exact(total)
|
|
553
|
+
return INT32_RANGE
|
|
554
|
+
if name in self.typed.function_signatures:
|
|
555
|
+
# Union ranges of return expressions from the function body.
|
|
556
|
+
fn = next((x for x in self.tree.body if isinstance(x, ast.FunctionDef) and x.name == name), None)
|
|
557
|
+
if fn is not None:
|
|
558
|
+
ranges = [self._expr_range(x.value, record_checks=False) for x in ast.walk(fn) if isinstance(x, ast.Return) and x.value is not None]
|
|
559
|
+
if ranges:
|
|
560
|
+
merged = ranges[0]
|
|
561
|
+
for item in ranges[1:]:
|
|
562
|
+
merged = merged.union(item)
|
|
563
|
+
return merged
|
|
564
|
+
if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
|
565
|
+
owner, method = node.func.value.id, node.func.attr
|
|
566
|
+
if owner == "Serial":
|
|
567
|
+
if method == "available":
|
|
568
|
+
return NumericRange(0, INT32_MAX)
|
|
569
|
+
if method == "read":
|
|
570
|
+
return NumericRange(-1, 255)
|
|
571
|
+
if owner == "Wire":
|
|
572
|
+
if method == "available":
|
|
573
|
+
return NumericRange(0, INT32_MAX)
|
|
574
|
+
if method == "read":
|
|
575
|
+
return NumericRange(-1, 255)
|
|
576
|
+
try:
|
|
577
|
+
owner_sym = self.resolved.symbol_for_name(node.func.value)
|
|
578
|
+
owner_type = self.typed.symbol_types.get(owner_sym.symbol_id)
|
|
579
|
+
except KeyError:
|
|
580
|
+
owner_type = None
|
|
581
|
+
if owner_type and owner_type.kind == TypeKind.HARDWARE:
|
|
582
|
+
if owner_type.hardware_class == "Servo" and method == "read":
|
|
583
|
+
return NumericRange(0, 180)
|
|
584
|
+
if owner_type.hardware_class == "Servo" and method == "attached":
|
|
585
|
+
return NumericRange(0, 1)
|
|
586
|
+
if owner_type.hardware_class == "Ultrasonic" and method == "read":
|
|
587
|
+
return NumericRange(0, INT32_MAX)
|
|
588
|
+
typ = self.typed.expression_types.get(id(node))
|
|
589
|
+
if typ and typ.kind == TypeKind.TIME_U32:
|
|
590
|
+
return TIME_U32_RANGE
|
|
591
|
+
if typ and typ.kind == TypeKind.INT:
|
|
592
|
+
return INT32_RANGE
|
|
593
|
+
return UNKNOWN_RANGE
|
|
594
|
+
|
|
595
|
+
def _range_target_fact(self, iter_node: ast.AST) -> NumericRange:
|
|
596
|
+
if not isinstance(iter_node, ast.Call) or not isinstance(iter_node.func, ast.Name) or iter_node.func.id != "range":
|
|
597
|
+
return INT32_RANGE
|
|
598
|
+
args = iter_node.args
|
|
599
|
+
try:
|
|
600
|
+
values = [self._literal_int(x) for x in args]
|
|
601
|
+
except ValueError:
|
|
602
|
+
return INT32_RANGE
|
|
603
|
+
if any(v is None for v in values):
|
|
604
|
+
return INT32_RANGE
|
|
605
|
+
if len(values) == 1:
|
|
606
|
+
start, stop, step = 0, values[0], 1
|
|
607
|
+
elif len(values) == 2:
|
|
608
|
+
start, stop, step = values[0], values[1], 1
|
|
609
|
+
else:
|
|
610
|
+
start, stop, step = values
|
|
611
|
+
if step == 0:
|
|
612
|
+
return INT32_RANGE
|
|
613
|
+
seq = range(start, stop, step)
|
|
614
|
+
if len(seq) == 0:
|
|
615
|
+
return INT32_RANGE
|
|
616
|
+
first, last = seq[0], seq[-1]
|
|
617
|
+
return NumericRange(min(first, last), max(first, last))
|
|
618
|
+
|
|
619
|
+
@staticmethod
|
|
620
|
+
def _literal_int(node: ast.AST) -> int | None:
|
|
621
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool):
|
|
622
|
+
return node.value
|
|
623
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Constant) and isinstance(node.operand.value, int):
|
|
624
|
+
return -node.operand.value
|
|
625
|
+
return None
|
|
626
|
+
|
|
627
|
+
@staticmethod
|
|
628
|
+
def _literal_float32(node: ast.AST) -> float | None:
|
|
629
|
+
import struct
|
|
630
|
+
sign = 1.0
|
|
631
|
+
value_node = node
|
|
632
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
|
633
|
+
sign = -1.0
|
|
634
|
+
value_node = node.operand
|
|
635
|
+
if not isinstance(value_node, ast.Constant) or not isinstance(value_node.value, float):
|
|
636
|
+
return None
|
|
637
|
+
try:
|
|
638
|
+
return sign * struct.unpack("<f", struct.pack("<f", float(value_node.value)))[0]
|
|
639
|
+
except OverflowError:
|
|
640
|
+
return None
|
|
641
|
+
|
|
642
|
+
def _constant_fixed_int_list(self, node: ast.AST) -> list[int] | None:
|
|
643
|
+
"""Return immutable literal Int list contents when proven from source.
|
|
644
|
+
|
|
645
|
+
This is intentionally conservative: exactly one whole-list literal
|
|
646
|
+
assignment and no element stores/rebinds anywhere in the resolved
|
|
647
|
+
symbol's scope. It exists only to honor the locked compile-time
|
|
648
|
+
overflow rule for statically foldable sum().
|
|
649
|
+
"""
|
|
650
|
+
if not isinstance(node, ast.Name):
|
|
651
|
+
return None
|
|
652
|
+
try:
|
|
653
|
+
wanted = self.resolved.symbol_for_name(node).symbol_id
|
|
654
|
+
except KeyError:
|
|
655
|
+
return None
|
|
656
|
+
found: list[int] | None = None
|
|
657
|
+
whole_assignments = 0
|
|
658
|
+
for item in ast.walk(self.tree):
|
|
659
|
+
if isinstance(item, ast.Assign):
|
|
660
|
+
for target in item.targets:
|
|
661
|
+
if isinstance(target, ast.Name):
|
|
662
|
+
try:
|
|
663
|
+
same = self.resolved.symbol_for_name(target).symbol_id == wanted
|
|
664
|
+
except KeyError:
|
|
665
|
+
same = False
|
|
666
|
+
if same:
|
|
667
|
+
whole_assignments += 1
|
|
668
|
+
if not isinstance(item.value, ast.List):
|
|
669
|
+
return None
|
|
670
|
+
vals: list[int] = []
|
|
671
|
+
for elt in item.value.elts:
|
|
672
|
+
value = self._literal_int(elt)
|
|
673
|
+
if value is None:
|
|
674
|
+
return None
|
|
675
|
+
vals.append(value)
|
|
676
|
+
found = vals
|
|
677
|
+
elif isinstance(target, ast.Subscript) and isinstance(target.value, ast.Name):
|
|
678
|
+
try:
|
|
679
|
+
same = self.resolved.symbol_for_name(target.value).symbol_id == wanted
|
|
680
|
+
except KeyError:
|
|
681
|
+
same = False
|
|
682
|
+
if same:
|
|
683
|
+
return None
|
|
684
|
+
elif isinstance(item, ast.AugAssign) and isinstance(item.target, ast.Subscript) and isinstance(item.target.value, ast.Name):
|
|
685
|
+
try:
|
|
686
|
+
same = self.resolved.symbol_for_name(item.target.value).symbol_id == wanted
|
|
687
|
+
except KeyError:
|
|
688
|
+
same = False
|
|
689
|
+
if same:
|
|
690
|
+
return None
|
|
691
|
+
return found if whole_assignments == 1 else None
|
|
692
|
+
|
|
693
|
+
# ---------- target boundary rules ----------
|
|
694
|
+
def _validate_time_comparisons(self) -> None:
|
|
695
|
+
for node in ast.walk(self.tree):
|
|
696
|
+
if not isinstance(node, ast.Compare):
|
|
697
|
+
continue
|
|
698
|
+
values = [node.left, *node.comparators]
|
|
699
|
+
for left, right in zip(values, values[1:]):
|
|
700
|
+
lt = self.typed.expression_types.get(id(left))
|
|
701
|
+
rt = self.typed.expression_types.get(id(right))
|
|
702
|
+
if lt is None or rt is None:
|
|
703
|
+
continue
|
|
704
|
+
if lt.kind == TypeKind.TIME_U32 and rt.kind == TypeKind.INT:
|
|
705
|
+
self._validate_time_threshold(right)
|
|
706
|
+
elif rt.kind == TypeKind.TIME_U32 and lt.kind == TypeKind.INT:
|
|
707
|
+
self._validate_time_threshold(left)
|
|
708
|
+
|
|
709
|
+
def _validate_time_threshold(self, node: ast.AST) -> None:
|
|
710
|
+
fact = self.expression_ranges.get(id(node), UNKNOWN_RANGE)
|
|
711
|
+
if fact.known and fact.maximum < 0:
|
|
712
|
+
raise NumericRangeError(
|
|
713
|
+
"TimeU32 comparisons require a non-negative integer threshold.",
|
|
714
|
+
line=getattr(node, "lineno", None),
|
|
715
|
+
)
|
|
716
|
+
if fact.within(0, UINT32_MAX):
|
|
717
|
+
return
|
|
718
|
+
self.checked_time_thresholds.add(id(node))
|
|
719
|
+
|
|
720
|
+
def _validate_pwm_boundaries(self) -> None:
|
|
721
|
+
for node in ast.walk(self.tree):
|
|
722
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name) or node.func.id != "analogWrite":
|
|
723
|
+
continue
|
|
724
|
+
if len(node.args) < 2:
|
|
725
|
+
continue
|
|
726
|
+
value = node.args[1]
|
|
727
|
+
fact = self.expression_ranges.get(id(value), UNKNOWN_RANGE)
|
|
728
|
+
if fact.known and (fact.minimum < 0 or fact.maximum > 255):
|
|
729
|
+
if fact.minimum == fact.maximum:
|
|
730
|
+
raise NumericRangeError(
|
|
731
|
+
f"analogWrite() value {fact.minimum} is outside the Uno PWM range 0..255.",
|
|
732
|
+
line=getattr(value, "lineno", None),
|
|
733
|
+
)
|
|
734
|
+
# A proven partially out-of-range dynamic interval still needs
|
|
735
|
+
# a runtime boundary check; do not reject useful dynamic code.
|
|
736
|
+
self.checked_pwm_arguments.add(id(value))
|
|
737
|
+
elif not fact.within(0, 255):
|
|
738
|
+
self.checked_pwm_arguments.add(id(value))
|
|
739
|
+
|
|
740
|
+
def _overflow(self, node: ast.AST, description: str) -> NumericRangeError:
|
|
741
|
+
return NumericRangeError(
|
|
742
|
+
f"{description} is outside the Barmaja V1 signed 32-bit Int range.",
|
|
743
|
+
line=getattr(node, "lineno", None),
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def analyze_numeric_ranges(typed: TypedModule) -> NumericAnalysis:
|
|
748
|
+
return NumericAnalyzer(typed).analyze()
|