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.
Files changed (87) hide show
  1. Barmaja_PyArduino/BARMAJA_PyArduino.ico +0 -0
  2. Barmaja_PyArduino/__init__.py +528 -0
  3. Barmaja_PyArduino/__init__.pyi +331 -0
  4. Barmaja_PyArduino/__main__.py +2 -0
  5. Barmaja_PyArduino/adapters/__init__.py +35 -0
  6. Barmaja_PyArduino/api_registry.py +459 -0
  7. Barmaja_PyArduino/backend/__init__.py +3 -0
  8. Barmaja_PyArduino/backend/uno_cpp.py +1680 -0
  9. Barmaja_PyArduino/board.py +80 -0
  10. Barmaja_PyArduino/cli.py +223 -0
  11. Barmaja_PyArduino/compiler.py +1721 -0
  12. Barmaja_PyArduino/console.py +217 -0
  13. Barmaja_PyArduino/device_packs.py +367 -0
  14. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/library.properties +9 -0
  15. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/src/Barmaja_Button.h +18 -0
  16. Barmaja_PyArduino/device_packs_builtin/Button/example.py +10 -0
  17. Barmaja_PyArduino/device_packs_builtin/Button/pack.json +40 -0
  18. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/library.properties +9 -0
  19. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/src/Barmaja_ClockDS3231.h +20 -0
  20. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/example.py +8 -0
  21. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/pack.json +136 -0
  22. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/library.properties +9 -0
  23. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/src/Barmaja_DistanceVL53L0X.h +12 -0
  24. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/example.py +8 -0
  25. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/pack.json +48 -0
  26. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/library.properties +9 -0
  27. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/src/Barmaja_LCDParallel.h +17 -0
  28. Barmaja_PyArduino/device_packs_builtin/LCDParallel/example.py +10 -0
  29. Barmaja_PyArduino/device_packs_builtin/LCDParallel/pack.json +137 -0
  30. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/library.properties +9 -0
  31. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/src/Barmaja_LoadCellHX711.h +16 -0
  32. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/example.py +11 -0
  33. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/pack.json +74 -0
  34. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/library.properties +9 -0
  35. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/src/Barmaja_NeoPixelStrip.h +15 -0
  36. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/example.py +13 -0
  37. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/pack.json +114 -0
  38. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/library.properties +9 -0
  39. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/src/Barmaja_OLEDText.h +21 -0
  40. Barmaja_PyArduino/device_packs_builtin/OLEDText/example.py +9 -0
  41. Barmaja_PyArduino/device_packs_builtin/OLEDText/pack.json +103 -0
  42. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/library.properties +9 -0
  43. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/src/Barmaja_PressureBMP280.h +16 -0
  44. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/example.py +8 -0
  45. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/pack.json +92 -0
  46. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/library.properties +9 -0
  47. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/src/Barmaja_RotaryEncoder.h +18 -0
  48. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/example.py +10 -0
  49. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/pack.json +44 -0
  50. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/library.properties +9 -0
  51. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/src/Barmaja_ServoDriverPCA9685.h +15 -0
  52. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/example.py +10 -0
  53. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/pack.json +117 -0
  54. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/library.properties +9 -0
  55. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/src/Barmaja_ShiftRegister595.h +14 -0
  56. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/example.py +9 -0
  57. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/pack.json +83 -0
  58. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/library.properties +9 -0
  59. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/src/Barmaja_TemperatureDS18B20.h +16 -0
  60. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/example.py +8 -0
  61. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/pack.json +63 -0
  62. Barmaja_PyArduino/doctor.py +406 -0
  63. Barmaja_PyArduino/environment.py +595 -0
  64. Barmaja_PyArduino/errors.py +166 -0
  65. Barmaja_PyArduino/flow.py +754 -0
  66. Barmaja_PyArduino/frontend.py +611 -0
  67. Barmaja_PyArduino/installer.py +151 -0
  68. Barmaja_PyArduino/ir.py +660 -0
  69. Barmaja_PyArduino/library_adder.py +440 -0
  70. Barmaja_PyArduino/live.py +859 -0
  71. Barmaja_PyArduino/lowering.py +596 -0
  72. Barmaja_PyArduino/numeric.py +748 -0
  73. Barmaja_PyArduino/pack_builder.py +544 -0
  74. Barmaja_PyArduino/py.typed +0 -0
  75. Barmaja_PyArduino/serial_monitor.py +172 -0
  76. Barmaja_PyArduino/source.py +184 -0
  77. Barmaja_PyArduino/stubgen.py +148 -0
  78. Barmaja_PyArduino/symbols.py +646 -0
  79. Barmaja_PyArduino/target.py +1631 -0
  80. Barmaja_PyArduino/toolchain.py +506 -0
  81. Barmaja_PyArduino/typesys.py +917 -0
  82. Barmaja_PyArduino/workflow.py +213 -0
  83. barmaja_pyarduino-2.0.0.dist-info/METADATA +39 -0
  84. barmaja_pyarduino-2.0.0.dist-info/RECORD +87 -0
  85. barmaja_pyarduino-2.0.0.dist-info/WHEEL +5 -0
  86. barmaja_pyarduino-2.0.0.dist-info/licenses/LICENSE.txt +7 -0
  87. 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()