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,1631 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ import ast
5
+ from typing import Iterable
6
+
7
+ from .api_registry import CONSTANT_VALUES, DEPENDENCIES, HARDWARE_CLASSES, SINGLETONS, CORE_FUNCTIONS
8
+ from . import device_packs as _packs
9
+ from .board import BoardProfile
10
+ from .errors import CompileError, HardwareError, InternalCompilerError, UnsupportedFeatureError
11
+ from .flow import StorageClass
12
+ from .ir import (
13
+ IRModule, IRStmt, IRExpr, IRConstant, IRLoad, IRListLiteral, IRIndex, IRUnary, IRUnaryOp, IRBinary, IRBinaryOp,
14
+ IRCompareChain, IRBool, IRConditional, IRCall, IRCallKind, IRFormatString,
15
+ IRStore, IRIndexStore, IRAugStore, IRExprStmt, IRPrint, IRIf, IRWhile, IRRangeLoop,
16
+ IRFixedListLoop, IRReturn, IRObjectDecl,
17
+ )
18
+ from .numeric import NumericAnalysis
19
+ from .source import SourceSpan
20
+ from .typesys import TypeKind, BarmajaType
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class TargetDiagnostic:
25
+ category: str
26
+ code: str
27
+ severity: str
28
+ message: str
29
+ source: str | None = None
30
+ span: SourceSpan | None = None
31
+ hint: str | None = None
32
+ related: tuple[SourceSpan, ...] = ()
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class TargetPlan:
37
+ board: BoardProfile
38
+ ir_module: IRModule
39
+ includes: tuple[str, ...]
40
+ dependency_ids: tuple[str, ...]
41
+ setup_hooks: tuple[str, ...]
42
+ resolved_serial_baud: int | None
43
+ diagnostics: tuple[TargetDiagnostic, ...]
44
+ static_sram_estimate: int
45
+ resources: frozenset[str]
46
+ lifecycle_facts: tuple[str, ...]
47
+ # Compile-time integer value of each hardware-constructor argument, keyed by
48
+ # object symbol id. Constructors run before setup(), so the backend must
49
+ # emit these proven values instead of references to setup-time variables.
50
+ object_arg_values: tuple[tuple[object, tuple[int | None, ...]], ...] = ()
51
+ # True when the program can WRITE text to the USB Serial port (Python print(),
52
+ # Serial.print/println/write). Used by the PC-side monitor; Serial.begin alone
53
+ # or Serial.available()/read() do not need a PC console.
54
+ serial_output: bool = False
55
+
56
+ @property
57
+ def warnings(self) -> tuple[TargetDiagnostic, ...]:
58
+ return tuple(d for d in self.diagnostics if d.severity == "warning")
59
+
60
+
61
+ @dataclass
62
+ class _LifeState:
63
+ dht_begun: set[object] = field(default_factory=set)
64
+ lcd_inited: set[object] = field(default_factory=set)
65
+ servo_attached: set[object] = field(default_factory=set)
66
+ modules_started: set[object] = field(default_factory=set)
67
+ wire_begun: bool = False
68
+ serial_begun: bool = False
69
+
70
+ def copy(self) -> "_LifeState":
71
+ return _LifeState(set(self.dht_begun), set(self.lcd_inited), set(self.servo_attached), set(self.modules_started), self.wire_begun, self.serial_begun)
72
+
73
+ @staticmethod
74
+ def intersect(a: "_LifeState", b: "_LifeState") -> "_LifeState":
75
+ return _LifeState(
76
+ a.dht_begun & b.dht_begun,
77
+ a.lcd_inited & b.lcd_inited,
78
+ a.servo_attached & b.servo_attached,
79
+ a.modules_started & b.modules_started,
80
+ a.wire_begun and b.wire_begun,
81
+ a.serial_begun and b.serial_begun,
82
+ )
83
+
84
+
85
+ class TargetAnalyzer:
86
+ def __init__(self, ir_module: IRModule, numeric: NumericAnalysis, board: BoardProfile):
87
+ self.ir = ir_module
88
+ self.numeric = numeric
89
+ self.board = board
90
+ self._diagnostics: list[TargetDiagnostic] = []
91
+ self._diag_keys: set[tuple] = set()
92
+ self._dependencies: set[str] = set()
93
+ self._includes: set[str] = {"Arduino.h"}
94
+ self._resources: set[str] = set()
95
+ self._lifecycle_facts: set[str] = set()
96
+ self._functions = {fn.symbol.symbol_id: fn for fn in ir_module.functions}
97
+ self._storages = {d.symbol.symbol_id: d for d in ir_module.storages}
98
+ self._object_classes = {o.symbol.symbol_id: o.class_name for o in ir_module.objects}
99
+ self._symbol_values: dict[object, frozenset[int]] = {}
100
+ self._list_values: dict[object, frozenset[int]] = {}
101
+ self._servo_attach_sites: list[SourceSpan] = []
102
+ self._servo_attached_objects: set[object] = set()
103
+ self._pwm_timer1_sites: list[SourceSpan] = []
104
+ self._pwm_timer2_sites: list[SourceSpan] = []
105
+ self._tone_sites: list[SourceSpan] = []
106
+ self._serial_sites: list[SourceSpan] = []
107
+ self._serial_pin_sites: list[SourceSpan] = []
108
+ self._i2c_sites: list[SourceSpan] = []
109
+ self._i2c_pin_sites: list[SourceSpan] = []
110
+ self._explicit_serial_bauds: set[int] = set()
111
+ self._function_return_values: dict[object, frozenset[int]] = {}
112
+ self._object_arg_values: dict[object, tuple[int | None, ...]] = {}
113
+ self._lcd_begin_needs_wire = False
114
+ self._prepare_static_values()
115
+ self._prepare_function_return_values()
116
+
117
+ def analyze(self) -> TargetPlan:
118
+ self._analyze_object_declarations()
119
+ self._collect_dependencies_from_all_ir()
120
+ self._validate_known_constraints_in_all_functions()
121
+
122
+ serial_auto, serial_baud = self._analyze_serial_policy()
123
+
124
+ env = dict(self._symbol_values)
125
+ life = _LifeState(serial_begun=serial_auto)
126
+ if serial_auto:
127
+ self._lifecycle_facts.add("Serial begun automatically at 9600")
128
+ life, env = self._walk_sequence(self.ir.setup, life, env, call_stack=())
129
+ # setup lifecycle facts dominate each Arduino loop invocation.
130
+ self._walk_sequence(self.ir.main_loop, life.copy(), dict(env), call_stack=())
131
+
132
+ self._emit_resource_conflict_warnings()
133
+ self._validate_servo_capacity()
134
+ static_sram = self._estimate_static_sram()
135
+ self._validate_memory(static_sram)
136
+
137
+ hooks: list[str] = []
138
+ if serial_auto:
139
+ hooks.append("Serial.begin(9600);")
140
+ if self._lcd_begin_needs_wire:
141
+ hooks.append("Wire.begin();")
142
+ self._lifecycle_facts.add("Wire begun automatically for LiquidCrystal_I2C.begin()")
143
+ setup_hooks = tuple(hooks)
144
+ return TargetPlan(
145
+ board=self.board,
146
+ ir_module=self.ir,
147
+ includes=tuple(["Arduino.h", *sorted(self._includes - {"Arduino.h"})]),
148
+ dependency_ids=tuple(sorted(self._dependencies)),
149
+ setup_hooks=setup_hooks,
150
+ resolved_serial_baud=serial_baud,
151
+ diagnostics=tuple(self._diagnostics),
152
+ static_sram_estimate=static_sram,
153
+ resources=frozenset(self._resources),
154
+ lifecycle_facts=tuple(sorted(self._lifecycle_facts)),
155
+ object_arg_values=tuple(self._object_arg_values.items()),
156
+ serial_output=getattr(self, "_serial_output", False),
157
+ )
158
+
159
+ # ---------- Serial policy / initialization ----------
160
+ def _analyze_serial_policy(self) -> tuple[bool, int | None]:
161
+ """Resolve the single V1 Serial initialization policy.
162
+
163
+ Python ``print()`` owns an automatic 9600-baud setup hook unless the
164
+ student supplied one valid, direct setup-time ``Serial.begin``. Any
165
+ explicit begin outside top-level setup is rejected so target lifecycle
166
+ analysis can reason about one stable configuration.
167
+ """
168
+
169
+ begin_calls: list[tuple[IRCall, str, bool]] = []
170
+ has_python_print = False
171
+ explicit_serial_io: list[IRCall] = []
172
+
173
+ def visit_expr(expr: IRExpr, context: str, top_level: bool) -> None:
174
+ if isinstance(expr, IRCall):
175
+ if (
176
+ expr.kind == IRCallKind.SINGLETON_METHOD
177
+ and expr.owner_name == "Serial"
178
+ ):
179
+ if expr.method_name == "begin":
180
+ begin_calls.append((expr, context, top_level))
181
+ else:
182
+ explicit_serial_io.append(expr)
183
+ for arg in expr.args:
184
+ visit_expr(arg, context, False)
185
+ elif isinstance(expr, IRUnary):
186
+ visit_expr(expr.operand, context, False)
187
+ elif isinstance(expr, IRBinary):
188
+ visit_expr(expr.left, context, False); visit_expr(expr.right, context, False)
189
+ elif isinstance(expr, IRCompareChain):
190
+ for item in expr.operands: visit_expr(item, context, False)
191
+ elif isinstance(expr, IRBool):
192
+ for item in expr.values: visit_expr(item, context, False)
193
+ elif isinstance(expr, IRConditional):
194
+ visit_expr(expr.condition, context, False)
195
+ visit_expr(expr.when_true, context, False)
196
+ visit_expr(expr.when_false, context, False)
197
+ elif isinstance(expr, IRIndex):
198
+ visit_expr(expr.collection, context, False); visit_expr(expr.index, context, False)
199
+ elif isinstance(expr, IRListLiteral):
200
+ for item in expr.elements: visit_expr(item, context, False)
201
+ elif isinstance(expr, IRFormatString):
202
+ for part in expr.parts:
203
+ if not isinstance(part, str): visit_expr(part.value, context, False)
204
+
205
+ def visit_stmts(stmts: tuple[IRStmt, ...], context: str, direct: bool) -> None:
206
+ nonlocal has_python_print
207
+ for stmt in stmts:
208
+ if isinstance(stmt, IRPrint):
209
+ has_python_print = True
210
+ for value in stmt.values: visit_expr(value, context, False)
211
+ elif isinstance(stmt, IRExprStmt):
212
+ visit_expr(stmt.expression, context, direct)
213
+ elif isinstance(stmt, IRStore):
214
+ visit_expr(stmt.value, context, False)
215
+ elif isinstance(stmt, IRIndexStore):
216
+ visit_expr(stmt.index, context, False); visit_expr(stmt.value, context, False)
217
+ elif isinstance(stmt, IRAugStore):
218
+ visit_expr(stmt.value, context, False)
219
+ elif isinstance(stmt, IRIf):
220
+ visit_expr(stmt.condition, context, False)
221
+ visit_stmts(stmt.body, context, False); visit_stmts(stmt.otherwise, context, False)
222
+ elif isinstance(stmt, IRWhile):
223
+ visit_expr(stmt.condition, context, False); visit_stmts(stmt.body, context, False)
224
+ elif isinstance(stmt, IRRangeLoop):
225
+ visit_expr(stmt.start, context, False); visit_expr(stmt.stop, context, False); visit_expr(stmt.step, context, False)
226
+ visit_stmts(stmt.body, context, False)
227
+ elif isinstance(stmt, IRFixedListLoop):
228
+ if stmt.index_start is not None: visit_expr(stmt.index_start, context, False)
229
+ visit_stmts(stmt.body, context, False)
230
+ elif isinstance(stmt, IRReturn) and stmt.value is not None:
231
+ visit_expr(stmt.value, context, False)
232
+
233
+ visit_stmts(self.ir.setup, "setup", True)
234
+ visit_stmts(self.ir.main_loop, "main loop", True)
235
+ for fn in self.ir.functions:
236
+ visit_stmts(fn.body, f"function '{fn.symbol.source_name}'", True)
237
+
238
+ # PC-side monitor metadata: does the program ever write to USB Serial?
239
+ self._serial_output = bool(has_python_print) or any(
240
+ c.method_name in {"print", "println", "write"} for c in explicit_serial_io
241
+ )
242
+
243
+ if len(begin_calls) > 1:
244
+ call = begin_calls[1][0]
245
+ self._hardware_error(
246
+ "Barmaja PyArduino V1 allows only one explicit Serial.begin(...) call.",
247
+ call.span,
248
+ "Keep one setup-time Serial.begin(baud) call and remove the others.",
249
+ )
250
+
251
+ explicit_baud: int | None = None
252
+ if begin_calls:
253
+ call, context, is_top_level = begin_calls[0]
254
+ if context != "setup" or not is_top_level:
255
+ self._hardware_error(
256
+ "Serial.begin(...) must be a direct top-level setup statement in Barmaja PyArduino V1.",
257
+ call.span,
258
+ "Move Serial.begin(baud) before the main while True loop and outside functions/conditions.",
259
+ )
260
+ if len(call.args) != 1:
261
+ self._hardware_error("Serial.begin(...) requires exactly one baud value.", call.span)
262
+ explicit_baud = self._single_int(call.args[0], dict(self._symbol_values))
263
+ if explicit_baud is None or explicit_baud <= 0:
264
+ self._hardware_error(
265
+ "Serial.begin(...) baud must be a positive compile-time integer.",
266
+ call.span,
267
+ "Use a fixed value such as Serial.begin(9600) or Serial.begin(115200).",
268
+ )
269
+
270
+ if explicit_baud is not None:
271
+ self._explicit_serial_bauds.add(explicit_baud)
272
+ return False, explicit_baud
273
+ if has_python_print:
274
+ return True, 9600
275
+ if explicit_serial_io:
276
+ first = explicit_serial_io[0]
277
+ self._hardware_error(
278
+ f"Serial.{first.method_name}() requires Serial.begin(baud) in setup when Python print() is not used.",
279
+ first.span,
280
+ "Add Serial.begin(9600) or another fixed positive baud before the main loop.",
281
+ )
282
+ return False, None
283
+
284
+ # ---------- declarations / dependency selection ----------
285
+ def _prepare_static_values(self) -> None:
286
+ for decl in self.ir.storages:
287
+ if decl.initializer is None:
288
+ continue
289
+ values = self._value_set(decl.initializer, {})
290
+ if values is not None:
291
+ if decl.symbol.type.kind == TypeKind.FIXED_LIST:
292
+ self._list_values[decl.symbol.symbol_id] = values
293
+ elif len(values) <= 64:
294
+ self._symbol_values[decl.symbol.symbol_id] = values
295
+
296
+ # Some storage declarations intentionally omit initializers in IR because
297
+ # initialization is represented in the executable setup/module stream.
298
+ # Recover immutable fixed-list/tuple constant facts from the already
299
+ # resolved source tree so pin-set analysis can prove safe loop patterns.
300
+ resolved = self.numeric.typed.resolved
301
+ unknown_lists: set = set()
302
+ for node in ast.walk(resolved.frontend.tree):
303
+ if not isinstance(node, ast.Assign) or len(node.targets) != 1:
304
+ continue
305
+ target = node.targets[0]
306
+ if isinstance(target, ast.Name) and isinstance(node.value, ast.List):
307
+ vals = self._ast_int_values(node.value.elts)
308
+ try:
309
+ sym = resolved.symbol_for_name(target)
310
+ except KeyError:
311
+ continue
312
+ sid = sym.symbol_id
313
+ # A list may be rebound to several literals: its facts are the UNION of
314
+ # all of them, and any non-constant literal makes the list unknown.
315
+ if vals is None or sid in unknown_lists:
316
+ unknown_lists.add(sid)
317
+ self._list_values.pop(sid, None)
318
+ continue
319
+ merged = self._list_values.get(sid, frozenset()) | vals
320
+ if len(merged) > 64:
321
+ unknown_lists.add(sid)
322
+ self._list_values.pop(sid, None)
323
+ else:
324
+ self._list_values[sid] = merged
325
+ elif isinstance(target, (ast.Tuple, ast.List)) and isinstance(node.value, (ast.Tuple, ast.List)):
326
+ for t, v in zip(target.elts, node.value.elts):
327
+ if not isinstance(t, ast.Name):
328
+ continue
329
+ value = self._ast_single_int(v)
330
+ if value is None:
331
+ continue
332
+ try:
333
+ sym = resolved.symbol_for_name(t)
334
+ except KeyError:
335
+ continue
336
+ self._symbol_values[sym.symbol_id] = frozenset({value})
337
+
338
+ self._merge_list_element_stores()
339
+
340
+ def _merge_list_element_stores(self) -> None:
341
+ """Pin facts for a fixed list must include every value stored into it anywhere.
342
+
343
+ An element store whose value is not statically known makes the list's
344
+ pin set unknown, so later pin checks cannot trust the initializer alone.
345
+ """
346
+ stores: list[IRIndexStore] = []
347
+ def visit(stmts) -> None:
348
+ for stmt in stmts:
349
+ if isinstance(stmt, IRIndexStore):
350
+ stores.append(stmt)
351
+ for child_name in ("body", "otherwise"):
352
+ child = getattr(stmt, child_name, None)
353
+ if isinstance(child, tuple):
354
+ visit(child)
355
+ visit(self.ir.setup); visit(self.ir.main_loop)
356
+ for fn in self.ir.functions:
357
+ visit(fn.body)
358
+ for stmt in stores:
359
+ sid = stmt.collection.symbol_id
360
+ if sid not in self._list_values:
361
+ continue
362
+ values = self._value_set(stmt.value, {})
363
+ if values is None:
364
+ del self._list_values[sid]
365
+ continue
366
+ merged = self._list_values[sid] | values
367
+ if len(merged) > 64:
368
+ del self._list_values[sid]
369
+ else:
370
+ self._list_values[sid] = merged
371
+
372
+ @staticmethod
373
+ def _ast_single_int(node: ast.AST) -> int | None:
374
+ if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool):
375
+ return node.value
376
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Constant) and isinstance(node.operand.value, int):
377
+ return -node.operand.value
378
+ if isinstance(node, ast.Name) and node.id in CONSTANT_VALUES:
379
+ return CONSTANT_VALUES[node.id]
380
+ return None
381
+
382
+ @classmethod
383
+ def _ast_int_values(cls, nodes: list[ast.AST]) -> frozenset[int] | None:
384
+ values: set[int] = set()
385
+ for node in nodes:
386
+ value = cls._ast_single_int(node)
387
+ if value is None:
388
+ return None
389
+ values.add(value)
390
+ return frozenset(values)
391
+
392
+
393
+ def _prepare_function_return_values(self) -> None:
394
+ # Small fixed-point summary used only to prove finite hardware-pin sets.
395
+ # Functions whose return depends on unknown parameters remain unknown.
396
+ for _ in range(8):
397
+ changed = False
398
+ for fn in self.ir.functions:
399
+ values: set[int] = set()
400
+ valid = True
401
+ found = False
402
+ for stmt in fn.body:
403
+ if isinstance(stmt, IRReturn) and stmt.value is not None:
404
+ found = True
405
+ got = self._value_set(stmt.value, {})
406
+ if got is None:
407
+ valid = False
408
+ break
409
+ values.update(got)
410
+ elif isinstance(stmt, IRIf):
411
+ for branch_stmt in (*stmt.body, *stmt.otherwise):
412
+ if isinstance(branch_stmt, IRReturn) and branch_stmt.value is not None:
413
+ found = True
414
+ got = self._value_set(branch_stmt.value, {})
415
+ if got is None:
416
+ valid = False
417
+ break
418
+ values.update(got)
419
+ if not valid:
420
+ break
421
+ if valid and found and len(values) <= 64:
422
+ frozen = frozenset(values)
423
+ if self._function_return_values.get(fn.symbol.symbol_id) != frozen:
424
+ self._function_return_values[fn.symbol.symbol_id] = frozen
425
+ changed = True
426
+ if not changed:
427
+ break
428
+
429
+ def _analyze_object_declarations(self) -> None:
430
+ software_serial = [o for o in self.ir.objects if o.class_name in {"BluetoothSerial", "GPSModule", "WiFiESPAT"}
431
+ or "softserial" in HARDWARE_CLASSES[o.class_name].resources]
432
+ if len(software_serial) > 1:
433
+ self._hardware_error("Uno SoftwareSerial supports only one active receiver; use only one BluetoothSerial/GPSModule/WiFiESPAT device per sketch.", software_serial[1].span)
434
+ for obj in self.ir.objects:
435
+ spec = HARDWARE_CLASSES[obj.class_name]
436
+ self._select_dependencies(spec.includes, spec.dependency_ids, spec.resources)
437
+ resolved: list[int | None] = []
438
+ for arg in obj.args:
439
+ value = self._single_int(arg, self._symbol_values)
440
+ if value is None and not self._is_static_ctor_arg(arg):
441
+ self._hardware_error(
442
+ f"{obj.class_name}() arguments must be compile-time values (numbers or constants).",
443
+ obj.span,
444
+ "Use a number or a variable that is assigned only once, e.g. trig = 7.",
445
+ )
446
+ resolved.append(value)
447
+ self._object_arg_values[obj.symbol.symbol_id] = tuple(resolved)
448
+ if obj.class_name in _packs.LOADED:
449
+ self._analyze_pack_object(obj, resolved, _packs.LOADED[obj.class_name])
450
+ continue
451
+ if obj.class_name == "DHT":
452
+ self._require_compile_time_pin(obj.args[0], self.board.digital_capable_pins, "DHT pin", obj.span)
453
+ self._record_shared_pin_use(frozenset(v for v in resolved[:1] if v is not None), obj.span)
454
+ elif obj.class_name == "Ultrasonic":
455
+ self._require_compile_time_pin(obj.args[0], self.board.digital_capable_pins, "Ultrasonic trigger pin", obj.span)
456
+ self._require_compile_time_pin(obj.args[1], self.board.digital_capable_pins, "Ultrasonic echo pin", obj.span)
457
+ self._record_shared_pin_use(frozenset(v for v in resolved[:2] if v is not None), obj.span)
458
+ elif obj.class_name in {"BluetoothSerial", "GPSModule", "WiFiESPAT"}:
459
+ pins = resolved
460
+ if any(p is None or p not in self.board.digital_capable_pins for p in pins) or len(set(pins)) != 2:
461
+ self._hardware_error(f"{obj.class_name} requires two distinct Uno digital pins: RX and TX.", obj.span)
462
+ if any(p in (0, 1) for p in pins if p is not None):
463
+ self._hardware_error(f"{obj.class_name} should not use Uno USB serial pins D0/D1. Use e.g. D6/D7.", obj.span)
464
+ self._record_shared_pin_use(frozenset(p for p in pins if p is not None), obj.span)
465
+ elif obj.class_name == "MPU6050Sensor":
466
+ self._resources.add("i2c")
467
+ if obj.span: self._i2c_sites.append(obj.span)
468
+ elif obj.class_name == "EthernetW5500":
469
+ cs = resolved[0]
470
+ if cs is None or cs not in self.board.digital_capable_pins:
471
+ self._hardware_error("EthernetW5500 requires a valid SPI chip-select digital pin.", obj.span)
472
+ if cs in (11,12,13):
473
+ self._hardware_error("EthernetW5500 CS cannot be D11/D12/D13; these are SPI data/clock pins.", obj.span)
474
+ for byte in resolved[1:]:
475
+ if byte is None or not 0 <= byte <= 255:
476
+ self._hardware_error("EthernetW5500 MAC bytes must be compile-time values 0..255.", obj.span)
477
+ if resolved[1] is not None and (resolved[1] & 1):
478
+ self._hardware_error("EthernetW5500 MAC must be unicast (first byte least significant bit = 0).", obj.span)
479
+ self._resources.add("spi")
480
+ if cs is not None: self._record_shared_pin_use(frozenset({cs}), obj.span)
481
+ elif obj.class_name == "RFID":
482
+ ss, rst = resolved
483
+ if ss is not None and ss != 10:
484
+ self._hardware_error("RFID uses SPI chip select D10 in the standard BARMAJA Uno wiring.", obj.span, "Use RFID(10, 9) unless a verified advanced circuit explicitly changes SS.")
485
+ for pin in (ss,rst):
486
+ if pin is None or pin not in self.board.digital_capable_pins:
487
+ self._hardware_error("RFID pins must be real Arduino Uno digital pins.", obj.span)
488
+ self._record_shared_pin_use(frozenset(p for p in (ss,rst) if p is not None), obj.span)
489
+ self._resources.add("spi")
490
+ elif obj.class_name == "IRRemote":
491
+ pin = resolved[0]
492
+ if pin is None or pin not in self.board.digital_capable_pins:
493
+ self._hardware_error("IRRemote receiver pin must be a real Arduino Uno digital pin.", obj.span)
494
+ if pin is not None: self._record_shared_pin_use(frozenset({pin}), obj.span)
495
+ elif obj.class_name == "Keypad4x4":
496
+ pins = resolved
497
+ if any(p is None or p not in self.board.digital_capable_pins for p in pins) or len(set(pins)) != 8:
498
+ self._hardware_error("Keypad4x4 requires eight distinct Arduino Uno digital-capable pins.", obj.span)
499
+ self._record_shared_pin_use(frozenset(p for p in pins if p is not None), obj.span)
500
+ elif obj.class_name == "StepperMotor":
501
+ pins = resolved
502
+ if any(p is None or p not in self.board.digital_capable_pins for p in pins) or len(set(pins)) != 4:
503
+ self._hardware_error("StepperMotor requires four distinct Arduino Uno digital-capable pins.", obj.span)
504
+ self._record_shared_pin_use(frozenset(p for p in pins if p is not None), obj.span)
505
+ elif obj.class_name in {"LEDMatrixMAX7219", "FourDigitTM1637", "OneWireBus"}:
506
+ pins = resolved
507
+ if any(p is None or p not in self.board.digital_capable_pins for p in pins) or len(set(pins)) != len(pins):
508
+ self._hardware_error(f"{obj.class_name} requires distinct compile-time Uno digital pins.", obj.span)
509
+ self._record_shared_pin_use(frozenset(p for p in pins if p is not None), obj.span)
510
+ elif obj.class_name == "LiquidCrystal_I2C":
511
+ self._resources.add("i2c")
512
+ if obj.span:
513
+ self._i2c_sites.append(obj.span)
514
+ address = self._single_int(obj.args[0], {})
515
+ cols = self._single_int(obj.args[1], {})
516
+ rows = self._single_int(obj.args[2], {})
517
+ if address is None or not (0 <= address <= 0x7F):
518
+ self._hardware_error("LiquidCrystal_I2C address must be a compile-time value in 0..127.", obj.span)
519
+ if cols is None or rows is None or not (1 <= cols <= 255) or not (1 <= rows <= 255):
520
+ self._hardware_error("LiquidCrystal_I2C columns and rows must be compile-time values in 1..255.", obj.span)
521
+
522
+ def _rebinds_list(self, body, symbol_id, visiting: set) -> bool:
523
+ """True if `body` (or a user function it calls) stores a whole new list to symbol_id."""
524
+ from dataclasses import fields, is_dataclass
525
+
526
+ functions = {fn.symbol.symbol_id: fn for fn in self.ir.functions}
527
+
528
+ def walk(value) -> bool:
529
+ if isinstance(value, IRStore) and value.target.symbol_id == symbol_id:
530
+ return True
531
+ if isinstance(value, IRCall) and value.kind == IRCallKind.USER_FUNCTION and value.function_symbol is not None:
532
+ fid = value.function_symbol.symbol_id
533
+ if fid not in visiting and fid in functions:
534
+ visiting.add(fid)
535
+ if walk(functions[fid].body):
536
+ return True
537
+ if is_dataclass(value) and not isinstance(value, type):
538
+ return any(walk(getattr(value, f.name)) for f in fields(value))
539
+ if isinstance(value, (tuple, list)):
540
+ return any(walk(item) for item in value)
541
+ return False
542
+
543
+ return walk(body)
544
+
545
+ def _record_shared_pin_use(self, values, span: SourceSpan | None) -> None:
546
+ """Device pins on UART 0/1 or I2C A4/A5 feed the same conflict warnings as digital I/O."""
547
+ if not values or span is None:
548
+ return
549
+ if set(values) & set(self.board.uart_pins):
550
+ self._serial_pin_sites.append(span)
551
+ if set(values) & set(self.board.i2c_pins):
552
+ self._i2c_pin_sites.append(span)
553
+
554
+ @staticmethod
555
+ def _is_static_ctor_arg(arg: IRExpr) -> bool:
556
+ from .symbols import SymbolKind
557
+ if isinstance(arg, IRConstant):
558
+ return True
559
+ return isinstance(arg, IRLoad) and arg.symbol.kind in {SymbolKind.API, SymbolKind.BUILTIN}
560
+
561
+ def _select_dependencies(self, includes: Iterable[str], dependency_ids: Iterable[str], resources: Iterable[str]) -> None:
562
+ self._includes.update(includes)
563
+ self._resources.update(resources)
564
+ pending = list(dependency_ids)
565
+ while pending:
566
+ dep_id = pending.pop()
567
+ if dep_id in self._dependencies:
568
+ continue
569
+ self._dependencies.add(dep_id)
570
+ dep = DEPENDENCIES[dep_id]
571
+ if dep.include:
572
+ self._includes.add(dep.include)
573
+ pending.extend(dep.transitive)
574
+
575
+ def _collect_dependencies_from_all_ir(self) -> None:
576
+ """Collect headers/dependencies from emitted IR, including unused functions.
577
+
578
+ Backend emission includes function bodies even when they are never called,
579
+ so singleton/library references there still require their headers.
580
+ """
581
+ def visit_expr(expr: IRExpr) -> None:
582
+ if isinstance(expr, IRCall):
583
+ if expr.kind == IRCallKind.API_FUNCTION:
584
+ spec = CORE_FUNCTIONS.get(expr.name)
585
+ if spec:
586
+ self._select_dependencies((spec.include,) if spec.include else (), (spec.dependency_id,) if spec.dependency_id else (), spec.resources)
587
+ elif expr.kind == IRCallKind.SINGLETON_METHOD and expr.owner_name in SINGLETONS:
588
+ spec = SINGLETONS[expr.owner_name]
589
+ self._select_dependencies(spec.includes, spec.dependency_ids, spec.resources)
590
+ elif expr.kind == IRCallKind.OBJECT_METHOD and expr.owner_symbol is not None:
591
+ cls = self._object_classes.get(expr.owner_symbol.symbol_id) or expr.owner_symbol.type.hardware_class
592
+ if cls in HARDWARE_CLASSES:
593
+ spec = HARDWARE_CLASSES[cls]
594
+ self._select_dependencies(spec.includes, spec.dependency_ids, spec.resources)
595
+ for a in expr.args: visit_expr(a)
596
+ elif isinstance(expr, IRListLiteral):
597
+ for x in expr.elements: visit_expr(x)
598
+ elif isinstance(expr, IRIndex):
599
+ visit_expr(expr.collection); visit_expr(expr.index)
600
+ elif isinstance(expr, IRUnary): visit_expr(expr.operand)
601
+ elif isinstance(expr, IRBinary): visit_expr(expr.left); visit_expr(expr.right)
602
+ elif isinstance(expr, IRCompareChain):
603
+ for x in expr.operands: visit_expr(x)
604
+ elif isinstance(expr, IRBool):
605
+ for x in expr.values: visit_expr(x)
606
+ elif isinstance(expr, IRConditional):
607
+ visit_expr(expr.condition); visit_expr(expr.when_true); visit_expr(expr.when_false)
608
+ elif isinstance(expr, IRFormatString):
609
+ for part in expr.parts:
610
+ if not isinstance(part, str): visit_expr(part.value)
611
+
612
+ def visit_stmts(stmts: tuple[IRStmt, ...]) -> None:
613
+ for stmt in stmts:
614
+ if isinstance(stmt, IRExprStmt): visit_expr(stmt.expression)
615
+ elif isinstance(stmt, IRPrint):
616
+ self._resources.add("uart")
617
+ for x in stmt.values: visit_expr(x)
618
+ elif isinstance(stmt, IRStore): visit_expr(stmt.value)
619
+ elif isinstance(stmt, IRIndexStore): visit_expr(stmt.index); visit_expr(stmt.value)
620
+ elif isinstance(stmt, IRAugStore): visit_expr(stmt.value)
621
+ elif isinstance(stmt, IRIf): visit_expr(stmt.condition); visit_stmts(stmt.body); visit_stmts(stmt.otherwise)
622
+ elif isinstance(stmt, IRWhile): visit_expr(stmt.condition); visit_stmts(stmt.body)
623
+ elif isinstance(stmt, IRRangeLoop): visit_expr(stmt.start); visit_expr(stmt.stop); visit_expr(stmt.step); visit_stmts(stmt.body)
624
+ elif isinstance(stmt, IRFixedListLoop):
625
+ if stmt.index_start is not None: visit_expr(stmt.index_start)
626
+ visit_stmts(stmt.body)
627
+ elif isinstance(stmt, IRReturn) and stmt.value is not None: visit_expr(stmt.value)
628
+ visit_stmts(self.ir.setup)
629
+ visit_stmts(self.ir.main_loop)
630
+ for fn in self.ir.functions:
631
+ visit_stmts(fn.body)
632
+
633
+ def _validate_known_constraints_in_all_functions(self) -> None:
634
+ """Catch impossible literal pin use even in currently unreachable functions.
635
+
636
+ Unknown parameters are intentionally left for call-site proof so useful
637
+ helpers like ``def set_led(pin): ...`` remain supported.
638
+ """
639
+ def visit_expr(expr: IRExpr) -> None:
640
+ if isinstance(expr, IRCall):
641
+ if expr.kind == IRCallKind.API_FUNCTION and expr.args:
642
+ values = self._value_set(expr.args[0], {})
643
+ if values is not None:
644
+ if expr.name in {"pinMode", "digitalWrite", "digitalRead", "tone", "noTone"}:
645
+ bad = set(values) - set(self.board.digital_capable_pins)
646
+ if bad: self._hardware_error(f"{expr.name}() contains invalid {self.board.name} pin value(s): {', '.join(map(str, sorted(bad)))}.", expr.span)
647
+ elif expr.name == "analogRead":
648
+ is_alias = isinstance(expr.args[0], IRLoad) and expr.args[0].symbol.source_name in self.board.analog_aliases
649
+ if not is_alias and not set(values).issubset(set(self.board.analog_channels)):
650
+ self._hardware_error(f"analogRead() pin must be A0..A5 or compatibility channel 0..5 on {self.board.name}.", expr.span)
651
+ elif expr.name == "analogWrite" and not set(values).issubset(set(self.board.pwm_pins)):
652
+ self._hardware_error(f"analogWrite() uses a non-PWM pin on {self.board.name}.", expr.span)
653
+ elif expr.kind == IRCallKind.OBJECT_METHOD and expr.method_name == "attach" and expr.owner_symbol and expr.owner_symbol.type.hardware_class == "Servo":
654
+ values = self._value_set(expr.args[0], {}) if expr.args else None
655
+ if values is not None and not set(values).issubset(set(self.board.digital_capable_pins)):
656
+ self._hardware_error(f"Servo attach pin is invalid on {self.board.name}.", expr.span)
657
+ for a in expr.args: visit_expr(a)
658
+ elif isinstance(expr, IRUnary): visit_expr(expr.operand)
659
+ elif isinstance(expr, IRBinary): visit_expr(expr.left); visit_expr(expr.right)
660
+ elif isinstance(expr, IRCompareChain):
661
+ for x in expr.operands: visit_expr(x)
662
+ elif isinstance(expr, IRBool):
663
+ for x in expr.values: visit_expr(x)
664
+ elif isinstance(expr, IRConditional): visit_expr(expr.condition); visit_expr(expr.when_true); visit_expr(expr.when_false)
665
+ elif isinstance(expr, IRIndex): visit_expr(expr.collection); visit_expr(expr.index)
666
+ elif isinstance(expr, IRListLiteral):
667
+ for x in expr.elements: visit_expr(x)
668
+ elif isinstance(expr, IRFormatString):
669
+ for part in expr.parts:
670
+ if not isinstance(part, str): visit_expr(part.value)
671
+ def visit_stmts(stmts: tuple[IRStmt, ...]) -> None:
672
+ for stmt in stmts:
673
+ if isinstance(stmt, IRExprStmt): visit_expr(stmt.expression)
674
+ elif isinstance(stmt, IRPrint):
675
+ for x in stmt.values: visit_expr(x)
676
+ elif isinstance(stmt, IRStore): visit_expr(stmt.value)
677
+ elif isinstance(stmt, IRIndexStore): visit_expr(stmt.index); visit_expr(stmt.value)
678
+ elif isinstance(stmt, IRAugStore): visit_expr(stmt.value)
679
+ elif isinstance(stmt, IRIf): visit_expr(stmt.condition); visit_stmts(stmt.body); visit_stmts(stmt.otherwise)
680
+ elif isinstance(stmt, IRWhile): visit_expr(stmt.condition); visit_stmts(stmt.body)
681
+ elif isinstance(stmt, (IRRangeLoop, IRFixedListLoop)): visit_stmts(stmt.body)
682
+ elif isinstance(stmt, IRReturn) and stmt.value is not None: visit_expr(stmt.value)
683
+ for fn in self.ir.functions:
684
+ visit_stmts(fn.body)
685
+
686
+ # ---------- sequence/lifecycle analysis ----------
687
+ def _walk_sequence(self, statements: tuple[IRStmt, ...], life: _LifeState, env: dict, call_stack: tuple) -> tuple[_LifeState, dict]:
688
+ for stmt in statements:
689
+ if isinstance(stmt, IRStore):
690
+ self._walk_expr(stmt.value, life, env, call_stack)
691
+ values = self._value_set(stmt.value, env)
692
+ if stmt.target.type.kind == TypeKind.TIME_U32 and stmt.value.type.kind == TypeKind.INT and values and max(values) < 0:
693
+ self._hardware_error(
694
+ f"'{stmt.target.source_name}' holds a millis() time, so it cannot be negative.",
695
+ stmt.span,
696
+ "Start timer variables at 0 or at millis().",
697
+ )
698
+ if values is not None and len(values) <= 64:
699
+ env[stmt.target.symbol_id] = values
700
+ else:
701
+ env.pop(stmt.target.symbol_id, None)
702
+ elif isinstance(stmt, IRIndexStore):
703
+ self._walk_expr(stmt.index, life, env, call_stack)
704
+ self._walk_expr(stmt.value, life, env, call_stack)
705
+ elif isinstance(stmt, IRAugStore):
706
+ self._walk_expr(stmt.value, life, env, call_stack)
707
+ env.pop(stmt.target.symbol_id, None)
708
+ elif isinstance(stmt, IRExprStmt):
709
+ self._walk_expr(stmt.expression, life, env, call_stack)
710
+ elif isinstance(stmt, IRPrint):
711
+ self._resources.add("uart")
712
+ if stmt.span:
713
+ self._serial_sites.append(stmt.span)
714
+ if not life.serial_begun:
715
+ self._hardware_error(
716
+ "Python print() executes before Serial is initialized.",
717
+ stmt.span,
718
+ "Move the explicit Serial.begin(baud) before this output, or remove it and let Python print() use automatic 9600 baud.",
719
+ )
720
+ for expr in stmt.values:
721
+ self._walk_expr(expr, life, env, call_stack)
722
+ elif isinstance(stmt, IRIf):
723
+ self._walk_expr(stmt.condition, life, env, call_stack)
724
+ left_life, left_env = self._walk_sequence(stmt.body, life.copy(), dict(env), call_stack)
725
+ right_life, right_env = self._walk_sequence(stmt.otherwise, life.copy(), dict(env), call_stack)
726
+ life = _LifeState.intersect(left_life, right_life)
727
+ env = self._intersect_env(left_env, right_env)
728
+ elif isinstance(stmt, IRWhile):
729
+ self._walk_expr(stmt.condition, life, env, call_stack)
730
+ self._walk_sequence(stmt.body, life.copy(), dict(env), call_stack)
731
+ elif isinstance(stmt, IRRangeLoop):
732
+ self._walk_expr(stmt.start, life, env, call_stack)
733
+ self._walk_expr(stmt.stop, life, env, call_stack)
734
+ self._walk_expr(stmt.step, life, env, call_stack)
735
+ # A step that is a compile-time constant expression equal to zero
736
+ # (e.g. STEP = 0 or N - N) is a program error in Python (ValueError);
737
+ # report it before code generation instead of relying on the runtime trap.
738
+ if self._single_int(stmt.step, {}) == 0:
739
+ where = stmt.step.span or stmt.span
740
+ raise CompileError(
741
+ "range() step cannot be zero.",
742
+ where.line if where else None,
743
+ (where.column + 1) if where else None,
744
+ end_line=where.end_line if where else None,
745
+ end_column=where.end_column if where else None,
746
+ )
747
+ loop_env = dict(env)
748
+ values = self._range_values(stmt.start, stmt.stop, stmt.step, env)
749
+ if values is not None:
750
+ loop_env[stmt.target.symbol_id] = values
751
+ self._walk_sequence(stmt.body, life.copy(), loop_env, call_stack)
752
+ elif isinstance(stmt, IRFixedListLoop):
753
+ if self._rebinds_list(stmt.body, stmt.collection.symbol_id, set()):
754
+ # Python keeps iterating the ORIGINAL list object when the name is
755
+ # rebound to a new list; a fixed Uno array is overwritten in place.
756
+ where = stmt.span
757
+ raise UnsupportedFeatureError(
758
+ f"Do not assign a new list to '{stmt.collection.source_name}' while a for-loop is iterating over it "
759
+ "(Python would keep looping over the old list). Change single elements, for example values[i] = x, "
760
+ "or store the new list in another variable.",
761
+ line=where.line if where else None,
762
+ column=(where.column + 1) if where else None,
763
+ )
764
+ loop_env = dict(env)
765
+ values = self._list_values.get(stmt.collection.symbol_id)
766
+ if values:
767
+ loop_env[stmt.value_target.symbol_id] = values
768
+ if stmt.index_start is not None:
769
+ self._walk_expr(stmt.index_start, life, env, call_stack)
770
+ self._walk_sequence(stmt.body, life.copy(), loop_env, call_stack)
771
+ elif isinstance(stmt, IRReturn):
772
+ if stmt.value is not None:
773
+ self._walk_expr(stmt.value, life, env, call_stack)
774
+ # break/continue carry no target facts.
775
+ return life, env
776
+
777
+ def _walk_expr(self, expr: IRExpr, life: _LifeState, env: dict, call_stack: tuple) -> None:
778
+ if isinstance(expr, (IRConstant, IRLoad)):
779
+ return
780
+ if isinstance(expr, IRListLiteral):
781
+ for x in expr.elements: self._walk_expr(x, life, env, call_stack)
782
+ return
783
+ if isinstance(expr, IRIndex):
784
+ self._walk_expr(expr.collection, life, env, call_stack); self._walk_expr(expr.index, life, env, call_stack); return
785
+ if isinstance(expr, IRUnary):
786
+ self._walk_expr(expr.operand, life, env, call_stack); return
787
+ if isinstance(expr, IRBinary):
788
+ self._walk_expr(expr.left, life, env, call_stack); self._walk_expr(expr.right, life, env, call_stack); return
789
+ if isinstance(expr, IRCompareChain):
790
+ for x in expr.operands: self._walk_expr(x, life, env, call_stack)
791
+ return
792
+ if isinstance(expr, IRBool):
793
+ for x in expr.values: self._walk_expr(x, life, env, call_stack)
794
+ return
795
+ if isinstance(expr, IRConditional):
796
+ self._walk_expr(expr.condition, life, env, call_stack)
797
+ self._walk_expr(expr.when_true, life.copy(), dict(env), call_stack)
798
+ self._walk_expr(expr.when_false, life.copy(), dict(env), call_stack)
799
+ return
800
+ if isinstance(expr, IRFormatString):
801
+ for part in expr.parts:
802
+ if not isinstance(part, str): self._walk_expr(part.value, life, env, call_stack)
803
+ return
804
+ if isinstance(expr, IRCall):
805
+ for arg in expr.args:
806
+ self._walk_expr(arg, life, env, call_stack)
807
+ self._handle_call(expr, life, env, call_stack)
808
+ return
809
+ raise InternalCompilerError(f"Target analysis has no expression path for {type(expr).__name__}.")
810
+
811
+ def _handle_call(self, call: IRCall, life: _LifeState, env: dict, call_stack: tuple) -> None:
812
+ if call.kind == IRCallKind.USER_FUNCTION:
813
+ if call.function_symbol is None:
814
+ raise InternalCompilerError("User call reached target analysis without a function symbol.")
815
+ sid = call.function_symbol.symbol_id
816
+ if sid in call_stack:
817
+ return # recursion is rejected earlier; defensive only.
818
+ fn = self._functions.get(sid)
819
+ if fn is None:
820
+ raise InternalCompilerError("User call reached target analysis without function IR.")
821
+ fn_env = dict(env)
822
+ for param, arg in zip(fn.parameters, call.args):
823
+ values = self._value_set(arg, env)
824
+ if values is not None and len(values) <= 64:
825
+ fn_env[param.symbol_id] = values
826
+ self._walk_sequence(fn.body, life, fn_env, call_stack + (sid,))
827
+ return
828
+
829
+ if call.kind == IRCallKind.API_FUNCTION:
830
+ spec = CORE_FUNCTIONS.get(call.name)
831
+ if spec:
832
+ self._select_dependencies((spec.include,) if spec.include else (), (spec.dependency_id,) if spec.dependency_id else (), spec.resources)
833
+ self._validate_core_call(call, env)
834
+ return
835
+
836
+ if call.kind == IRCallKind.SINGLETON_METHOD:
837
+ owner = call.owner_name or ""
838
+ spec = SINGLETONS[owner]
839
+ self._select_dependencies(spec.includes, spec.dependency_ids, spec.resources)
840
+ method = call.method_name or ""
841
+ if owner == "Wire":
842
+ if call.span: self._i2c_sites.append(call.span)
843
+ if method == "begin":
844
+ life.wire_begun = True; self._lifecycle_facts.add("Wire begun")
845
+ elif not life.wire_begun:
846
+ self._hardware_error(f"Wire.{method}() requires Wire.begin() first.", call.span, "Call Wire.begin() during setup before I2C transactions.")
847
+ if method == "write" and call.args:
848
+ self._require_values_within(call.args[0], env, 0, 255, "Wire.write() sends one byte: use a value from 0 to 255.", call.span)
849
+ if method in {"beginTransmission", "requestFrom"} and call.args:
850
+ self._require_values_within(call.args[0], env, 0, 127, f"Wire.{method}() address must be 0..127.", call.span)
851
+ if method == "requestFrom" and len(call.args) >= 2:
852
+ self._require_values_within(
853
+ call.args[1], env, 0, 255,
854
+ "Wire.requestFrom() count must be in 0..255 for the pinned AVR Wire implementation.", call.span,
855
+ )
856
+ elif owner == "EEPROM":
857
+ if method in {"read", "write", "update"} and call.args:
858
+ self._require_values_within(call.args[0], env, 0, 1023, "Uno EEPROM address must be 0..1023.", call.span)
859
+ if method in {"write", "update"} and len(call.args) > 1:
860
+ self._require_values_within(call.args[1], env, 0, 255, "EEPROM stores one byte, value must be 0..255.", call.span)
861
+ elif owner == "SPI":
862
+ if method == "transfer" and call.args:
863
+ self._require_values_within(call.args[0], env, 0, 255, "SPI.transfer() requires a byte 0..255.", call.span)
864
+ elif owner == "Serial":
865
+ if call.span: self._serial_sites.append(call.span)
866
+ self._resources.add("uart")
867
+ if method == "begin":
868
+ baud = self._single_int(call.args[0], env) if call.args else None
869
+ if baud is not None:
870
+ self._explicit_serial_bauds.add(baud)
871
+ life.serial_begun = True; self._lifecycle_facts.add("Serial begun explicitly")
872
+ elif not life.serial_begun:
873
+ self._hardware_error(
874
+ f"Serial.{method}() requires Serial.begin(baud) first.",
875
+ call.span,
876
+ "Add one setup-time Serial.begin(baud), or use Python print() when automatic 9600-baud initialization is desired.",
877
+ )
878
+ if method in {"print", "println"} and len(call.args) == 2:
879
+ self._require_values_within(
880
+ call.args[1], env, 0, 255,
881
+ f"Serial.{method}() format/base/digits argument must fit the pinned Arduino Print uint8 domain 0..255.",
882
+ call.span,
883
+ )
884
+ return
885
+
886
+ if call.kind == IRCallKind.OBJECT_METHOD:
887
+ if call.owner_symbol is None:
888
+ raise InternalCompilerError("Object method reached target analysis without owner symbol.")
889
+ sid = call.owner_symbol.symbol_id
890
+ cls = self._object_classes.get(sid) or call.owner_symbol.type.hardware_class
891
+ if cls not in HARDWARE_CLASSES:
892
+ raise InternalCompilerError("Unknown hardware object reached target analysis.")
893
+ spec = HARDWARE_CLASSES[cls]
894
+ self._select_dependencies(spec.includes, spec.dependency_ids, spec.resources)
895
+ method = call.method_name or ""
896
+ if cls in _packs.LOADED:
897
+ self._analyze_pack_method(call, env, life, sid, _packs.LOADED[cls], method)
898
+ return
899
+ if cls in {"RFID", "IRRemote", "BluetoothSerial", "MPU6050Sensor", "GPSModule", "EthernetW5500", "WiFiESPAT"}:
900
+ if method in ({"begin", "beginIP"} if cls == "EthernetW5500" else {"begin"}):
901
+ life.modules_started.add(sid)
902
+ self._lifecycle_facts.add(f"{cls} initialized")
903
+ elif sid not in life.modules_started:
904
+ self._hardware_error(f"{cls}.{method}() requires begin() first.", call.span, f"Call the {cls.lower()} object begin() during setup.")
905
+ if cls in {"BluetoothSerial", "WiFiESPAT", "GPSModule"} and method == "begin" and call.args:
906
+ self._require_values_within(call.args[0], env, 300, 57600, f"{cls}.begin() baud must be 300..57600 for reliable Uno SoftwareSerial.", call.span)
907
+ if cls in {"BluetoothSerial", "EthernetW5500", "WiFiESPAT"} and method == "write" and call.args:
908
+ self._require_values_within(call.args[0], env, 0, 255, f"{cls}.write() requires a byte 0..255.", call.span)
909
+ if cls in {"EthernetW5500", "WiFiESPAT"}:
910
+ if method in {"beginIP", "tcpConnect"} or (cls == "EthernetW5500" and method == "connect"):
911
+ for ix in range(4):
912
+ self._require_values_within(call.args[ix], env, 0, 255, f"{cls} IPv4 octets must be 0..255.", call.span)
913
+ if method == "tcpConnect" or (cls == "EthernetW5500" and method == "connect"):
914
+ self._require_values_within(call.args[4], env, 1, 65535, f"{cls} TCP port must be 1..65535.", call.span)
915
+ if cls == "OneWireBus" and method == "write" and call.args:
916
+ self._require_values_within(call.args[0], env, 0, 255, "OneWireBus.write() requires 0..255.", call.span)
917
+ if cls == "LEDMatrixMAX7219":
918
+ if method == "setPixel":
919
+ for ix in (0,1): self._require_values_within(call.args[ix], env, 0, 7, "LED matrix row/column must be 0..7.", call.span)
920
+ if method == "setRow":
921
+ self._require_values_within(call.args[0], env, 0, 7, "LED matrix row must be 0..7.", call.span)
922
+ self._require_values_within(call.args[1], env, 0, 255, "LED matrix row bits must be 0..255.", call.span)
923
+ if method == "setBrightness": self._require_values_within(call.args[0], env, 0, 15, "MAX7219 brightness must be 0..15.", call.span)
924
+ if cls == "FourDigitTM1637":
925
+ if method == "setBrightness": self._require_values_within(call.args[0], env, 0, 7, "TM1637 brightness must be 0..7.", call.span)
926
+ if method == "showNumber": self._require_values_within(call.args[0], env, -999, 9999, "TM1637 number must be -999..9999.", call.span)
927
+ if cls == "Servo":
928
+ if method == "attach":
929
+ self._require_compile_time_pin(call.args[0], self.board.digital_capable_pins, "Servo attach pin", call.span, env)
930
+ self._record_shared_pin_use(self._value_set(call.args[0], env), call.span)
931
+ life.servo_attached.add(sid)
932
+ self._servo_attached_objects.add(sid)
933
+ self._resources.add(self.board.servo_timer.lower())
934
+ if call.span: self._servo_attach_sites.append(call.span)
935
+ self._lifecycle_facts.add("Servo attached")
936
+ elif method == "detach":
937
+ life.servo_attached.discard(sid)
938
+ elif method in {"write", "writeMicroseconds", "read"} and sid not in life.servo_attached:
939
+ self._hardware_error(f"Servo.{method}() requires attach() first.", call.span, "Call servo.attach(pin) before using the Servo.")
940
+ if method == "write" and call.args:
941
+ self._require_values_within(call.args[0], env, 0, 180, "Servo.write() angle must be between 0 and 180.", call.span)
942
+ if method == "writeMicroseconds" and call.args:
943
+ self._require_values_within(call.args[0], env, 0, 3000, "Servo.writeMicroseconds() value must be a pulse width in 0..3000 microseconds.", call.span)
944
+ if method == "attach" and len(call.args) == 3:
945
+ lo = self._single_int(call.args[1], env); hi = self._single_int(call.args[2], env)
946
+ if lo is not None and hi is not None and not self._servo_attach_bounds_valid(lo, hi):
947
+ self._hardware_error(
948
+ "Servo.attach(pin, min, max) pulse bounds are unsafe for the pinned Servo adapter.",
949
+ call.span,
950
+ "Use 0 < min < max and calibration values close enough to the default 544..2400 us range to fit the library's int8 offset storage.",
951
+ )
952
+ elif cls == "Ultrasonic":
953
+ if method == "setTimeout" and call.args:
954
+ self._require_not_all_negative(call.args[0], env, "Ultrasonic.setTimeout() needs a non-negative timeout.", call.span)
955
+ elif cls == "DHT":
956
+ if method == "begin":
957
+ life.dht_begun.add(sid); self._lifecycle_facts.add("DHT begun")
958
+ elif method in {"readTemperature", "readHumidity"} and sid not in life.dht_begun:
959
+ self._hardware_error(f"DHT.{method}() requires begin() first.", call.span, "Call dht.begin() during setup before reading the sensor.")
960
+ elif cls == "LiquidCrystal_I2C":
961
+ self._resources.add("i2c")
962
+ if call.span: self._i2c_sites.append(call.span)
963
+ if method in {"init", "begin"}:
964
+ life.lcd_inited.add(sid); self._lifecycle_facts.add("LCD initialized")
965
+ if method == "init":
966
+ # Pinned LiquidCrystal_I2C 1.1.4 init() calls Wire.begin()
967
+ # inside init_priv(), so subsequent Wire operations are valid.
968
+ life.wire_begun = True
969
+ self._lifecycle_facts.add("Wire begun by LiquidCrystal_I2C.init()")
970
+ else:
971
+ # Pinned begin() does not call Wire.begin(); the backend adds
972
+ # one setup hook for this Barmaja compatibility path.
973
+ self._lcd_begin_needs_wire = True
974
+ life.wire_begun = True
975
+ elif method in {"clear", "home", "setCursor", "print", "backlight", "noBacklight", "display", "noDisplay"} and sid not in life.lcd_inited:
976
+ self._hardware_error(f"LiquidCrystal_I2C.{method}() requires init() or begin() first.", call.span, "Initialize the LCD during setup before using it.")
977
+ if method == "begin" and len(call.args) >= 2:
978
+ self._require_values_within(
979
+ call.args[0], env, 1, 255,
980
+ "LiquidCrystal_I2C.begin() columns and rows must be in 1..255.", call.span,
981
+ )
982
+ self._require_values_within(
983
+ call.args[1], env, 1, 255,
984
+ "LiquidCrystal_I2C.begin() columns and rows must be in 1..255.", call.span,
985
+ )
986
+ if len(call.args) >= 3:
987
+ values = self._value_set(call.args[2], env)
988
+ if values is not None and not set(values).issubset({0x00, 0x04}):
989
+ self._hardware_error(
990
+ "LiquidCrystal_I2C.begin() charsize must be 0 (5x8) or 4 (5x10).",
991
+ call.span,
992
+ )
993
+ if method == "setCursor" and len(call.args) >= 2:
994
+ self._require_values_within(
995
+ call.args[0], env, 0, 255,
996
+ "LiquidCrystal_I2C.setCursor() column must be in 0..255.", call.span,
997
+ )
998
+ self._require_values_within(
999
+ call.args[1], env, 0, 3,
1000
+ "LiquidCrystal_I2C.setCursor() row must be 0, 1, 2, or 3 (HD44780 displays have at most 4 rows).", call.span,
1001
+ )
1002
+ if method == "print" and call.args:
1003
+ self._validate_lcd_output_literals(call.args[0], call.span)
1004
+ return
1005
+
1006
+ # ---------- device packs (generic rules from pack.json) ----------
1007
+ def _analyze_pack_object(self, obj, resolved: list[int | None], pack) -> None:
1008
+ board = self.board
1009
+ allowed = {
1010
+ "digital_pin": set(board.digital_capable_pins),
1011
+ "analog_pin": set(board.analog_pins),
1012
+ "pwm_pin": set(board.pwm_pins),
1013
+ }
1014
+ resolved = list(resolved) + [p.default for p in pack.constructor[len(resolved):]]
1015
+ names = {p.name: v for p, v in zip(pack.constructor, resolved)}
1016
+ pins: list[int] = []
1017
+ for param, value in zip(pack.constructor, resolved):
1018
+ if param.rule != "none":
1019
+ if value is None or value not in allowed[param.rule]:
1020
+ kind = {"digital_pin": "digital", "analog_pin": "analog (A0..A5)", "pwm_pin": "PWM (~)"}[param.rule]
1021
+ self._hardware_error(f"{pack.class_name}() '{param.name}' must be a compile-time {kind} pin on {board.name}.", obj.span)
1022
+ if value in pack.forbid_pins:
1023
+ self._hardware_error(
1024
+ f"{pack.class_name}() '{param.name}' cannot use pin {value} on {board.name}.", obj.span,
1025
+ "D0/D1 are the USB serial pins used for upload and print(); choose another pin." if value in (0, 1) else None)
1026
+ pins.append(value)
1027
+ if param.range is not None:
1028
+ low = _packs.resolve_bound(param.range[0], names)
1029
+ high = _packs.resolve_bound(param.range[1], names)
1030
+ if value is None or (low is not None and value < low) or (high is not None and value > high):
1031
+ self._hardware_error(f"{pack.class_name}() '{param.name}' must be a compile-time number {low}..{high}.", obj.span)
1032
+ if pack.distinct_pins and len(set(pins)) != len(pins):
1033
+ self._hardware_error(f"{pack.class_name}() pins must all be different.", obj.span)
1034
+ if "spi" in pack.resources:
1035
+ if set(pins) & {11, 12, 13}:
1036
+ self._hardware_error(f"{pack.class_name}() cannot use D11/D12/D13; they are the SPI bus pins.", obj.span)
1037
+ self._record_shared_pin_use(frozenset(set(pins) | {11, 12, 13}), obj.span)
1038
+ else:
1039
+ self._record_shared_pin_use(frozenset(pins), obj.span)
1040
+ if "i2c" in pack.resources and obj.span:
1041
+ self._i2c_sites.append(obj.span)
1042
+ for text in pack.warnings:
1043
+ self._warn("HardwareWarning", "BJ3900", f"{pack.class_name}: {text}", obj.span)
1044
+ if pack.status == "prototype":
1045
+ self._warn("HardwareWarning", "BJ3901", f"{pack.class_name} is a PROTOTYPE device pack (compile-tested only, not hardware-verified).", obj.span)
1046
+
1047
+ def _analyze_pack_method(self, call, env: dict, life, sid, pack, method: str) -> None:
1048
+ spec = pack.methods[method]
1049
+ starter = next((m for m in pack.methods.values() if m.starts), None)
1050
+ if spec.starts:
1051
+ life.modules_started.add(sid)
1052
+ self._lifecycle_facts.add(f"{pack.class_name} initialized")
1053
+ elif starter is not None and spec.requires_begin and sid not in life.modules_started:
1054
+ self._hardware_error(
1055
+ f"{pack.class_name}.{method}() requires {starter.name}() first.", call.span,
1056
+ f"Call {starter.name}() on the {pack.class_name} object during setup, before the main loop.")
1057
+ if "i2c" in pack.resources and call.span:
1058
+ self._i2c_sites.append(call.span)
1059
+ given = list(dict(self._object_arg_values).get(sid, ()))
1060
+ given += [p.default for p in pack.constructor[len(given):]]
1061
+ ctor_values = dict(zip((p.name for p in pack.constructor), given))
1062
+ for param, arg in zip(spec.params, call.args):
1063
+ if param.rule != "none":
1064
+ allowed = {"digital_pin": self.board.digital_capable_pins, "analog_pin": self.board.analog_pins,
1065
+ "pwm_pin": self.board.pwm_pins}[param.rule]
1066
+ self._require_proven_pin(arg, allowed, f"{pack.class_name}.{method}() {param.name}", call.span, env)
1067
+ values = self._value_set(arg, env) or ()
1068
+ if set(values) & set(pack.forbid_pins):
1069
+ self._hardware_error(f"{pack.class_name}.{method}() {param.name} cannot use pin(s) {sorted(set(values) & set(pack.forbid_pins))}.", call.span)
1070
+ self._record_shared_pin_use(frozenset(values), call.span)
1071
+ if param.range is None or param.types != ("Int",):
1072
+ continue
1073
+ low = _packs.resolve_bound(param.range[0], ctor_values)
1074
+ high = _packs.resolve_bound(param.range[1], ctor_values)
1075
+ if low is None or high is None:
1076
+ continue
1077
+ self._require_values_within(arg, env, low, high,
1078
+ f"{pack.class_name}.{method}() '{param.name}' must be {low}..{high}.", call.span)
1079
+
1080
+ def _validate_lcd_output_literals(self, expr: IRExpr, span: SourceSpan | None) -> None:
1081
+ """Reject literal characters the pinned HD44780 adapter cannot promise."""
1082
+
1083
+ def check(text: str) -> None:
1084
+ bad = [ch for ch in text if not (0x20 <= ord(ch) <= 0x7E) and ch != "°"]
1085
+ if bad:
1086
+ sample = bad[0]
1087
+ self._hardware_error(
1088
+ f"LCD literal text contains unsupported character {sample!r}.",
1089
+ span,
1090
+ "Use printable ASCII (degree sign ° is also supported). General UTF-8/Arabic text is not supported by the V1 HD44780 adapter.",
1091
+ )
1092
+
1093
+ if isinstance(expr, IRConstant) and isinstance(expr.value, str):
1094
+ check(expr.value)
1095
+ elif isinstance(expr, IRFormatString):
1096
+ for part in expr.parts:
1097
+ if isinstance(part, str):
1098
+ check(part)
1099
+
1100
+ # ---------- pin/resource validation ----------
1101
+ def _validate_core_call(self, call: IRCall, env: dict) -> None:
1102
+ if not call.args:
1103
+ return
1104
+ if call.name == "delay":
1105
+ self._require_not_all_negative(call.args[0], env, "delay() needs a non-negative number of milliseconds.", call.span)
1106
+ if call.name == "delayMicroseconds":
1107
+ self._require_values_within(call.args[0], env, 0, 16383, "delayMicroseconds() accepts 0..16383 microseconds on Uno.", call.span)
1108
+ if call.name == "tone":
1109
+ self._require_values_within(call.args[1], env, 31, 65535, "tone() frequency must be 31..65535 Hz on Arduino Uno.", call.span)
1110
+ if len(call.args) == 3:
1111
+ self._require_not_all_negative(call.args[2], env, "tone() duration must be non-negative milliseconds.", call.span)
1112
+ if call.span: self._tone_sites.append(call.span)
1113
+ if call.name == "map":
1114
+ a, b = self._value_set(call.args[1], env), self._value_set(call.args[2], env)
1115
+ if a is not None and b is not None and len(a) == len(b) == 1 and next(iter(a)) == next(iter(b)):
1116
+ self._hardware_error("map() input minimum and maximum must differ.", call.span)
1117
+ if call.name == "constrain":
1118
+ a, b = self._value_set(call.args[1], env), self._value_set(call.args[2], env)
1119
+ if a is not None and b is not None and min(a) > max(b):
1120
+ self._hardware_error("constrain() lower bound must not exceed upper bound.", call.span)
1121
+ if call.name == "pinMode" and len(call.args) >= 2:
1122
+ modes = self._value_set(call.args[1], env)
1123
+ allowed = {CONSTANT_VALUES["INPUT"], CONSTANT_VALUES["OUTPUT"], CONSTANT_VALUES["INPUT_PULLUP"]}
1124
+ if modes is not None and not set(modes).issubset(allowed):
1125
+ self._hardware_error("pinMode() mode must be INPUT, OUTPUT, or INPUT_PULLUP.", call.span)
1126
+ if call.name == "digitalWrite" and len(call.args) >= 2:
1127
+ levels = self._value_set(call.args[1], env)
1128
+ allowed = {CONSTANT_VALUES["LOW"], CONSTANT_VALUES["HIGH"]}
1129
+ if levels is not None and not set(levels).issubset(allowed):
1130
+ self._hardware_error("digitalWrite() value must be LOW, HIGH, False, or True.", call.span)
1131
+ if call.name in {"pinMode", "digitalWrite", "digitalRead", "tone", "noTone"}:
1132
+ self._require_proven_pin(call.args[0], self.board.digital_capable_pins, f"{call.name}() pin", call.span, env)
1133
+ values = self._value_set(call.args[0], env)
1134
+ if values and set(values) & set(self.board.uart_pins):
1135
+ if call.span: self._serial_pin_sites.append(call.span)
1136
+ if values and set(values) & set(self.board.i2c_pins):
1137
+ if call.span: self._i2c_pin_sites.append(call.span)
1138
+ elif call.name == "analogRead":
1139
+ self._validate_analog_pin(call.args[0], call.span, env)
1140
+ values = self._value_set(call.args[0], env)
1141
+ # Alias A4/A5 count as independent I2C pin use; numeric channel 4/5
1142
+ # maps to the same physical pins too.
1143
+ if values:
1144
+ physical = {self.board.analog_aliases.get(f"A{v}", v) if v in self.board.analog_channels else v for v in values}
1145
+ if set(physical) & set(self.board.i2c_pins) and call.span:
1146
+ self._i2c_pin_sites.append(call.span)
1147
+ elif call.name == "analogWrite":
1148
+ values = self._value_set(call.args[0], env)
1149
+ if values is None:
1150
+ self._hardware_error(f"analogWrite() pin must be statically provable as a PWM pin on {self.board.name}.", call.span)
1151
+ bad = sorted(set(values) - set(self.board.pwm_pins))
1152
+ if bad:
1153
+ if len(values) == 1:
1154
+ self._hardware_error(
1155
+ f"{self.board.name} pin {bad[0]} does not support PWM. Use pin 3, 5, 6, 9, 10, or 11.", call.span
1156
+ )
1157
+ self._hardware_error(
1158
+ f"analogWrite() pin set contains a non-PWM {self.board.name} pin. Use only 3, 5, 6, 9, 10, or 11.", call.span
1159
+ )
1160
+ if set(values) & set(self.board.servo_pwm_conflict_pins):
1161
+ if call.span: self._pwm_timer1_sites.append(call.span)
1162
+ if set(values) & {3, 11}:
1163
+ if call.span: self._pwm_timer2_sites.append(call.span)
1164
+
1165
+ @staticmethod
1166
+ def _servo_attach_bounds_valid(min_us: int, max_us: int) -> bool:
1167
+ # Pinned Servo 1.3.0 stores calibration deltas in int8_t units of 4 us.
1168
+ # Its own source notes the required invariant: abs(delta) / 4 < 128.
1169
+ return (
1170
+ 0 < min_us < max_us
1171
+ and abs(544 - min_us) // 4 < 128
1172
+ and abs(2400 - max_us) // 4 < 128
1173
+ )
1174
+
1175
+ def _require_not_all_negative(self, expr: IRExpr, env: dict, message: str, span: SourceSpan | None) -> None:
1176
+ values = self._value_set(expr, env)
1177
+ if values and max(values) < 0:
1178
+ self._hardware_error(message, span)
1179
+
1180
+ def _require_values_within(self, expr: IRExpr, env: dict, low: int, high: int, message: str, span: SourceSpan | None) -> None:
1181
+ values = self._value_set(expr, env)
1182
+ if values and (min(values) < low or max(values) > high):
1183
+ self._hardware_error(message, span)
1184
+
1185
+ def _validate_analog_pin(self, expr: IRExpr, span: SourceSpan | None, env: dict) -> None:
1186
+ if isinstance(expr, IRLoad) and expr.symbol.source_name in self.board.analog_aliases:
1187
+ return
1188
+ values = self._value_set(expr, env)
1189
+ alias_pins = set(self.board.analog_aliases.values())
1190
+ channels = set(self.board.analog_channels)
1191
+ if values is None or not set(values).issubset(channels | alias_pins):
1192
+ self._hardware_error(f"analogRead() pin must be A0..A5 or compatibility channel 0..5 on {self.board.name}.", span)
1193
+ if set(values).issubset(alias_pins):
1194
+ # e.g. `ldr = A0` then `analogRead(ldr)`: the physical analog pin is explicit.
1195
+ return
1196
+ self._warn(
1197
+ "HardwareWarning", "BJ3104",
1198
+ f"analogRead() uses numeric channel 0..5; A0..A5 is clearer on {self.board.name}.", span,
1199
+ "Prefer A0, A1, ... A5 so the physical analog pin is explicit.",
1200
+ )
1201
+
1202
+ def _require_compile_time_pin(self, expr: IRExpr, allowed: Iterable[int], label: str, span: SourceSpan | None, env: dict | None = None) -> int:
1203
+ value = self._single_int(expr, env or self._symbol_values)
1204
+ if value is None:
1205
+ self._hardware_error(f"{label} must be a compile-time pin value on {self.board.name}.", span)
1206
+ if value not in set(allowed):
1207
+ self._hardware_error(f"{label} {self._display_pin(expr, value)} is not valid on {self.board.name}.", span)
1208
+ return value
1209
+
1210
+ def _require_proven_pin(self, expr: IRExpr, allowed: Iterable[int], label: str, span: SourceSpan | None, env: dict) -> None:
1211
+ values = self._value_set(expr, env)
1212
+ allowed_set = set(allowed)
1213
+ if values is None:
1214
+ self._hardware_error(f"{label} must be statically provable as a valid {self.board.name} pin.", span)
1215
+ bad = sorted(set(values) - allowed_set)
1216
+ if bad:
1217
+ rendered = ", ".join(str(x) for x in bad[:6])
1218
+ self._hardware_error(f"{label} contains invalid {self.board.name} pin value(s): {rendered}.", span)
1219
+
1220
+ # ---------- warnings/conflicts ----------
1221
+ def _emit_resource_conflict_warnings(self) -> None:
1222
+ if self._tone_sites and "ir_timer" in self._resources:
1223
+ self._warn(
1224
+ "HardwareWarning", "BJ3111",
1225
+ "tone() and IRRemote may interfere with Timer2/receive timing on Uno. Test separately or avoid combining them.",
1226
+ self._tone_sites[0], "Avoid tone() while reading IR; test the exact library timer settings.",
1227
+ )
1228
+ if self._tone_sites and self._pwm_timer2_sites:
1229
+ self._warn(
1230
+ "HardwareWarning", "BJ3110",
1231
+ "tone() uses Timer2 on Arduino Uno: analogWrite() PWM on D3/D11 may not work normally.",
1232
+ self._pwm_timer2_sites[0], "Use D5, D6, D9 or D10 for PWM while tone() is active.", tuple(self._tone_sites[:2]),
1233
+ )
1234
+ if self._servo_attach_sites and self._pwm_timer1_sites:
1235
+ self._warn(
1236
+ "HardwareWarning", "BJ3101",
1237
+ f"Servo on {self.board.name} uses Timer1; PWM on pins 9 and 10 will not behave normally.",
1238
+ self._pwm_timer1_sites[0], "Use PWM pin 3, 5, 6, or 11 while Servo is active.", tuple(self._servo_attach_sites[:2]),
1239
+ )
1240
+ if self._serial_sites and self._serial_pin_sites:
1241
+ self._warn(
1242
+ "HardwareWarning", "BJ3102",
1243
+ "Serial is active while pin 0 or 1 is also used as general digital I/O (RX/TX conflict).",
1244
+ self._serial_pin_sites[0], "Avoid pins 0 and 1 when using Serial/upload/monitor.", tuple(self._serial_sites[:2]),
1245
+ )
1246
+ if self._i2c_sites and self._i2c_pin_sites:
1247
+ self._warn(
1248
+ "HardwareWarning", "BJ3103",
1249
+ "I2C is active while A4 or A5 is also used independently (SDA/SCL conflict).",
1250
+ self._i2c_pin_sites[0], "Keep A4/A5 dedicated to I2C while Wire/LCD is active.", tuple(self._i2c_sites[:2]),
1251
+ )
1252
+
1253
+ def _validate_servo_capacity(self) -> None:
1254
+ # Pinned Servo 1.3.0 allocates the servoIndex in Servo::Servo(), not in
1255
+ # attach(). Therefore the hard limit applies to constructed Servo
1256
+ # objects even if some are never attached or are later detached.
1257
+ servo_objects = [obj for obj in self.ir.objects if obj.class_name == "Servo"]
1258
+ if len(servo_objects) > self.board.servo_max_channels:
1259
+ bad = servo_objects[self.board.servo_max_channels]
1260
+ self._hardware_error(
1261
+ f"{self.board.name} supports at most {self.board.servo_max_channels} Servo objects with the pinned Servo library.",
1262
+ bad.span,
1263
+ "Create no more than 12 Servo() objects on Arduino Uno; the library assigns the channel index in each constructor.",
1264
+ )
1265
+
1266
+ # ---------- memory ----------
1267
+ def _estimate_static_sram(self) -> int:
1268
+ total = 0
1269
+ for decl in self.ir.storages:
1270
+ if decl.storage not in {StorageClass.MODULE_CONSTANT, StorageClass.PERSISTENT_MODULE}:
1271
+ # setup()/loop() fixed lists are emitted with static storage by the
1272
+ # backend, so they occupy SRAM exactly like module data.
1273
+ if decl.symbol.type.kind == TypeKind.FIXED_LIST:
1274
+ total += self._type_size(decl.symbol.type)
1275
+ continue
1276
+ total += self._type_size(decl.symbol.type)
1277
+ if decl.symbol.type.kind == TypeKind.STRING and isinstance(decl.initializer, IRConstant) and isinstance(decl.initializer.value, str):
1278
+ total += len(decl.initializer.value.encode("utf-8")) + 1
1279
+ total += self._function_local_list_sram()
1280
+ # Object and library RAM measured with the pinned 0.6.x Uno toolchain
1281
+ # (arduino:avr 1.8.8 + pinned libraries), so the estimate tracks the
1282
+ # real "Global variables use ..." figure instead of only user data.
1283
+ object_sizes = {"Servo": 3, "Ultrasonic": 11, "DHT": 15, "LiquidCrystal_I2C": 25, "RFID": 55, "IRRemote": 4, "Keypad4x4": 25, "StepperMotor": 48}
1284
+ for obj in self.ir.objects:
1285
+ total += object_sizes.get(obj.class_name, 8)
1286
+ # AVR C/C++ string literals used as general stored values live in data
1287
+ # memory unless the backend deliberately wraps them in F()/PROGMEM.
1288
+ # Count those bytes (including the NUL terminator) while excluding the
1289
+ # direct print/Serial/LCD literal paths that the backend flash-backs.
1290
+ total += self._stored_string_literal_sram()
1291
+ classes = {obj.class_name for obj in self.ir.objects}
1292
+ total += 9 # Arduino core (millis timer state)
1293
+ if "uart" in self._resources or "DHT" in classes: # DHT library links HardwareSerial
1294
+ total += 179
1295
+ if "i2c" in self._resources:
1296
+ total += 216
1297
+ if "timer1" in self._resources or "Servo" in classes:
1298
+ total += 38
1299
+ return total
1300
+
1301
+ def _function_local_list_sram(self) -> int:
1302
+ """Fixed lists local to user functions (static storage in the backend)."""
1303
+ from dataclasses import fields, is_dataclass
1304
+ from .symbols import SymbolKind
1305
+
1306
+ seen: dict[object, int] = {}
1307
+
1308
+ def walk(value) -> None:
1309
+ if isinstance(value, IRStore) and value.target.kind == SymbolKind.LOCAL and value.target.type.kind == TypeKind.FIXED_LIST:
1310
+ seen.setdefault(value.target.symbol_id, self._type_size(value.target.type))
1311
+ if is_dataclass(value) and not isinstance(value, type):
1312
+ for f in fields(value):
1313
+ walk(getattr(value, f.name))
1314
+ elif isinstance(value, (tuple, list)):
1315
+ for item in value:
1316
+ walk(item)
1317
+
1318
+ for fn in self.ir.functions:
1319
+ walk(fn.body)
1320
+ return sum(seen.values())
1321
+
1322
+ def _stored_string_literal_sram(self) -> int:
1323
+ total = 0
1324
+
1325
+ def add_text(text: str) -> None:
1326
+ nonlocal total
1327
+ total += len(text.encode("utf-8")) + 1
1328
+
1329
+ def visit_output_expr(expr: IRExpr) -> None:
1330
+ # These exact shapes are emitted through F()/flash-backed output.
1331
+ if isinstance(expr, IRConstant) and isinstance(expr.value, str):
1332
+ return
1333
+ if isinstance(expr, IRFormatString):
1334
+ for part in expr.parts:
1335
+ if not isinstance(part, str):
1336
+ visit_expr(part.value)
1337
+ return
1338
+ visit_expr(expr)
1339
+
1340
+ def visit_expr(expr: IRExpr) -> None:
1341
+ if isinstance(expr, IRConstant):
1342
+ if isinstance(expr.value, str):
1343
+ add_text(expr.value)
1344
+ return
1345
+ if isinstance(expr, IRLoad):
1346
+ return
1347
+ if isinstance(expr, IRListLiteral):
1348
+ for item in expr.elements:
1349
+ visit_expr(item)
1350
+ return
1351
+ if isinstance(expr, IRIndex):
1352
+ visit_expr(expr.collection); visit_expr(expr.index); return
1353
+ if isinstance(expr, IRUnary):
1354
+ visit_expr(expr.operand); return
1355
+ if isinstance(expr, IRBinary):
1356
+ visit_expr(expr.left); visit_expr(expr.right); return
1357
+ if isinstance(expr, IRCompareChain):
1358
+ for item in expr.operands: visit_expr(item)
1359
+ return
1360
+ if isinstance(expr, IRBool):
1361
+ for item in expr.values: visit_expr(item)
1362
+ return
1363
+ if isinstance(expr, IRConditional):
1364
+ visit_expr(expr.condition); visit_expr(expr.when_true); visit_expr(expr.when_false); return
1365
+ if isinstance(expr, IRFormatString):
1366
+ # A format string only appears in output in valid V1 IR. Literal
1367
+ # segments are flash-backed; formatted expressions may still
1368
+ # contain general string literals.
1369
+ for part in expr.parts:
1370
+ if not isinstance(part, str): visit_expr(part.value)
1371
+ return
1372
+ if isinstance(expr, IRCall):
1373
+ direct_flash_output = (
1374
+ len(expr.args) == 1
1375
+ and (
1376
+ (expr.kind == IRCallKind.SINGLETON_METHOD and expr.owner_name == "Serial" and expr.method_name in {"print", "println"})
1377
+ or (
1378
+ expr.kind == IRCallKind.OBJECT_METHOD
1379
+ and expr.owner_symbol is not None
1380
+ and expr.owner_symbol.type.hardware_class == "LiquidCrystal_I2C"
1381
+ and expr.method_name == "print"
1382
+ )
1383
+ )
1384
+ )
1385
+ for i, arg in enumerate(expr.args):
1386
+ if direct_flash_output and i == 0:
1387
+ visit_output_expr(arg)
1388
+ else:
1389
+ visit_expr(arg)
1390
+ return
1391
+
1392
+ def visit_stmts(stmts: tuple[IRStmt, ...]) -> None:
1393
+ for stmt in stmts:
1394
+ if isinstance(stmt, IRPrint):
1395
+ for expr in stmt.values: visit_output_expr(expr)
1396
+ elif isinstance(stmt, IRExprStmt): visit_expr(stmt.expression)
1397
+ elif isinstance(stmt, IRStore): visit_expr(stmt.value)
1398
+ elif isinstance(stmt, IRIndexStore): visit_expr(stmt.index); visit_expr(stmt.value)
1399
+ elif isinstance(stmt, IRAugStore): visit_expr(stmt.value)
1400
+ elif isinstance(stmt, IRIf): visit_expr(stmt.condition); visit_stmts(stmt.body); visit_stmts(stmt.otherwise)
1401
+ elif isinstance(stmt, IRWhile): visit_expr(stmt.condition); visit_stmts(stmt.body)
1402
+ elif isinstance(stmt, IRRangeLoop): visit_expr(stmt.start); visit_expr(stmt.stop); visit_expr(stmt.step); visit_stmts(stmt.body)
1403
+ elif isinstance(stmt, IRFixedListLoop):
1404
+ if stmt.index_start is not None: visit_expr(stmt.index_start)
1405
+ visit_stmts(stmt.body)
1406
+ elif isinstance(stmt, IRReturn) and stmt.value is not None: visit_expr(stmt.value)
1407
+
1408
+ for decl in self.ir.storages:
1409
+ if decl.initializer is not None:
1410
+ visit_expr(decl.initializer)
1411
+ for obj in self.ir.objects:
1412
+ for arg in obj.args: visit_expr(arg)
1413
+ visit_stmts(self.ir.setup)
1414
+ visit_stmts(self.ir.main_loop)
1415
+ for fn in self.ir.functions: visit_stmts(fn.body)
1416
+ return total
1417
+
1418
+ def _type_size(self, typ: BarmajaType) -> int:
1419
+ if typ.kind in {TypeKind.INT, TypeKind.FLOAT, TypeKind.TIME_U32}: return 4
1420
+ if typ.kind == TypeKind.BOOL: return 1
1421
+ if typ.kind == TypeKind.STRING: return 2 # AVR pointer; stored literal counted separately.
1422
+ if typ.kind == TypeKind.FIXED_LIST:
1423
+ if typ.element_type is None or typ.length is None: return 0
1424
+ return self._type_size(typ.element_type) * typ.length
1425
+ return 0
1426
+
1427
+ # Stack headroom measured on the pinned toolchain (avr-gcc 7.3 + arduino:avr
1428
+ # 1.8.8, simavr ATmega328P). Worker-A stack paint measured the final float32
1429
+ # formatter at 275 B total from RAMEND on the extreme path; static call-frame
1430
+ # analysis gave a 285 B conservative worst case including the UART ISR. Keep a
1431
+ # 320 B minimum whenever heavy float formatting is reachable, while ordinary
1432
+ # sketches retain the smaller base reserve. User-call depth is additive.
1433
+ _STACK_BASE_RESERVE = 160
1434
+ _STACK_FLOAT_FORMAT_RESERVE = 320
1435
+ _STACK_PER_CALL_LEVEL = 16
1436
+
1437
+ def _stack_reserve(self) -> int:
1438
+ from dataclasses import fields, is_dataclass
1439
+
1440
+ functions = {fn.symbol.symbol_id: fn for fn in self.ir.functions}
1441
+
1442
+ def heavy(value) -> bool:
1443
+ # float32 output/rounding helpers use large frames plus 64-bit libgcc calls
1444
+ if isinstance(value, IRPrint) and any(v.type.kind == TypeKind.FLOAT for v in value.values):
1445
+ return True
1446
+ if isinstance(value, IRFormatString) and any(
1447
+ not isinstance(p, str) and (p.precision is not None or p.value.type.kind == TypeKind.FLOAT) for p in value.parts
1448
+ ):
1449
+ return True
1450
+ if isinstance(value, IRCall) and value.kind == IRCallKind.BUILTIN and value.name == "round" and len(value.args) == 2:
1451
+ return True
1452
+ if is_dataclass(value) and not isinstance(value, type):
1453
+ return any(heavy(getattr(value, f.name)) for f in fields(value))
1454
+ if isinstance(value, (tuple, list)):
1455
+ return any(heavy(item) for item in value)
1456
+ return False
1457
+
1458
+ depth_cache: dict[object, int] = {}
1459
+
1460
+ def calls(value, out: set) -> None:
1461
+ if isinstance(value, IRCall) and value.kind == IRCallKind.USER_FUNCTION and value.function_symbol is not None:
1462
+ out.add(value.function_symbol.symbol_id)
1463
+ if is_dataclass(value) and not isinstance(value, type):
1464
+ for f in fields(value):
1465
+ calls(getattr(value, f.name), out)
1466
+ elif isinstance(value, (tuple, list)):
1467
+ for item in value:
1468
+ calls(item, out)
1469
+
1470
+ def depth(fid, stack=()) -> int:
1471
+ if fid in depth_cache:
1472
+ return depth_cache[fid]
1473
+ if fid in stack or fid not in functions:
1474
+ return 0
1475
+ callees: set = set()
1476
+ calls(functions[fid].body, callees)
1477
+ d = 1 + max((depth(c, stack + (fid,)) for c in callees), default=0)
1478
+ depth_cache[fid] = d
1479
+ return d
1480
+
1481
+ roots: set = set()
1482
+ calls(self.ir.setup, roots)
1483
+ calls(self.ir.main_loop, roots)
1484
+ max_depth = max((depth(f) for f in roots), default=0)
1485
+ reserve = self._STACK_BASE_RESERVE + self._STACK_PER_CALL_LEVEL * max_depth
1486
+ heavy_reachable = heavy(self.ir.setup) or heavy(self.ir.main_loop) or any(heavy(fn.body) for fn in self.ir.functions)
1487
+ if heavy_reachable:
1488
+ reserve = max(reserve, self._STACK_FLOAT_FORMAT_RESERVE + self._STACK_PER_CALL_LEVEL * max_depth)
1489
+ return reserve
1490
+
1491
+ def _validate_memory(self, estimate: int) -> None:
1492
+ ratio = estimate / self.board.sram_bytes if self.board.sram_bytes else 0.0
1493
+ reserve = self._stack_reserve()
1494
+ if ratio < self.board.sram_error_ratio and self.board.sram_bytes and estimate + reserve > self.board.sram_bytes:
1495
+ raise HardwareError(
1496
+ f"This program needs about {estimate} of the {self.board.sram_bytes} bytes of {self.board.name} RAM/SRAM for its data, "
1497
+ f"and about {reserve} more bytes of stack for its function calls and number printing. "
1498
+ "The board would run out of memory while running. Use shorter lists/strings or fewer nested function calls.",
1499
+ details="Stack reserve measured on the pinned Uno toolchain; see target._stack_reserve().",
1500
+ )
1501
+ if ratio >= self.board.sram_error_ratio:
1502
+ raise HardwareError(
1503
+ f"This program needs about {estimate} of the {self.board.sram_bytes} bytes of {self.board.name} RAM/SRAM ({ratio:.0%}); "
1504
+ f"above {self.board.sram_error_ratio:.0%} the board can crash while running. Use shorter lists/strings or fewer libraries.",
1505
+ details="Estimate includes user data plus measured core/library RAM; the stack needs the remaining space.",
1506
+ )
1507
+ if ratio >= self.board.sram_warning_ratio:
1508
+ self._warn(
1509
+ "MemoryWarning", "BJ3201",
1510
+ f"This program needs about {estimate} of the {self.board.sram_bytes} bytes of {self.board.name} RAM/SRAM ({ratio:.0%}); little memory is left for running.",
1511
+ None, "Reduce large global lists/strings where possible.",
1512
+ )
1513
+
1514
+ # ---------- value facts ----------
1515
+ def _value_set(self, expr: IRExpr, env: dict) -> frozenset[int] | None:
1516
+ if isinstance(expr, IRConstant) and isinstance(expr.value, (int, bool)):
1517
+ return frozenset({int(expr.value)})
1518
+ if isinstance(expr, IRLoad):
1519
+ if expr.symbol.source_name in CONSTANT_VALUES:
1520
+ return frozenset({CONSTANT_VALUES[expr.symbol.source_name]})
1521
+ known = env.get(expr.symbol.symbol_id) or self._symbol_values.get(expr.symbol.symbol_id)
1522
+ if known is not None:
1523
+ return known
1524
+ fact = self.numeric.range_for_symbol_id(expr.symbol.symbol_id)
1525
+ if fact.known and fact.maximum - fact.minimum <= 63:
1526
+ return frozenset(range(fact.minimum, fact.maximum + 1))
1527
+ return None
1528
+ if isinstance(expr, IRIndex) and isinstance(expr.collection, IRLoad):
1529
+ # Any element of a list whose every possible element value is known,
1530
+ # e.g. digitalWrite(leds[k % 5], HIGH) with leds = [2, 3, 4, 5, 6].
1531
+ values = self._list_values.get(expr.collection.symbol.symbol_id)
1532
+ return values if values else None
1533
+ if isinstance(expr, IRListLiteral):
1534
+ out: set[int] = set()
1535
+ for item in expr.elements:
1536
+ values = self._value_set(item, env)
1537
+ if values is None: return None
1538
+ out.update(values)
1539
+ if len(out) > 64: return None
1540
+ return frozenset(out)
1541
+ if isinstance(expr, IRUnary) and expr.op == IRUnaryOp.NEGATE and expr.type.kind == TypeKind.INT:
1542
+ inner = self._value_set(expr.operand, env)
1543
+ return frozenset(-v for v in inner) if inner is not None else None
1544
+ if (
1545
+ isinstance(expr, IRBinary)
1546
+ and expr.type.kind == TypeKind.INT
1547
+ and expr.op in {IRBinaryOp.ADD, IRBinaryOp.SUBTRACT, IRBinaryOp.MULTIPLY, IRBinaryOp.MODULO}
1548
+ ):
1549
+ # Exact small value-set arithmetic, e.g. `pinMode(i + 2, OUTPUT)` inside
1550
+ # `for i in range(5)`, or `digitalWrite(13, 1 + 1)`. Any result that would
1551
+ # trap at run time (Int32 overflow, modulo by zero) makes the set unknown,
1552
+ # so the backend's runtime guard remains the authority for it.
1553
+ left = self._value_set(expr.left, env)
1554
+ right = self._value_set(expr.right, env)
1555
+ if left is None or right is None or len(left) * len(right) > 4096:
1556
+ return None
1557
+ out_values: set[int] = set()
1558
+ for a in left:
1559
+ for b in right:
1560
+ if expr.op == IRBinaryOp.ADD:
1561
+ value = a + b
1562
+ elif expr.op == IRBinaryOp.SUBTRACT:
1563
+ value = a - b
1564
+ elif expr.op == IRBinaryOp.MULTIPLY:
1565
+ value = a * b
1566
+ else:
1567
+ if b == 0:
1568
+ return None
1569
+ value = a % b # Python floor-modulo, as generated by _bj_mod_i32
1570
+ if value < -(2**31) or value > 2**31 - 1:
1571
+ return None
1572
+ out_values.add(value)
1573
+ if len(out_values) > 64:
1574
+ return None
1575
+ return frozenset(out_values)
1576
+ if isinstance(expr, IRConditional):
1577
+ a = self._value_set(expr.when_true, env); b = self._value_set(expr.when_false, env)
1578
+ if a is not None and b is not None and len(a | b) <= 64: return a | b
1579
+ if isinstance(expr, IRCall) and expr.kind == IRCallKind.USER_FUNCTION and expr.function_symbol is not None:
1580
+ return self._function_return_values.get(expr.function_symbol.symbol_id)
1581
+ return None
1582
+
1583
+ def _single_int(self, expr: IRExpr, env: dict) -> int | None:
1584
+ values = self._value_set(expr, env)
1585
+ if values is not None and len(values) == 1:
1586
+ return next(iter(values))
1587
+ return None
1588
+
1589
+ def _range_values(self, start: IRExpr, stop: IRExpr, step: IRExpr, env: dict) -> frozenset[int] | None:
1590
+ a, b, s = self._single_int(start, env), self._single_int(stop, env), self._single_int(step, env)
1591
+ if a is None or b is None or s in (None, 0): return None
1592
+ values = list(range(a, b, s))
1593
+ if len(values) > 64: return None
1594
+ return frozenset(values)
1595
+
1596
+ @staticmethod
1597
+ def _intersect_env(a: dict, b: dict) -> dict:
1598
+ out = {}
1599
+ for key in a.keys() & b.keys():
1600
+ merged = a[key] | b[key]
1601
+ if len(merged) <= 64:
1602
+ out[key] = merged
1603
+ return out
1604
+
1605
+ def _display_pin(self, expr: IRExpr, value: int) -> str:
1606
+ if isinstance(expr, IRLoad) and expr.symbol.source_name in self.board.analog_aliases:
1607
+ return expr.symbol.source_name
1608
+ return str(value)
1609
+
1610
+ # ---------- diagnostics ----------
1611
+ def _hardware_error(self, message: str, span: SourceSpan | None, hint: str | None = None):
1612
+ raise HardwareError(
1613
+ message,
1614
+ line=span.line if span else None,
1615
+ column=(span.column + 1) if span else None,
1616
+ end_line=span.end_line if span else None,
1617
+ end_column=span.end_column if span else None,
1618
+ details=hint,
1619
+ )
1620
+
1621
+ def _warn(self, category: str, code: str, message: str, span: SourceSpan | None, hint: str | None = None, related: tuple[SourceSpan, ...] = ()) -> None:
1622
+ key = (code, span.line if span else None, span.column if span else None, message)
1623
+ if key in self._diag_keys:
1624
+ return
1625
+ self._diag_keys.add(key)
1626
+ self._diagnostics.append(TargetDiagnostic(category, code, "warning", message, self.ir.source_name, span, hint, related))
1627
+
1628
+
1629
+
1630
+ def analyze_target(ir_module: IRModule, numeric: NumericAnalysis, board: BoardProfile) -> TargetPlan:
1631
+ return TargetAnalyzer(ir_module, numeric, board).analyze()