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,1721 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from .board import BoardProfile, UNO
8
+ from .errors import BarmajaError, CompileError, InternalCompilerError
9
+ from .frontend import FrontendModule, is_noop_stmt, validate_frontend
10
+ from .flow import FlowAnalyzedModule, StorageClass, analyze_flow
11
+ from .source import ParsedSource, load_source_file, parse_source_text
12
+ from .symbols import ResolvedModule, SymbolKind, resolve_symbols
13
+ from .typesys import TypedModule, TypeKind, analyze_types
14
+ from .numeric import NumericAnalysis, analyze_numeric_ranges
15
+ from .ir import IRModule
16
+ from .lowering import lower_to_ir
17
+ from .target import TargetPlan, TargetDiagnostic, analyze_target
18
+ from .backend import GeneratedSketch, generate_uno_cpp
19
+ from .api_registry import CORE_FUNCTIONS, HARDWARE_CLASSES as API_HARDWARE_CLASSES, HOST_COMMANDS, SINGLETONS
20
+
21
+ CORE_CALLS = frozenset(CORE_FUNCTIONS)
22
+ BUILTIN_CALLS = {"round", "min", "max", "abs", "int", "float", "bool", "len", "sum", "enumerate"}
23
+ HARDWARE_CLASSES = frozenset(API_HARDWARE_CLASSES)
24
+ OBJECT_METHODS = {name: frozenset(spec.methods) for name, spec in API_HARDWARE_CLASSES.items()}
25
+ SINGLETON_METHODS = {name: frozenset(spec.methods) for name, spec in SINGLETONS.items()}
26
+ CONSTANTS = {
27
+ "HIGH", "LOW", "INPUT", "OUTPUT", "INPUT_PULLUP",
28
+ "A0", "A1", "A2", "A3", "A4", "A5",
29
+ }
30
+
31
+
32
+ @dataclass
33
+ class Program:
34
+ globals: list[ast.Assign] = field(default_factory=list)
35
+ objects: list[ast.Assign] = field(default_factory=list)
36
+ functions: list[ast.FunctionDef] = field(default_factory=list)
37
+ setup: list[ast.stmt] = field(default_factory=list)
38
+ loop: list[ast.stmt] = field(default_factory=list)
39
+ imports: set[str] = field(default_factory=set)
40
+ uses_print: bool = False
41
+ used_singletons: set[str] = field(default_factory=set)
42
+
43
+
44
+ @dataclass
45
+ class CompileResult:
46
+ source_path: Path | None
47
+ cpp: str
48
+ program: Program
49
+ board: BoardProfile
50
+ parsed_source: ParsedSource | None = None
51
+ resolved_module: ResolvedModule | None = None
52
+ flow_analyzed_module: FlowAnalyzedModule | None = None
53
+ typed_module: TypedModule | None = None
54
+ numeric_analysis: NumericAnalysis | None = None
55
+ ir_module: IRModule | None = None
56
+ target_plan: TargetPlan | None = None
57
+ generated_sketch: GeneratedSketch | None = None
58
+ warnings: tuple[TargetDiagnostic, ...] = ()
59
+
60
+
61
+ @dataclass
62
+ class FunctionInfo:
63
+ return_type: str
64
+ param_types: tuple[str, ...]
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class _OutputPart:
69
+ literal: str | None = None
70
+ expression: str | None = None
71
+ type_kind: TypeKind | None = None
72
+ precision: int | None = None
73
+
74
+
75
+ def _is_host_command_stmt(stmt: ast.stmt) -> bool:
76
+ return (
77
+ isinstance(stmt, ast.Expr)
78
+ and isinstance(stmt.value, ast.Call)
79
+ and isinstance(stmt.value.func, ast.Name)
80
+ and stmt.value.func.id in HOST_COMMANDS
81
+ )
82
+
83
+
84
+ def _strip_host_commands(parsed: ParsedSource) -> ParsedSource:
85
+ """Remove top-level host workflow lines such as `upload()` and `LiveRun()`.
86
+
87
+ They are PC-side workflow commands (PictoBlox/IDE "Run" button) and never
88
+ part of the Arduino program. Any other use is a clear student error.
89
+ """
90
+ import dataclasses
91
+ body = [stmt for stmt in parsed.tree.body if not _is_host_command_stmt(stmt)]
92
+ for node in ast.walk(ast.Module(body=body, type_ignores=[])):
93
+ if isinstance(node, ast.Name) and node.id in HOST_COMMANDS:
94
+ raise CompileError(
95
+ f"{node.id}() must be written on its own line at the top level of the program (for example right after the import).",
96
+ getattr(node, "lineno", None),
97
+ (getattr(node, "col_offset", 0) or 0) + 1,
98
+ )
99
+ if len(body) == len(parsed.tree.body):
100
+ return parsed
101
+ tree = ast.Module(body=body, type_ignores=list(getattr(parsed.tree, "type_ignores", [])))
102
+ return dataclasses.replace(parsed, tree=tree)
103
+
104
+
105
+ class Transpiler:
106
+ def __init__(self, board: BoardProfile = UNO):
107
+ self.board = board
108
+ self._global_values: dict[str, object] = {}
109
+ self._functions: dict[str, FunctionInfo] = {}
110
+ self._objects: dict[str, str] = {}
111
+ self._resolved: ResolvedModule | None = None
112
+ self._flow: FlowAnalyzedModule | None = None
113
+ self._typed: TypedModule | None = None
114
+ self._numeric: NumericAnalysis | None = None
115
+ self._target_plan: TargetPlan | None = None
116
+ self._temp_counter = 0
117
+
118
+ def _reset_state(self) -> None:
119
+ # One Transpiler instance may compile multiple independent student files.
120
+ self._global_values = {}
121
+ self._functions = {}
122
+ self._objects = {}
123
+ self._resolved = None
124
+ self._flow = None
125
+ self._typed = None
126
+ self._numeric = None
127
+ self._target_plan = None
128
+ self._temp_counter = 0
129
+
130
+ def _compile_parsed(self, parsed: ParsedSource, source_path: Path | None = None) -> CompileResult:
131
+ self._reset_state()
132
+ try:
133
+ parsed = _strip_host_commands(parsed)
134
+ frontend = validate_frontend(parsed.tree)
135
+ resolved = resolve_symbols(frontend)
136
+ self._resolved = resolved
137
+ flow = analyze_flow(resolved)
138
+ self._flow = flow
139
+ # Program is retained only as a compatibility/introspection view. It is
140
+ # classified before type/IR phases so no production phase after typed
141
+ # lowering depends on raw Python AST semantics.
142
+ program = self._classify(frontend)
143
+ typed = analyze_types(flow)
144
+ self._typed = typed
145
+ numeric = analyze_numeric_ranges(typed)
146
+ self._numeric = numeric
147
+ ir_module = lower_to_ir(typed, numeric, parsed.filename)
148
+ target_plan = analyze_target(ir_module, numeric, self.board)
149
+ self._target_plan = target_plan
150
+ generated = generate_uno_cpp(ir_module, target_plan)
151
+ return CompileResult(
152
+ source_path=source_path,
153
+ cpp=generated.cpp,
154
+ program=program,
155
+ board=self.board,
156
+ parsed_source=parsed,
157
+ resolved_module=resolved,
158
+ flow_analyzed_module=flow,
159
+ typed_module=typed,
160
+ numeric_analysis=numeric,
161
+ ir_module=ir_module,
162
+ target_plan=target_plan,
163
+ generated_sketch=generated,
164
+ warnings=target_plan.warnings,
165
+ )
166
+ except RecursionError:
167
+ raise CompileError(
168
+ "This expression or statement is nested too deeply for Barmaja PyArduino. Split it into smaller steps.",
169
+ None,
170
+ ) from None
171
+ except BarmajaError as exc:
172
+ # Attach the source identity without changing existing error wording.
173
+ if getattr(exc, "source", None) is None:
174
+ exc.source = parsed.filename
175
+ raise
176
+ except Exception as exc:
177
+ # Student code must never surface raw IndexError/KeyError/etc. The
178
+ # detailed root cause is retained for trainer/developer diagnostics.
179
+ raise InternalCompilerError(
180
+ "An internal compiler error occurred while processing this program.",
181
+ source=parsed.filename,
182
+ details=f"{type(exc).__name__}: {exc}",
183
+ ) from None
184
+
185
+ def compile_text(self, source: str, source_path: Path | None = None) -> CompileResult:
186
+ parsed = parse_source_text(source, source_path)
187
+ return self._compile_parsed(parsed, source_path)
188
+
189
+ def compile_file(self, path: str | Path) -> CompileResult:
190
+ p = Path(path)
191
+ parsed = load_source_file(p)
192
+ return self._compile_parsed(parsed, p)
193
+
194
+ # ---------- classification / validation ----------
195
+ def _classify(self, frontend: FrontendModule) -> Program:
196
+ tree = frontend.tree
197
+ p = Program()
198
+ main_loop_seen = False
199
+ for node in tree.body:
200
+ if isinstance(node, ast.ImportFrom):
201
+ if node.module != "Barmaja_PyArduino":
202
+ raise CompileError(
203
+ "Arduino projects may only import supported Barmaja PyArduino modules.",
204
+ node.lineno, node.col_offset + 1,
205
+ )
206
+ p.imports.update(alias.name for alias in node.names)
207
+ continue
208
+ if isinstance(node, ast.Import):
209
+ raise CompileError("Use 'from Barmaja_PyArduino import ...' in Arduino projects.", node.lineno)
210
+ if is_noop_stmt(node, docstring_allowed=True) and not main_loop_seen:
211
+ continue
212
+ if isinstance(node, ast.FunctionDef) and not main_loop_seen:
213
+ p.functions.append(node)
214
+ continue
215
+ if isinstance(node, ast.Assign) and not main_loop_seen and self._is_hardware_object_assignment(node):
216
+ p.objects.append(node)
217
+ continue
218
+ if isinstance(node, ast.Assign) and not main_loop_seen and self._is_compile_time_assignment(node):
219
+ # Step 6: a literal assignment is file-scope only when flow
220
+ # analysis proves constant/persistent module storage. Mutable
221
+ # setup-only values remain setup locals.
222
+ storages = []
223
+ if self._flow is not None:
224
+ targets = [node.targets[0]]
225
+ if isinstance(node.targets[0], (ast.Tuple, ast.List)):
226
+ targets = list(node.targets[0].elts)
227
+ for target in targets:
228
+ if isinstance(target, ast.Name):
229
+ sym = self._resolved.symbol_for_name(target) if self._resolved is not None else None
230
+ if sym is not None:
231
+ storages.append(self._flow.storage_for(sym))
232
+ if storages and all(x in {StorageClass.MODULE_CONSTANT, StorageClass.PERSISTENT_MODULE} for x in storages):
233
+ p.globals.append(node)
234
+ else:
235
+ p.setup.append(node)
236
+ continue
237
+ if isinstance(node, ast.While) and self._is_true(node.test):
238
+ if main_loop_seen:
239
+ raise CompileError("Only one top-level 'while True:' main loop is allowed.", node.lineno)
240
+ if node.orelse:
241
+ raise CompileError("The main 'while True:' loop cannot have an else block.", node.lineno)
242
+ main_loop_seen = True
243
+ p.loop.extend(node.body)
244
+ continue
245
+ if main_loop_seen:
246
+ raise CompileError("Nothing may follow the main top-level 'while True:' loop.", getattr(node, "lineno", None))
247
+ p.setup.append(node)
248
+ return p
249
+
250
+ @staticmethod
251
+ def _is_true(node: ast.AST) -> bool:
252
+ return isinstance(node, ast.Constant) and node.value is True
253
+
254
+
255
+ @staticmethod
256
+ def _is_hardware_object_assignment(node: ast.Assign) -> bool:
257
+ return (
258
+ len(node.targets) == 1
259
+ and isinstance(node.targets[0], ast.Name)
260
+ and isinstance(node.value, ast.Call)
261
+ and isinstance(node.value.func, ast.Name)
262
+ and node.value.func.id in HARDWARE_CLASSES
263
+ )
264
+
265
+ @staticmethod
266
+ def _is_compile_time_assignment(node: ast.Assign) -> bool:
267
+ if len(node.targets) != 1:
268
+ return False
269
+ target = node.targets[0]
270
+ if isinstance(target, ast.Name):
271
+ if isinstance(node.value, (ast.Constant, ast.UnaryOp)):
272
+ return True
273
+ if isinstance(node.value, ast.List) and node.value.elts:
274
+ return all(isinstance(v, (ast.Constant, ast.UnaryOp)) for v in node.value.elts)
275
+ return False
276
+ if isinstance(target, (ast.Tuple, ast.List)) and isinstance(node.value, (ast.Tuple, ast.List)):
277
+ return len(target.elts) == len(node.value.elts) and all(isinstance(t, ast.Name) for t in target.elts) and all(
278
+ isinstance(v, (ast.Constant, ast.UnaryOp)) for v in node.value.elts
279
+ )
280
+ return False
281
+
282
+ # LEGACY REFERENCE BACKEND (Steps 1-14)
283
+ # ---------------------------------------
284
+ # Step 15 retired the methods below from the production compile pipeline.
285
+ # They are temporarily retained as a regression/reference implementation
286
+ # while Step 16+ stabilizes packaging/toolchain work. Production Phase H
287
+ # uses backend.uno_cpp.generate_uno_cpp(IRModule, TargetPlan) exclusively.
288
+ def _analyze(self, p: Program) -> None:
289
+ for node in p.globals:
290
+ self._infer_global(node)
291
+ for node in p.objects:
292
+ name = node.targets[0].id
293
+ class_name = node.value.func.id
294
+ self._objects[name] = class_name
295
+ for arg in node.value.args:
296
+ self._validate_expr(arg, p)
297
+ if self._typed is None:
298
+ raise InternalCompilerError("Typed analysis is unavailable during compiler validation.")
299
+ for fn in p.functions:
300
+ signature = self._typed.signature_for(fn.name)
301
+ self._functions[fn.name] = FunctionInfo(
302
+ signature.return_type.cpp_type,
303
+ tuple(t.cpp_type for t in signature.parameter_types),
304
+ )
305
+ for fn in p.functions:
306
+ self._validate_function(fn, p)
307
+ for stmt in [*p.setup, *p.loop]:
308
+ self._validate_stmt(stmt, p)
309
+
310
+ def _infer_global(self, node: ast.Assign) -> None:
311
+ target = node.targets[0]
312
+ pairs = [(target, node.value)] if isinstance(target, ast.Name) else list(zip(target.elts, node.value.elts))
313
+ for name_node, value_node in pairs:
314
+ self._global_values[name_node.id] = self._literal_python_value(value_node)
315
+
316
+ def _validate_function(self, fn: ast.FunctionDef, p: Program) -> None:
317
+ if fn.decorator_list:
318
+ raise CompileError("Decorators are not supported in Barmaja PyArduino V1.", fn.lineno)
319
+ if fn.args.vararg or fn.args.kwarg or fn.args.kwonlyargs:
320
+ raise CompileError("Use simple positional function parameters in Barmaja PyArduino V1.", fn.lineno)
321
+ for stmt in fn.body:
322
+ self._validate_stmt(stmt, p, in_function=True)
323
+
324
+ def _validate_stmt(self, node: ast.stmt, p: Program, in_function: bool = False) -> None:
325
+ if is_noop_stmt(node, docstring_allowed=True):
326
+ return
327
+ if isinstance(node, ast.Expr):
328
+ if not isinstance(node.value, ast.Call):
329
+ raise CompileError("Only supported function calls may be standalone statements here.", node.lineno)
330
+ self._validate_call(node.value, p)
331
+ return
332
+ if isinstance(node, ast.Assign):
333
+ if len(node.targets) != 1:
334
+ raise CompileError("Chained assignment is not supported in V1.", node.lineno)
335
+ target = node.targets[0]
336
+ if not isinstance(target, (ast.Name, ast.Subscript)):
337
+ raise CompileError("Use a variable name or fixed-list item assignment.", node.lineno)
338
+ if isinstance(target, ast.Subscript):
339
+ self._validate_expr(target, p)
340
+ self._validate_expr(node.value, p)
341
+ return
342
+ if isinstance(node, ast.AugAssign):
343
+ if not isinstance(node.target, ast.Name) or not isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod)):
344
+ raise CompileError("This augmented assignment is not supported yet.", node.lineno)
345
+ self._validate_expr(node.value, p)
346
+ return
347
+ if isinstance(node, ast.If):
348
+ self._validate_expr(node.test, p)
349
+ for s in node.body + node.orelse:
350
+ self._validate_stmt(s, p, in_function)
351
+ return
352
+ if isinstance(node, ast.While):
353
+ self._validate_expr(node.test, p)
354
+ for s in node.body:
355
+ self._validate_stmt(s, p, in_function)
356
+ return
357
+ if isinstance(node, ast.For):
358
+ if isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) and node.iter.func.id == "range":
359
+ if not isinstance(node.target, ast.Name):
360
+ raise CompileError("range() loop target must be a simple name.", node.lineno)
361
+ self._parse_range(node.iter, node, p)
362
+ elif isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) and node.iter.func.id == "enumerate":
363
+ self._validate_call(node.iter, p)
364
+ else:
365
+ self._validate_expr(node.iter, p)
366
+ for s in node.body:
367
+ self._validate_stmt(s, p, in_function)
368
+ if node.orelse:
369
+ raise CompileError("for/else is not supported in Barmaja PyArduino V1.", node.lineno)
370
+ return
371
+ if isinstance(node, ast.Return) and in_function:
372
+ if node.value is not None:
373
+ self._validate_expr(node.value, p)
374
+ return
375
+ if isinstance(node, ast.Global) and in_function:
376
+ # Phase C has already validated and resolved restricted global names.
377
+ return
378
+ if isinstance(node, (ast.Break, ast.Continue)):
379
+ # Phase D has already resolved/validated the loop target.
380
+ return
381
+ raise CompileError(f"'{type(node).__name__}' is not supported yet.", getattr(node, "lineno", None))
382
+
383
+ def _validate_expr(self, node: ast.AST, p: Program) -> None:
384
+ if isinstance(node, (ast.Constant, ast.Name)):
385
+ return
386
+ if isinstance(node, ast.JoinedStr):
387
+ for v in node.values:
388
+ if isinstance(v, ast.FormattedValue):
389
+ self._validate_expr(v.value, p)
390
+ return
391
+ if isinstance(node, ast.Call):
392
+ self._validate_call(node, p)
393
+ return
394
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.Not)):
395
+ self._validate_expr(node.operand, p); return
396
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod)):
397
+ self._validate_expr(node.left, p); self._validate_expr(node.right, p); return
398
+ if isinstance(node, ast.Compare):
399
+ self._validate_expr(node.left, p)
400
+ for c in node.comparators: self._validate_expr(c, p)
401
+ return
402
+ if isinstance(node, ast.BoolOp) and isinstance(node.op, (ast.And, ast.Or)):
403
+ for v in node.values: self._validate_expr(v, p)
404
+ return
405
+ if isinstance(node, ast.IfExp):
406
+ self._validate_expr(node.test, p); self._validate_expr(node.body, p); self._validate_expr(node.orelse, p); return
407
+ if isinstance(node, ast.List):
408
+ for item in node.elts: self._validate_expr(item, p)
409
+ return
410
+ if isinstance(node, ast.Subscript):
411
+ self._validate_expr(node.value, p); self._validate_expr(node.slice, p); return
412
+ raise CompileError(f"Expression '{type(node).__name__}' is not supported yet.", getattr(node, "lineno", None))
413
+
414
+ def _validate_call(self, node: ast.Call, p: Program) -> None:
415
+ if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
416
+ owner = node.func.value.id
417
+ method = node.func.attr
418
+ if owner in self._objects:
419
+ cls = self._objects[owner]
420
+ if method not in OBJECT_METHODS[cls]:
421
+ raise CompileError(f"{cls}.{method}() is not supported in Barmaja PyArduino V1.", node.lineno)
422
+ elif owner in SINGLETON_METHODS:
423
+ if method not in SINGLETON_METHODS[owner]:
424
+ raise CompileError(f"{owner}.{method}() is not supported in Barmaja PyArduino V1.", node.lineno)
425
+ p.used_singletons.add(owner)
426
+ else:
427
+ raise CompileError(f"Unknown Arduino object '{owner}'.", node.lineno)
428
+ for arg in node.args:
429
+ self._validate_expr(arg, p)
430
+ if node.keywords:
431
+ raise CompileError("Keyword arguments are not supported for Arduino library methods.", node.lineno)
432
+ return
433
+ if not isinstance(node.func, ast.Name):
434
+ raise CompileError("Unsupported call form.", node.lineno)
435
+ name = node.func.id
436
+ if name == "print":
437
+ p.uses_print = True
438
+ for arg in node.args: self._validate_expr(arg, p)
439
+ if node.keywords:
440
+ raise CompileError("Custom print() sep/end are not supported in V1.", node.lineno)
441
+ return
442
+ if name not in CORE_CALLS and name not in BUILTIN_CALLS and name not in self._functions:
443
+ raise CompileError(f"'{name}' is not a supported Barmaja PyArduino call yet.", node.lineno)
444
+ for arg in node.args: self._validate_expr(arg, p)
445
+ if node.keywords:
446
+ raise CompileError("Keyword arguments are not supported for this call.", node.lineno)
447
+
448
+ # ---------- resolved backend names ----------
449
+ def _resolved_name(self, node: ast.Name) -> str:
450
+ if self._resolved is None:
451
+ raise InternalCompilerError("Symbol resolution is unavailable during code generation.")
452
+ try:
453
+ return self._resolved.symbol_for_name(node).cpp_name
454
+ except KeyError:
455
+ raise InternalCompilerError(
456
+ "A source name reached code generation without a resolved binding.",
457
+ line=getattr(node, "lineno", None),
458
+ details=f"unresolved name: {node.id}",
459
+ ) from None
460
+
461
+ def _resolved_function_name(self, fn: ast.FunctionDef) -> str:
462
+ if self._resolved is None:
463
+ raise InternalCompilerError("Symbol resolution is unavailable during code generation.")
464
+ try:
465
+ return self._resolved.symbol_for_function(fn).cpp_name
466
+ except KeyError:
467
+ raise InternalCompilerError(
468
+ "A function reached code generation without a resolved binding.",
469
+ line=getattr(fn, "lineno", None),
470
+ details=f"unresolved function: {fn.name}",
471
+ ) from None
472
+
473
+ def _resolved_parameter_name(self, arg: ast.arg) -> str:
474
+ if self._resolved is None:
475
+ raise InternalCompilerError("Symbol resolution is unavailable during code generation.")
476
+ try:
477
+ return self._resolved.symbol_for_parameter(arg).cpp_name
478
+ except KeyError:
479
+ raise InternalCompilerError(
480
+ "A function parameter reached code generation without a resolved binding.",
481
+ line=getattr(arg, "lineno", None),
482
+ details=f"unresolved parameter: {arg.arg}",
483
+ ) from None
484
+
485
+ # ---------- C++ generation ----------
486
+ def _generate(self, p: Program) -> str:
487
+ if self._resolved is None or self._flow is None:
488
+ raise InternalCompilerError("Flow/symbol analysis is unavailable during code generation.")
489
+
490
+ if self._target_plan is None:
491
+ raise InternalCompilerError("Target analysis is unavailable during code generation.")
492
+ includes = [f"#include <{header}>" for header in self._target_plan.includes]
493
+ lines = [
494
+ "// Generated by Barmaja PyArduino 0.6.0",
495
+ "// Edit the Python source, not this generated file.",
496
+ *includes, "",
497
+ ]
498
+ helpers = self._numeric_helper_lines()
499
+ if helpers:
500
+ lines.extend(helpers)
501
+ lines.append("")
502
+ output_helpers = self._output_helper_lines()
503
+ if output_helpers:
504
+ lines.extend(output_helpers)
505
+ lines.append("")
506
+
507
+ initialized_global_ids = set()
508
+ for node in p.globals:
509
+ target = node.targets[0]
510
+ pairs = [(target, node.value)] if isinstance(target, ast.Name) else list(zip(target.elts, node.value.elts))
511
+ for t, v in pairs:
512
+ symbol = self._resolved.symbol_for_name(t)
513
+ initialized_global_ids.add(symbol.symbol_id)
514
+ typ_obj = self._typed.type_for_symbol(symbol) if self._typed is not None else None
515
+ if typ_obj is not None and typ_obj.kind == TypeKind.FIXED_LIST:
516
+ if not isinstance(v, ast.List):
517
+ raise InternalCompilerError("Fixed list reached global generation without a list initializer.")
518
+ const_prefix = "const " if self._flow.storage_for(symbol) == StorageClass.MODULE_CONSTANT else ""
519
+ elem = typ_obj.element_type.cpp_type if typ_obj.element_type is not None else "int32_t"
520
+ items = ", ".join(self._expr(x) for x in v.elts)
521
+ lines.append(f"{const_prefix}{elem} {symbol.cpp_name}[{typ_obj.length}] = {{{items}}};")
522
+ else:
523
+ typ = self._cpp_type_for_symbol(symbol)
524
+ if self._flow.storage_for(symbol) == StorageClass.MODULE_CONSTANT:
525
+ decl_type = "const char* const" if typ == "const char*" else f"const {typ}"
526
+ else:
527
+ decl_type = typ
528
+ lines.append(f"{decl_type} {symbol.cpp_name} = {self._expr(v)};")
529
+
530
+ # Persistent state whose initialization must execute in setup/loop is
531
+ # allocated exactly once at file scope, without moving the assignment.
532
+ persistent = [
533
+ fact.symbol for fact in self._flow.symbol_facts.values()
534
+ if fact.storage == StorageClass.PERSISTENT_MODULE
535
+ and fact.symbol.symbol_id not in initialized_global_ids
536
+ ]
537
+ for symbol in sorted(persistent, key=lambda x: (x.declaration_line or 10**9, x.symbol_id.value)):
538
+ lines.append(self._declaration_line(symbol))
539
+ if p.globals or persistent:
540
+ lines.append("")
541
+
542
+ for node in p.objects:
543
+ target = node.targets[0]
544
+ name = self._resolved_name(target)
545
+ cls = node.value.func.id
546
+ args = ", ".join(self._expr(a) for a in node.value.args)
547
+ lines.append(f"{cls} {name}({args});" if args else f"{cls} {name};")
548
+ if p.objects:
549
+ lines.append("")
550
+
551
+ # Function prototypes make source-order irrelevant after Phase C/D.
552
+ for fn in p.functions:
553
+ info = self._functions[fn.name]
554
+ params = ", ".join(f"{typ} {self._resolved_parameter_name(a)}" for typ, a in zip(info.param_types, fn.args.args))
555
+ lines.append(f"{info.return_type} {self._resolved_function_name(fn)}({params});")
556
+ if p.functions:
557
+ lines.append("")
558
+
559
+ for fn in p.functions:
560
+ lines.extend(self._function(fn)); lines.append("")
561
+
562
+ lines.append("void setup() {")
563
+ setup_symbols = self._symbols_with_storage(StorageClass.SETUP_LOCAL)
564
+ setup_scope = self._module_predeclared_cpp_names(StorageClass.PERSISTENT_MODULE, StorageClass.MODULE_CONSTANT, StorageClass.HARDWARE_PERSISTENT)
565
+ for symbol in setup_symbols:
566
+ lines.append(" " + self._declaration_line(symbol))
567
+ setup_scope.add(symbol.cpp_name)
568
+ for hook in self._target_plan.setup_hooks:
569
+ lines.append(" " + hook)
570
+ for stmt in p.setup:
571
+ lines.extend(self._stmt(stmt, 1, setup_scope, p, loop_stack=()))
572
+ lines.append("}")
573
+ lines.append("")
574
+
575
+ lines.append("void loop() {")
576
+ loop_symbols = self._symbols_with_storage(StorageClass.LOOP_TEMPORARY)
577
+ loop_scope = self._module_predeclared_cpp_names(StorageClass.PERSISTENT_MODULE, StorageClass.MODULE_CONSTANT, StorageClass.HARDWARE_PERSISTENT)
578
+ for symbol in loop_symbols:
579
+ lines.append(" " + self._declaration_line(symbol))
580
+ loop_scope.add(symbol.cpp_name)
581
+ for stmt in p.loop:
582
+ lines.extend(self._stmt(stmt, 1, loop_scope, p, loop_stack=("main",)))
583
+ lines.append("}")
584
+ lines.append("")
585
+ return "\n".join(lines)
586
+
587
+ def _function(self, fn: ast.FunctionDef) -> list[str]:
588
+ if self._resolved is None or self._flow is None:
589
+ raise InternalCompilerError("Flow/symbol analysis is unavailable during function generation.")
590
+ info = self._functions[fn.name]
591
+ params = ", ".join(f"{typ} {self._resolved_parameter_name(a)}" for typ, a in zip(info.param_types, fn.args.args))
592
+ out = [f"{info.return_type} {self._resolved_function_name(fn)}({params}) {{"]
593
+ scope = {self._resolved_parameter_name(a) for a in fn.args.args}
594
+ fn_scope = self._resolved.function_scopes.get(fn.name)
595
+ if fn_scope is not None:
596
+ for global_name in fn_scope.global_names:
597
+ module_symbol = self._resolved.module_scope.symbols.get(global_name)
598
+ if module_symbol is not None:
599
+ scope.add(module_symbol.cpp_name)
600
+ locals_ = [
601
+ sym for sym in fn_scope.symbols.values()
602
+ if self._flow.storage_for(sym) == StorageClass.FUNCTION_LOCAL
603
+ ]
604
+ for symbol in sorted(locals_, key=lambda x: (x.declaration_line or 10**9, x.symbol_id.value)):
605
+ out.append(" " + self._declaration_line(symbol))
606
+ scope.add(symbol.cpp_name)
607
+ dummy = Program() # statement generator only; analysis already completed
608
+ for stmt in fn.body:
609
+ out.extend(self._stmt(stmt, 1, scope, dummy, loop_stack=()))
610
+ out.append("}")
611
+ return out
612
+
613
+ def _stmt(
614
+ self,
615
+ node: ast.stmt,
616
+ indent: int,
617
+ scope: set[str],
618
+ p: Program,
619
+ *,
620
+ loop_stack: tuple[str, ...],
621
+ ) -> list[str]:
622
+ pad = " " * indent
623
+ if is_noop_stmt(node, docstring_allowed=True):
624
+ return []
625
+ if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
626
+ if isinstance(node.value.func, ast.Name) and node.value.func.id == "print":
627
+ return self._serial_print(node.value, indent)
628
+ if isinstance(node.value.func, ast.Attribute) and node.value.func.attr in {"print", "println"}:
629
+ owner_node = node.value.func.value
630
+ if isinstance(owner_node, ast.Name):
631
+ owner_source = owner_node.id
632
+ is_supported_sink = owner_source == "Serial" or self._objects.get(owner_source) == "LiquidCrystal_I2C"
633
+ if is_supported_sink:
634
+ special = self._explicit_output_stmt(node.value, indent)
635
+ if special is not None:
636
+ return special
637
+ return [pad + self._expr(node.value) + ";"]
638
+ if isinstance(node, ast.Assign):
639
+ target = node.targets[0]
640
+ if isinstance(target, ast.Subscript):
641
+ return [f"{pad}{self._subscript_lvalue(target)} = {self._expr(node.value)};"]
642
+ if not isinstance(target, ast.Name):
643
+ raise InternalCompilerError("Unsupported assignment target reached backend.", line=getattr(node, "lineno", None))
644
+ name = self._resolved_name(target)
645
+ symbol = self._resolved.symbol_for_name(target) if self._resolved is not None else None
646
+ typ_obj = self._typed.type_for_symbol(symbol) if (self._typed is not None and symbol is not None) else None
647
+ if typ_obj is not None and typ_obj.kind == TypeKind.FIXED_LIST:
648
+ if not isinstance(node.value, ast.List):
649
+ raise CompileError("Whole fixed-list reassignment is not supported in V1; update individual items instead.", node.lineno)
650
+ items = [self._expr(x) for x in node.value.elts]
651
+ if name in scope:
652
+ return [f"{pad}{name}[{i}] = {value};" for i, value in enumerate(items)]
653
+ scope.add(name)
654
+ elem = typ_obj.element_type.cpp_type if typ_obj.element_type is not None else "int32_t"
655
+ return [f"{pad}{elem} {name}[{typ_obj.length}] = {{{', '.join(items)}}};"]
656
+ expr = self._expr(node.value)
657
+ if name in scope:
658
+ return [f"{pad}{name} = {expr};"]
659
+ scope.add(name)
660
+ typ = self._infer_expr_type(node.value)
661
+ return [f"{pad}{typ} {name} = {expr};"]
662
+ if isinstance(node, ast.AugAssign):
663
+ target_name = self._resolved_name(node.target)
664
+ target_type = self._typed.type_for_expr(node.target) if self._typed is not None else None
665
+ rhs = self._expr(node.value)
666
+ if isinstance(node.op, ast.Div):
667
+ return [f"{pad}{target_name} = _bj_true_div_f32((float)({target_name}), (float)({rhs}));"]
668
+ if isinstance(node.op, ast.Mod):
669
+ if target_type is not None and target_type.kind == TypeKind.FLOAT:
670
+ return [f"{pad}{target_name} = _bj_mod_f32((float)({target_name}), (float)({rhs}));"]
671
+ return [f"{pad}{target_name} = _bj_mod_i32({target_name}, {rhs});"]
672
+ if target_type is not None and target_type.kind == TypeKind.INT and isinstance(node.op, (ast.Add, ast.Sub, ast.Mult)):
673
+ helper = {ast.Add: "_bj_i32_add", ast.Sub: "_bj_i32_sub", ast.Mult: "_bj_i32_mul"}[type(node.op)]
674
+ return [f"{pad}{target_name} = {helper}({target_name}, {rhs});"]
675
+ ops = {ast.Add: "+=", ast.Sub: "-=", ast.Mult: "*="}
676
+ return [f"{pad}{target_name} {ops[type(node.op)]} {rhs};"]
677
+ if isinstance(node, ast.Return):
678
+ return [pad + ("return;" if node.value is None else f"return {self._expr(node.value)};")]
679
+ if isinstance(node, ast.Global):
680
+ return []
681
+ if isinstance(node, ast.Break):
682
+ if not loop_stack:
683
+ raise InternalCompilerError("break reached the backend without a loop target.", line=getattr(node, "lineno", None))
684
+ if loop_stack[-1] == "main":
685
+ raise InternalCompilerError("A main-loop break passed flow validation.", line=getattr(node, "lineno", None))
686
+ return [pad + "break;"]
687
+ if isinstance(node, ast.Continue):
688
+ if not loop_stack:
689
+ raise InternalCompilerError("continue reached the backend without a loop target.", line=getattr(node, "lineno", None))
690
+ # The special Python main while is represented by repeated calls to
691
+ # Arduino loop(); finishing this call is Python's next iteration.
692
+ return [pad + ("return;" if loop_stack[-1] == "main" else "continue;")]
693
+ if isinstance(node, ast.If):
694
+ out = [f"{pad}if ({self._expr(node.test)}) {{"]
695
+ child = set(scope)
696
+ for s in node.body:
697
+ out.extend(self._stmt(s, indent + 1, child, p, loop_stack=loop_stack))
698
+ out.append(pad + "}")
699
+ if node.orelse:
700
+ out[-1] += " else {"
701
+ child = set(scope)
702
+ for s in node.orelse:
703
+ out.extend(self._stmt(s, indent + 1, child, p, loop_stack=loop_stack))
704
+ out.append(pad + "}")
705
+ return out
706
+ if isinstance(node, ast.While):
707
+ out = [f"{pad}while ({self._expr(node.test)}) {{"]
708
+ child = set(scope)
709
+ for s in node.body:
710
+ out.extend(self._stmt(s, indent + 1, child, p, loop_stack=(*loop_stack, "nested")))
711
+ out.append(pad + "}")
712
+ return out
713
+ if isinstance(node, ast.For):
714
+ if isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) and node.iter.func.id == "range":
715
+ var = self._resolved_name(node.target)
716
+ start, stop, step = self._parse_range(node.iter, node, p)
717
+ step_value = self._literal_int(step)
718
+ if step_value == 0:
719
+ raise CompileError("range() step cannot be zero.", node.lineno)
720
+ start_temp = self._new_temp("range_start")
721
+ stop_temp = self._new_temp("range_stop")
722
+ step_temp = self._new_temp("range_step")
723
+ iterator = self._new_temp("range_iter")
724
+ out = [
725
+ f"{pad}int32_t {start_temp} = {self._expr(start)};",
726
+ f"{pad}int32_t {stop_temp} = {self._expr(stop)};",
727
+ f"{pad}int32_t {step_temp} = {self._expr(step)};",
728
+ ]
729
+ if step_value is None:
730
+ out.append(f"{pad}if ({step_temp} == 0) _bj_numeric_fail();")
731
+ cond = f"({step_temp} > 0 ? {iterator} < {stop_temp} : {iterator} > {stop_temp})"
732
+ else:
733
+ cond = f"{iterator} {'<' if step_value > 0 else '>'} {stop_temp}"
734
+ out.extend([
735
+ f"{pad}for (int32_t {iterator} = {start_temp}; {cond}; {iterator} = _bj_i32_add({iterator}, {step_temp})) {{",
736
+ f"{pad} {var} = {iterator};",
737
+ ])
738
+ child = set(scope); child.add(var)
739
+ for stmt in node.body:
740
+ out.extend(self._stmt(stmt, indent + 1, child, p, loop_stack=(*loop_stack, "nested")))
741
+ out.append(pad + "}")
742
+ return out
743
+
744
+ enumerate_mode = isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) and node.iter.func.id == "enumerate"
745
+ list_node = node.iter.args[0] if enumerate_mode else node.iter
746
+ if not isinstance(list_node, ast.Name):
747
+ raise InternalCompilerError("Fixed-list loop reached backend without a list name.", line=node.lineno)
748
+ list_type = self._typed.type_for_expr(list_node) if self._typed is not None else None
749
+ if list_type is None or list_type.kind != TypeKind.FIXED_LIST or list_type.length is None:
750
+ raise InternalCompilerError("Fixed-list loop reached backend without a fixed-list type.", line=node.lineno)
751
+ list_name = self._resolved_name(list_node)
752
+ iterator = self._new_temp("list_iter")
753
+ out = [f"{pad}for (int32_t {iterator} = 0; {iterator} < {list_type.length}; ++{iterator}) {{"]
754
+ child = set(scope)
755
+ if enumerate_mode:
756
+ if not isinstance(node.target, (ast.Tuple, ast.List)) or len(node.target.elts) != 2:
757
+ raise InternalCompilerError("enumerate target shape changed after validation.", line=node.lineno)
758
+ idx_target, value_target = node.target.elts
759
+ idx_name = self._resolved_name(idx_target)
760
+ value_name = self._resolved_name(value_target)
761
+ if len(node.iter.args) == 2:
762
+ start_temp = self._new_temp("enumerate_start")
763
+ out.insert(0, f"{pad}int32_t {start_temp} = {self._expr(node.iter.args[1])};")
764
+ out.append(f"{pad} {idx_name} = _bj_i32_add({start_temp}, {iterator});")
765
+ else:
766
+ out.append(f"{pad} {idx_name} = {iterator};")
767
+ out.append(f"{pad} {value_name} = {list_name}[{iterator}];")
768
+ child.update({idx_name, value_name})
769
+ else:
770
+ if not isinstance(node.target, ast.Name):
771
+ raise InternalCompilerError("Fixed-list target shape changed after validation.", line=node.lineno)
772
+ value_name = self._resolved_name(node.target)
773
+ out.append(f"{pad} {value_name} = {list_name}[{iterator}];")
774
+ child.add(value_name)
775
+ for stmt in node.body:
776
+ out.extend(self._stmt(stmt, indent + 1, child, p, loop_stack=(*loop_stack, "nested")))
777
+ out.append(pad + "}")
778
+ return out
779
+ raise CompileError(f"Cannot generate statement {type(node).__name__}.", getattr(node, "lineno", None))
780
+
781
+ def _numeric_helper_lines(self) -> list[str]:
782
+ """Emit checked numeric/Python-semantic helpers only when required."""
783
+ if self._numeric is None or self._typed is None:
784
+ return []
785
+ tree = self._resolved.frontend.tree if self._resolved is not None else None
786
+ need_add = need_sub = need_mul = need_neg = False
787
+ need_div = need_mod_i = need_mod_f = False
788
+ need_pwm = bool(self._numeric.checked_pwm_arguments)
789
+ need_time = bool(self._numeric.checked_time_thresholds)
790
+ need_float_to_int = False
791
+ need_round_i32 = need_round_f32_digits = need_round_i32_digits = False
792
+ need_abs_i = False
793
+ need_string_eq = False
794
+ need_list_index = False
795
+ need_runtime_fail = False
796
+
797
+ if tree is not None:
798
+ for node in ast.walk(tree):
799
+ if isinstance(node, ast.Subscript):
800
+ if self._literal_int(node.slice) is None:
801
+ need_list_index = True
802
+ if isinstance(node, ast.For):
803
+ if isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) and node.iter.func.id == "range":
804
+ need_add = True
805
+ if isinstance(node.iter, ast.Call) and len(node.iter.args) == 3 and self._literal_int(node.iter.args[2]) is None:
806
+ need_runtime_fail = True
807
+ elif isinstance(node, ast.AugAssign):
808
+ try:
809
+ target_type = self._typed.type_for_expr(node.target)
810
+ except KeyError:
811
+ target_type = None
812
+ if isinstance(node.op, ast.Div):
813
+ need_div = True
814
+ elif isinstance(node.op, ast.Mod):
815
+ if target_type is not None and target_type.kind == TypeKind.FLOAT:
816
+ need_mod_f = True
817
+ else:
818
+ need_mod_i = True
819
+ elif target_type is not None and target_type.kind == TypeKind.INT:
820
+ need_add |= isinstance(node.op, ast.Add)
821
+ need_sub |= isinstance(node.op, ast.Sub)
822
+ need_mul |= isinstance(node.op, ast.Mult)
823
+ elif isinstance(node, ast.BinOp):
824
+ if isinstance(node.op, ast.Div):
825
+ need_div = True
826
+ elif isinstance(node.op, ast.Mod):
827
+ try:
828
+ typ = self._typed.type_for_expr(node)
829
+ except KeyError:
830
+ typ = None
831
+ need_mod_f |= typ is not None and typ.kind == TypeKind.FLOAT
832
+ need_mod_i |= typ is None or typ.kind == TypeKind.INT
833
+ elif self._numeric.int_expr_needs_runtime_check(node):
834
+ need_add |= isinstance(node.op, ast.Add)
835
+ need_sub |= isinstance(node.op, ast.Sub)
836
+ need_mul |= isinstance(node.op, ast.Mult)
837
+ elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and self._numeric.int_expr_needs_runtime_check(node):
838
+ need_neg = True
839
+ elif isinstance(node, ast.Compare):
840
+ values = [node.left, *node.comparators]
841
+ for a, b in zip(values, values[1:]):
842
+ try:
843
+ need_string_eq |= self._typed.type_for_expr(a).kind == TypeKind.STRING and self._typed.type_for_expr(b).kind == TypeKind.STRING
844
+ except KeyError:
845
+ pass
846
+ elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
847
+ if node.func.id == "sum" and node.args:
848
+ try:
849
+ list_type = self._typed.type_for_expr(node.args[0])
850
+ need_add |= list_type.kind == TypeKind.FIXED_LIST and list_type.element_type is not None and list_type.element_type.kind == TypeKind.INT
851
+ except KeyError:
852
+ pass
853
+ if node.func.id == "enumerate" and len(node.args) == 2:
854
+ need_add = True
855
+ if node.func.id == "int" and node.args:
856
+ try:
857
+ need_float_to_int |= self._typed.type_for_expr(node.args[0]).kind == TypeKind.FLOAT
858
+ except KeyError:
859
+ pass
860
+ elif node.func.id == "round" and node.args:
861
+ try:
862
+ arg_kind = self._typed.type_for_expr(node.args[0]).kind
863
+ except KeyError:
864
+ arg_kind = None
865
+ if len(node.args) == 1 and arg_kind == TypeKind.FLOAT:
866
+ need_round_i32 = True
867
+ elif len(node.args) == 2 and arg_kind == TypeKind.FLOAT:
868
+ need_round_f32_digits = True
869
+ elif len(node.args) == 2 and arg_kind == TypeKind.INT:
870
+ need_round_i32_digits = True
871
+ elif node.func.id == "abs" and node.args:
872
+ try:
873
+ need_abs_i |= self._typed.type_for_expr(node.args[0]).kind == TypeKind.INT
874
+ except KeyError:
875
+ pass
876
+
877
+ need_runtime_fail |= any((need_add, need_sub, need_mul, need_neg, need_div, need_mod_i, need_mod_f,
878
+ need_pwm, need_time, need_float_to_int, need_round_i32,
879
+ need_round_f32_digits, need_round_i32_digits, need_abs_i, need_list_index))
880
+ if not any((need_runtime_fail, need_string_eq)):
881
+ return []
882
+
883
+ out: list[str] = []
884
+ if need_runtime_fail:
885
+ out += [
886
+ "static void __attribute__((noinline, noreturn)) _bj_numeric_fail() {",
887
+ " for (;;) { __asm__ __volatile__(\"\"); }",
888
+ "}",
889
+ ]
890
+ if need_list_index:
891
+ out += [
892
+ "static int32_t _bj_list_index(int32_t index, int32_t length) {",
893
+ " if (index < 0 || index >= length) _bj_numeric_fail();",
894
+ " return index;",
895
+ "}",
896
+ ]
897
+ if need_add:
898
+ out += [
899
+ "static int32_t _bj_i32_add(int32_t a, int32_t b) {",
900
+ " int64_t r = (int64_t)a + (int64_t)b;",
901
+ " if (r < (-2147483647LL - 1LL) || r > 2147483647LL) _bj_numeric_fail();",
902
+ " return (int32_t)r;",
903
+ "}",
904
+ ]
905
+ if need_sub:
906
+ out += [
907
+ "static int32_t _bj_i32_sub(int32_t a, int32_t b) {",
908
+ " int64_t r = (int64_t)a - (int64_t)b;",
909
+ " if (r < (-2147483647LL - 1LL) || r > 2147483647LL) _bj_numeric_fail();",
910
+ " return (int32_t)r;",
911
+ "}",
912
+ ]
913
+ if need_mul:
914
+ out += [
915
+ "static int32_t _bj_i32_mul(int32_t a, int32_t b) {",
916
+ " int64_t r = (int64_t)a * (int64_t)b;",
917
+ " if (r < (-2147483647LL - 1LL) || r > 2147483647LL) _bj_numeric_fail();",
918
+ " return (int32_t)r;",
919
+ "}",
920
+ ]
921
+ if need_neg:
922
+ out += [
923
+ "static int32_t _bj_i32_neg(int32_t value) {",
924
+ " if (value == (-2147483647L - 1L)) _bj_numeric_fail();",
925
+ " return -value;",
926
+ "}",
927
+ ]
928
+ if need_div:
929
+ out += [
930
+ "static float _bj_true_div_f32(float a, float b) {",
931
+ " if (b == 0.0f) _bj_numeric_fail();",
932
+ " return a / b;",
933
+ "}",
934
+ ]
935
+ if need_mod_i:
936
+ out += [
937
+ "static int32_t _bj_mod_i32(int32_t a, int32_t b) {",
938
+ " if (b == 0) _bj_numeric_fail();",
939
+ " if (a == (-2147483647L - 1L) && b == -1) return 0;",
940
+ " int32_t r = a % b;",
941
+ " if (r != 0 && ((r < 0) != (b < 0))) r += b;",
942
+ " return r;",
943
+ "}",
944
+ ]
945
+ if need_mod_f:
946
+ out += [
947
+ "static float _bj_mod_f32(float a, float b) {",
948
+ " if (isnan(a) || isnan(b) || isinf(a) || isinf(b) || b == 0.0f) _bj_numeric_fail();",
949
+ " float r = fmodf(a, b);",
950
+ " if (r != 0.0f && ((r < 0.0f) != (b < 0.0f))) r += b;",
951
+ " if (r == 0.0f) r = copysignf(0.0f, b);",
952
+ " return r;",
953
+ "}",
954
+ ]
955
+ if need_pwm:
956
+ out += [
957
+ "static int32_t _bj_pwm_value(int32_t value) {",
958
+ " if (value < 0 || value > 255) _bj_numeric_fail();",
959
+ " return value;",
960
+ "}",
961
+ ]
962
+ if need_time:
963
+ out += [
964
+ "static uint32_t _bj_time_threshold(int32_t value) {",
965
+ " if (value < 0) _bj_numeric_fail();",
966
+ " return (uint32_t)value;",
967
+ "}",
968
+ ]
969
+ if need_float_to_int:
970
+ out += [
971
+ "static int32_t _bj_float_to_i32(float value) {",
972
+ " if (isnan(value) || isinf(value) || value < -2147483648.0f || value >= 2147483648.0f) _bj_numeric_fail();",
973
+ " return (int32_t)value;",
974
+ "}",
975
+ ]
976
+ if need_round_i32 or need_round_f32_digits:
977
+ out += [
978
+ "static float _bj_round_even_f32(float value) {",
979
+ " float base = floorf(value);",
980
+ " float frac = value - base;",
981
+ " if (frac > 0.5f) return base + 1.0f;",
982
+ " if (frac < 0.5f) return base;",
983
+ " return (fmodf(fabsf(base), 2.0f) == 0.0f) ? base : (base + 1.0f);",
984
+ "}",
985
+ ]
986
+ if need_round_i32:
987
+ out += [
988
+ "static int32_t _bj_round_i32(float value) {",
989
+ " if (isnan(value) || isinf(value) || value < -2147483648.0f || value >= 2147483648.0f) _bj_numeric_fail();",
990
+ " float rounded = _bj_round_even_f32(value);",
991
+ " if (rounded < -2147483648.0f || rounded >= 2147483648.0f) _bj_numeric_fail();",
992
+ " return (int32_t)rounded;",
993
+ "}",
994
+ ]
995
+ if need_round_f32_digits:
996
+ out += [
997
+ "static uint64_t _bj_round_ratio_even_u64(uint64_t num, uint64_t den) {",
998
+ " uint64_t q = num / den;",
999
+ " uint64_t r = num % den;",
1000
+ " uint64_t twice = r * 2ULL;",
1001
+ " if (twice > den || (twice == den && (q & 1ULL))) ++q;",
1002
+ " return q;",
1003
+ "}",
1004
+ "static uint64_t _bj_round_pow2_even_u64(uint64_t num, uint8_t shift) {",
1005
+ " if (shift == 0) return num;",
1006
+ " if (shift >= 64) return 0;",
1007
+ " uint64_t q = num >> shift;",
1008
+ " uint64_t mask = (1ULL << shift) - 1ULL;",
1009
+ " uint64_t r = num & mask;",
1010
+ " uint64_t half = 1ULL << (shift - 1);",
1011
+ " if (r > half || (r == half && (q & 1ULL))) ++q;",
1012
+ " return q;",
1013
+ "}",
1014
+ "static float _bj_ratio_to_f32(bool negative, uint64_t num, uint32_t den) {",
1015
+ " union { uint32_t u; float f; } out;",
1016
+ " if (num == 0) { out.u = negative ? 0x80000000UL : 0UL; return out.f; }",
1017
+ " int16_t exponent = 0;",
1018
+ " if (num >= den) {",
1019
+ " uint64_t scaled_den = den;",
1020
+ " while (num >= (scaled_den << 1)) { scaled_den <<= 1; ++exponent; }",
1021
+ " } else {",
1022
+ " uint64_t scaled_num = num;",
1023
+ " while (scaled_num < den) { scaled_num <<= 1; --exponent; }",
1024
+ " }",
1025
+ " int16_t shift = 23 - exponent;",
1026
+ " uint64_t scaled_num = num;",
1027
+ " uint64_t scaled_den = den;",
1028
+ " if (shift >= 0) scaled_num <<= shift; else scaled_den <<= -shift;",
1029
+ " uint64_t significand = _bj_round_ratio_even_u64(scaled_num, scaled_den);",
1030
+ " if (significand == (1ULL << 24)) { significand >>= 1; ++exponent; }",
1031
+ " uint32_t exponent_bits = (uint32_t)(exponent + 127);",
1032
+ " if (exponent_bits == 0 || exponent_bits >= 255) _bj_numeric_fail();",
1033
+ " out.u = (negative ? 0x80000000UL : 0UL) | (exponent_bits << 23) | ((uint32_t)significand & 0x7FFFFFUL);",
1034
+ " return out.f;",
1035
+ "}",
1036
+ "static float _bj_round_f32_digits(float value, int8_t ndigits) {",
1037
+ " if (isnan(value) || isinf(value) || value == 0.0f) return value;",
1038
+ " union { float f; uint32_t u; } bits; bits.f = value;",
1039
+ " bool negative = (bits.u & 0x80000000UL) != 0;",
1040
+ " uint32_t exp_bits = (bits.u >> 23) & 0xFFUL;",
1041
+ " uint32_t fraction = bits.u & 0x7FFFFFUL;",
1042
+ " uint64_t mantissa; int16_t exponent2; int16_t unbiased;",
1043
+ " if (exp_bits == 0) { mantissa = fraction; exponent2 = -149; unbiased = -126; }",
1044
+ " else { mantissa = (1UL << 23) | fraction; unbiased = (int16_t)exp_bits - 127; exponent2 = unbiased - 23; }",
1045
+ " static const uint32_t p5[7] = {1UL, 5UL, 25UL, 125UL, 625UL, 3125UL, 15625UL};",
1046
+ " static const uint32_t p10[7] = {1UL, 10UL, 100UL, 1000UL, 10000UL, 100000UL, 1000000UL};",
1047
+ " if (ndigits >= 0) {",
1048
+ " if (unbiased >= 23) return value;",
1049
+ " uint8_t n = (uint8_t)ndigits;",
1050
+ " uint64_t mag = mantissa * (uint64_t)p5[n];",
1051
+ " int16_t shift2 = exponent2 + n;",
1052
+ " uint64_t rounded;",
1053
+ " if (shift2 >= 0) rounded = mag << shift2;",
1054
+ " else rounded = _bj_round_pow2_even_u64(mag, (uint8_t)(-shift2));",
1055
+ " return _bj_ratio_to_f32(negative, rounded, p10[n]);",
1056
+ " }",
1057
+ " uint8_t n = (uint8_t)(-ndigits);",
1058
+ " if (unbiased >= 43) return value;",
1059
+ " int16_t shift2 = exponent2 - n;",
1060
+ " uint64_t rounded;",
1061
+ " if (shift2 >= 0) {",
1062
+ " uint64_t numerator = mantissa << shift2;",
1063
+ " rounded = _bj_round_ratio_even_u64(numerator, p5[n]);",
1064
+ " } else {",
1065
+ " uint8_t down = (uint8_t)(-shift2);",
1066
+ " if (down >= 24) rounded = 0;",
1067
+ " else rounded = _bj_round_ratio_even_u64(mantissa, ((uint64_t)p5[n]) << down);",
1068
+ " }",
1069
+ " return _bj_ratio_to_f32(negative, rounded * (uint64_t)p10[n], 1UL);",
1070
+ "}",
1071
+ ]
1072
+ if need_round_i32_digits:
1073
+ out += [
1074
+ "static int32_t _bj_round_i32_digits(int32_t value, int8_t ndigits) {",
1075
+ " if (ndigits >= 0) return value;",
1076
+ " static const int32_t p10[7] = {1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L};",
1077
+ " int64_t scale = p10[-ndigits];",
1078
+ " int64_t q = ((int64_t)value) / scale;",
1079
+ " int64_t r = ((int64_t)value) % scale;",
1080
+ " int64_t ar = r < 0 ? -r : r;",
1081
+ " int64_t twice = ar * 2LL;",
1082
+ " if (twice > scale || (twice == scale && (q % 2LL) != 0LL)) q += value < 0 ? -1LL : 1LL;",
1083
+ " int64_t result = q * scale;",
1084
+ " if (result < (-2147483647LL - 1LL) || result > 2147483647LL) _bj_numeric_fail();",
1085
+ " return (int32_t)result;",
1086
+ "}",
1087
+ ]
1088
+ if need_abs_i:
1089
+ out += [
1090
+ "static int32_t _bj_i32_abs(int32_t value) {",
1091
+ " if (value == (-2147483647L - 1L)) _bj_numeric_fail();",
1092
+ " return value < 0 ? -value : value;",
1093
+ "}",
1094
+ ]
1095
+ if need_string_eq:
1096
+ out += [
1097
+ "static bool _bj_str_eq(const char* a, const char* b) {",
1098
+ " while (*a && *b && *a == *b) { ++a; ++b; }",
1099
+ " return *a == *b;",
1100
+ "}",
1101
+ ]
1102
+ return out
1103
+
1104
+ def _output_helper_lines(self) -> list[str]:
1105
+ """Compiler-owned Python output helpers, emitted only when required."""
1106
+ if not self._needs_python_float_formatter():
1107
+ return []
1108
+ return [
1109
+ "static void _bj_print_py_float(Print& out, float value) {",
1110
+ " if (isnan(value)) { out.print(F(\"nan\")); return; }",
1111
+ " if (isinf(value)) { out.print(value < 0.0f ? F(\"-inf\") : F(\"inf\")); return; }",
1112
+ " if (value == 0.0f) {",
1113
+ " union { float f; uint32_t u; } bits; bits.f = value;",
1114
+ " out.print((bits.u & 0x80000000UL) ? F(\"-0.0\") : F(\"0.0\"));",
1115
+ " return;",
1116
+ " }",
1117
+ " char scientific[20];",
1118
+ " dtostre(value, scientific, 8, 0); // 9 significant digits: enough to round-trip float32.",
1119
+ " const char* p = scientific;",
1120
+ " char result[32]; char* w = result;",
1121
+ " if (*p == '-') *w++ = *p++;",
1122
+ " char digits[10]; uint8_t count = 0;",
1123
+ " if (*p) digits[count++] = *p++;",
1124
+ " if (*p == '.') ++p;",
1125
+ " while (*p && *p != 'e' && *p != 'E' && count < 9) digits[count++] = *p++;",
1126
+ " while (count > 1 && digits[count - 1] == '0') --count;",
1127
+ " if (*p == 'e' || *p == 'E') ++p;",
1128
+ " bool exp_negative = false;",
1129
+ " if (*p == '+' || *p == '-') { exp_negative = (*p == '-'); ++p; }",
1130
+ " int16_t exponent = 0;",
1131
+ " while (*p >= '0' && *p <= '9') { exponent = (int16_t)(exponent * 10 + (*p++ - '0')); }",
1132
+ " if (exp_negative) exponent = -exponent;",
1133
+ " if (exponent >= -4 && exponent < 16) {",
1134
+ " int16_t decimal_pos = exponent + 1;",
1135
+ " if (decimal_pos <= 0) {",
1136
+ " *w++ = '0'; *w++ = '.';",
1137
+ " for (int16_t i = 0; i < -decimal_pos; ++i) *w++ = '0';",
1138
+ " for (uint8_t i = 0; i < count; ++i) *w++ = digits[i];",
1139
+ " } else {",
1140
+ " for (int16_t i = 0; i < decimal_pos; ++i) *w++ = (i < count) ? digits[i] : '0';",
1141
+ " if (decimal_pos < count) {",
1142
+ " *w++ = '.';",
1143
+ " for (uint8_t i = (uint8_t)decimal_pos; i < count; ++i) *w++ = digits[i];",
1144
+ " } else {",
1145
+ " *w++ = '.'; *w++ = '0';",
1146
+ " }",
1147
+ " }",
1148
+ " } else {",
1149
+ " *w++ = digits[0];",
1150
+ " if (count > 1) { *w++ = '.'; for (uint8_t i = 1; i < count; ++i) *w++ = digits[i]; }",
1151
+ " *w++ = 'e';",
1152
+ " int16_t e = exponent;",
1153
+ " if (e < 0) { *w++ = '-'; e = -e; } else { *w++ = '+'; }",
1154
+ " char expbuf[6]; uint8_t n = 0;",
1155
+ " do { expbuf[n++] = (char)('0' + (e % 10)); e /= 10; } while (e && n < sizeof(expbuf));",
1156
+ " while (n) *w++ = expbuf[--n];",
1157
+ " }",
1158
+ " *w = '\\0';",
1159
+ " out.print(result);",
1160
+ "}",
1161
+ ]
1162
+
1163
+ def _needs_python_float_formatter(self) -> bool:
1164
+ if self._resolved is None or self._typed is None:
1165
+ return False
1166
+
1167
+ def fstring_needs(node: ast.JoinedStr) -> bool:
1168
+ for part in node.values:
1169
+ if not isinstance(part, ast.FormattedValue):
1170
+ continue
1171
+ if part.format_spec is not None:
1172
+ continue
1173
+ try:
1174
+ if self._typed.type_for_expr(part.value).kind == TypeKind.FLOAT:
1175
+ return True
1176
+ except KeyError:
1177
+ pass
1178
+ return False
1179
+
1180
+ for node in ast.walk(self._resolved.frontend.tree):
1181
+ if not isinstance(node, ast.Call):
1182
+ continue
1183
+ is_python_print = isinstance(node.func, ast.Name) and node.func.id == "print"
1184
+ is_output_method = (
1185
+ isinstance(node.func, ast.Attribute)
1186
+ and node.func.attr in {"print", "println"}
1187
+ )
1188
+ if not (is_python_print or is_output_method):
1189
+ continue
1190
+ for arg in node.args:
1191
+ if isinstance(arg, ast.JoinedStr):
1192
+ if fstring_needs(arg):
1193
+ return True
1194
+ elif is_python_print:
1195
+ try:
1196
+ if self._typed.type_for_expr(arg).kind == TypeKind.FLOAT:
1197
+ return True
1198
+ except KeyError:
1199
+ pass
1200
+ return False
1201
+
1202
+ def _new_temp(self, hint: str) -> str:
1203
+ self._temp_counter += 1
1204
+ return f"_bj_tmp_{hint}_{self._temp_counter:04d}"
1205
+
1206
+ def _symbols_with_storage(self, storage: StorageClass) -> list:
1207
+ if self._flow is None:
1208
+ return []
1209
+ symbols = [fact.symbol for fact in self._flow.symbol_facts.values() if fact.storage == storage]
1210
+ return sorted(symbols, key=lambda x: (x.declaration_line or 10**9, x.symbol_id.value))
1211
+
1212
+ def _module_predeclared_cpp_names(self, *storages: StorageClass) -> set[str]:
1213
+ if self._flow is None:
1214
+ return set()
1215
+ accepted = set(storages)
1216
+ return {
1217
+ fact.symbol.cpp_name for fact in self._flow.symbol_facts.values()
1218
+ if fact.symbol.scope_name == "<module>" and fact.storage in accepted
1219
+ }
1220
+
1221
+ def _cpp_type_for_symbol(self, symbol) -> str:
1222
+ if self._typed is None:
1223
+ raise InternalCompilerError("Typed analysis is unavailable during code generation.")
1224
+ return self._typed.type_for_symbol(symbol).cpp_type
1225
+
1226
+ def _declaration_type(self, symbol) -> str:
1227
+ """Formal Step-7 storage type supplied by the type phase."""
1228
+ return self._cpp_type_for_symbol(symbol)
1229
+
1230
+ def _first_assignment_expr(self, symbol):
1231
+ if self._resolved is None:
1232
+ return None
1233
+ found = None
1234
+
1235
+ class Finder(ast.NodeVisitor):
1236
+ def visit_FunctionDef(inner_self, node: ast.FunctionDef):
1237
+ nonlocal found
1238
+ # Search only the owning function for function-local symbols;
1239
+ # skip functions when looking for module storage.
1240
+ if symbol.scope_name == "<module>":
1241
+ return
1242
+ if node.name != symbol.scope_name:
1243
+ return
1244
+ inner_self.generic_visit(node)
1245
+
1246
+ def visit_Assign(inner_self, node: ast.Assign):
1247
+ nonlocal found
1248
+ if found is not None:
1249
+ return
1250
+ for target in node.targets:
1251
+ pairs = [(target, node.value)]
1252
+ if isinstance(target, (ast.Tuple, ast.List)) and isinstance(node.value, (ast.Tuple, ast.List)):
1253
+ pairs = list(zip(target.elts, node.value.elts))
1254
+ for t, value in pairs:
1255
+ if isinstance(t, ast.Name):
1256
+ try:
1257
+ sym = self._resolved.symbol_for_name(t)
1258
+ except KeyError:
1259
+ continue
1260
+ if sym.symbol_id == symbol.symbol_id:
1261
+ found = value
1262
+ return
1263
+ inner_self.generic_visit(node.value)
1264
+
1265
+ def visit_For(inner_self, node: ast.For):
1266
+ nonlocal found
1267
+ if found is None and isinstance(node.target, ast.Name):
1268
+ try:
1269
+ sym = self._resolved.symbol_for_name(node.target)
1270
+ except KeyError:
1271
+ sym = None
1272
+ if sym is not None and sym.symbol_id == symbol.symbol_id:
1273
+ found = ast.Constant(value=0)
1274
+ return
1275
+ inner_self.generic_visit(node)
1276
+
1277
+ Finder().visit(self._resolved.frontend.tree)
1278
+ return found
1279
+
1280
+ def _serial_print(self, call: ast.Call, indent: int) -> list[str]:
1281
+ """Lower Python print() with Python evaluation/formatting semantics."""
1282
+ pad = " " * indent
1283
+ preludes: list[str] = []
1284
+ parts: list[_OutputPart] = []
1285
+ for index, arg in enumerate(call.args):
1286
+ if index:
1287
+ parts.append(_OutputPart(literal=" "))
1288
+ arg_prelude, arg_parts = self._prepare_python_output_arg(arg)
1289
+ preludes.extend(arg_prelude)
1290
+ parts.extend(arg_parts)
1291
+
1292
+ out = [pad + line for line in preludes]
1293
+ for part in parts:
1294
+ out.extend(self._emit_python_output_part("Serial", part, indent, lcd_mode=False))
1295
+ # Python print() always emits exactly one final newline, including print().
1296
+ out.append(pad + "Serial.println();")
1297
+ return out
1298
+
1299
+ def _explicit_output_stmt(self, call: ast.Call, indent: int) -> list[str] | None:
1300
+ """Special output lowering for flash literals and Python f-strings.
1301
+
1302
+ Plain explicit Serial/LCD numeric calls remain Arduino-compatible and
1303
+ therefore fall back to the ordinary API emitter. Only f-strings need
1304
+ Python formatting, while direct string literals can safely use F().
1305
+ """
1306
+ if not isinstance(call.func, ast.Attribute) or not isinstance(call.func.value, ast.Name):
1307
+ return None
1308
+ owner_source = call.func.value.id
1309
+ method = call.func.attr
1310
+ if method not in {"print", "println"}:
1311
+ return None
1312
+ owner = self._expr(call.func.value)
1313
+ lcd_mode = self._objects.get(owner_source) == "LiquidCrystal_I2C"
1314
+ pad = " " * indent
1315
+
1316
+ if len(call.args) == 1 and isinstance(call.args[0], ast.Constant) and isinstance(call.args[0].value, str):
1317
+ literal = self._flash_literal(call.args[0].value, lcd_mode=lcd_mode)
1318
+ return [f"{pad}{owner}.{method}({literal});"]
1319
+
1320
+ if len(call.args) == 1 and isinstance(call.args[0], ast.JoinedStr):
1321
+ preludes, parts = self._prepare_python_output_arg(call.args[0])
1322
+ out = [pad + line for line in preludes]
1323
+ for part in parts:
1324
+ out.extend(self._emit_python_output_part(owner, part, indent, lcd_mode=lcd_mode))
1325
+ if method == "println":
1326
+ out.append(f"{pad}{owner}.println();")
1327
+ return out
1328
+ return None
1329
+
1330
+ def _prepare_python_output_arg(self, node: ast.AST) -> tuple[list[str], list[_OutputPart]]:
1331
+ if self._typed is None:
1332
+ raise InternalCompilerError("Typed analysis is unavailable during output lowering.")
1333
+ if isinstance(node, ast.JoinedStr):
1334
+ preludes: list[str] = []
1335
+ parts: list[_OutputPart] = []
1336
+ for item in node.values:
1337
+ if isinstance(item, ast.Constant) and isinstance(item.value, str):
1338
+ if item.value:
1339
+ parts.append(_OutputPart(literal=item.value))
1340
+ continue
1341
+ if not isinstance(item, ast.FormattedValue):
1342
+ raise InternalCompilerError("Unexpected f-string part reached output lowering.", line=getattr(item, "lineno", None))
1343
+ precision = self._fstring_precision(item)
1344
+ prelude, part = self._prepare_python_output_value(item.value, precision=precision)
1345
+ preludes.extend(prelude)
1346
+ parts.append(part)
1347
+ return preludes, parts
1348
+
1349
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
1350
+ return [], [_OutputPart(literal=node.value)]
1351
+ prelude, part = self._prepare_python_output_value(node, precision=None)
1352
+ return prelude, [part]
1353
+
1354
+ def _prepare_python_output_value(self, node: ast.AST, *, precision: int | None) -> tuple[list[str], _OutputPart]:
1355
+ if self._typed is None:
1356
+ raise InternalCompilerError("Typed analysis is unavailable during output lowering.")
1357
+ typ = self._typed.type_for_expr(node)
1358
+ if typ.kind not in {TypeKind.BOOL, TypeKind.INT, TypeKind.TIME_U32, TypeKind.FLOAT, TypeKind.STRING}:
1359
+ raise InternalCompilerError(
1360
+ f"Non-printable type {typ.kind.value} reached output lowering.",
1361
+ line=getattr(node, "lineno", None),
1362
+ )
1363
+ expr = self._expr(node)
1364
+ # Constants are side-effect free and immutable, so they need no temp.
1365
+ # Everything else is captured before *any* output bytes are emitted.
1366
+ if isinstance(node, ast.Constant):
1367
+ return [], _OutputPart(expression=expr, type_kind=typ.kind, precision=precision)
1368
+ temp = self._new_temp("print_arg")
1369
+ return [f"{typ.cpp_type} {temp} = {expr};"], _OutputPart(
1370
+ expression=temp,
1371
+ type_kind=typ.kind,
1372
+ precision=precision,
1373
+ )
1374
+
1375
+ def _emit_python_output_part(self, sink: str, part: _OutputPart, indent: int, *, lcd_mode: bool) -> list[str]:
1376
+ pad = " " * indent
1377
+ if part.literal is not None:
1378
+ return [f"{pad}{sink}.print({self._flash_literal(part.literal, lcd_mode=lcd_mode)});"]
1379
+ if part.expression is None or part.type_kind is None:
1380
+ raise InternalCompilerError("Malformed Python output part reached backend.")
1381
+ expr = part.expression
1382
+ if part.precision is not None:
1383
+ # Deliberate float cast prevents Arduino Print::print(long, base)
1384
+ # from interpreting .Nf precision as an integer number base.
1385
+ return [f"{pad}{sink}.print((float)({expr}), {part.precision});"]
1386
+ if part.type_kind == TypeKind.BOOL:
1387
+ return [f"{pad}if ({expr}) {sink}.print(F(\"True\")); else {sink}.print(F(\"False\"));"]
1388
+ if part.type_kind == TypeKind.INT:
1389
+ return [f"{pad}{sink}.print((int32_t)({expr}));"]
1390
+ if part.type_kind == TypeKind.TIME_U32:
1391
+ return [f"{pad}{sink}.print((uint32_t)({expr}));"]
1392
+ if part.type_kind == TypeKind.FLOAT:
1393
+ return [f"{pad}_bj_print_py_float({sink}, (float)({expr}));"]
1394
+ if part.type_kind == TypeKind.STRING:
1395
+ return [f"{pad}{sink}.print({expr});"]
1396
+ raise InternalCompilerError("Unsupported Python output type reached backend.")
1397
+
1398
+ @staticmethod
1399
+ def _fstring_precision(node: ast.FormattedValue) -> int | None:
1400
+ if node.format_spec is None:
1401
+ return None
1402
+ if not isinstance(node.format_spec, ast.JoinedStr):
1403
+ raise InternalCompilerError("Validated f-string format changed before backend lowering.")
1404
+ text = "".join(
1405
+ item.value for item in node.format_spec.values
1406
+ if isinstance(item, ast.Constant) and isinstance(item.value, str)
1407
+ )
1408
+ if text.startswith(".") and text.endswith("f") and text[1:-1].isdigit():
1409
+ return int(text[1:-1])
1410
+ raise InternalCompilerError("Unsupported f-string format reached backend after validation.")
1411
+
1412
+ def _flash_literal(self, text: str, *, lcd_mode: bool = False) -> str:
1413
+ return f"F({self._cpp_string_literal(text, lcd_mode=lcd_mode)})"
1414
+
1415
+ @staticmethod
1416
+ def _cpp_string_literal(text: str, *, lcd_mode: bool = False) -> str:
1417
+ data = bytearray()
1418
+ if lcd_mode:
1419
+ for ch in text:
1420
+ code = ord(ch)
1421
+ if 0x20 <= code <= 0x7E:
1422
+ data.append(code)
1423
+ elif ch == "°":
1424
+ data.append(0xDF)
1425
+ else:
1426
+ raise InternalCompilerError("Unsupported LCD character reached backend after target validation.")
1427
+ else:
1428
+ data.extend(text.encode("utf-8"))
1429
+
1430
+ pieces: list[str] = []
1431
+ for byte in data:
1432
+ if byte == 0x5C:
1433
+ pieces.append("\\\\")
1434
+ elif byte == 0x22:
1435
+ pieces.append('\\"')
1436
+ elif byte == 0x0A:
1437
+ pieces.append("\\n")
1438
+ elif byte == 0x0D:
1439
+ pieces.append("\\r")
1440
+ elif byte == 0x09:
1441
+ pieces.append("\\t")
1442
+ elif 0x20 <= byte <= 0x7E:
1443
+ pieces.append(chr(byte))
1444
+ else:
1445
+ # Fixed-width octal escapes cannot consume following hex chars.
1446
+ pieces.append(f"\\{byte:03o}")
1447
+ return '"' + "".join(pieces) + '"'
1448
+
1449
+ # ---------- semantic expression helpers ----------
1450
+ @staticmethod
1451
+ def _expr_has_effects(node: ast.AST) -> bool:
1452
+ return any(isinstance(x, ast.Call) for x in ast.walk(node))
1453
+
1454
+ def _cpp_expr_type(self, node: ast.AST) -> str:
1455
+ if self._typed is None:
1456
+ raise InternalCompilerError("Typed analysis is unavailable during semantic lowering.")
1457
+ return self._typed.type_for_expr(node).cpp_type
1458
+
1459
+ def _ordered_call(self, node: ast.Call, callee: str, args: list[str]) -> str:
1460
+ """Preserve Python left-to-right argument evaluation for effectful args."""
1461
+ if len(node.args) <= 1 or not any(self._expr_has_effects(a) for a in node.args):
1462
+ return f"{callee}({', '.join(args)})"
1463
+ decls: list[str] = []
1464
+ names: list[str] = []
1465
+ for arg_node, arg_expr in zip(node.args, args):
1466
+ name = self._new_temp("arg")
1467
+ names.append(name)
1468
+ decls.append(f"{self._cpp_expr_type(arg_node)} {name} = {arg_expr};")
1469
+ ret = self._typed.type_for_expr(node) if self._typed is not None else None
1470
+ call = f"{callee}({', '.join(names)})"
1471
+ if ret is not None and ret.kind == TypeKind.VOID:
1472
+ return "([&]() { " + " ".join(decls) + f" {call}; }}())"
1473
+ return "([&]() -> " + (ret.cpp_type if ret is not None else "auto") + " { " + " ".join(decls) + f" return {call}; }}())"
1474
+
1475
+ def _binary_expr(self, node: ast.BinOp) -> str:
1476
+ if self._typed is None:
1477
+ raise InternalCompilerError("Typed analysis is unavailable during binary lowering.")
1478
+ typ = self._typed.type_for_expr(node)
1479
+ left_expr = self._expr(node.left)
1480
+ right_expr = self._expr(node.right)
1481
+
1482
+ def operation(left: str, right: str) -> str:
1483
+ if typ.kind == TypeKind.TIME_U32 and isinstance(node.op, ast.Sub):
1484
+ return f"((uint32_t)({left}) - (uint32_t)({right}))"
1485
+ if isinstance(node.op, ast.Div):
1486
+ return f"_bj_true_div_f32((float)({left}), (float)({right}))"
1487
+ if isinstance(node.op, ast.Mod):
1488
+ if typ.kind == TypeKind.FLOAT:
1489
+ return f"_bj_mod_f32((float)({left}), (float)({right}))"
1490
+ return f"_bj_mod_i32({left}, {right})"
1491
+ if typ.kind == TypeKind.INT and self._numeric is not None and self._numeric.int_expr_needs_runtime_check(node):
1492
+ helper = {ast.Add: "_bj_i32_add", ast.Sub: "_bj_i32_sub", ast.Mult: "_bj_i32_mul"}.get(type(node.op))
1493
+ if helper is not None:
1494
+ return f"{helper}({left}, {right})"
1495
+ op = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*"}[type(node.op)]
1496
+ return f"({left} {op} {right})"
1497
+
1498
+ if self._expr_has_effects(node.left) or self._expr_has_effects(node.right):
1499
+ ltmp = self._new_temp("lhs")
1500
+ rtmp = self._new_temp("rhs")
1501
+ ltype = self._cpp_expr_type(node.left)
1502
+ rtype = self._cpp_expr_type(node.right)
1503
+ return (
1504
+ f"([&]() -> {typ.cpp_type} {{ {ltype} {ltmp} = {left_expr}; "
1505
+ f"{rtype} {rtmp} = {right_expr}; return {operation(ltmp, rtmp)}; }}())"
1506
+ )
1507
+ return operation(left_expr, right_expr)
1508
+
1509
+ def _comparison_operand(self, left_node: ast.AST, right_node: ast.AST, opnode: ast.cmpop, left: str, right: str) -> str:
1510
+ if self._typed is None:
1511
+ raise InternalCompilerError("Typed analysis is unavailable during comparison lowering.")
1512
+ lt = self._typed.type_for_expr(left_node)
1513
+ rt = self._typed.type_for_expr(right_node)
1514
+ if lt.kind == TypeKind.STRING and rt.kind == TypeKind.STRING:
1515
+ eq = f"_bj_str_eq({left}, {right})"
1516
+ return eq if isinstance(opnode, ast.Eq) else f"(!{eq})"
1517
+ if self._numeric is not None:
1518
+ if lt.kind == TypeKind.TIME_U32 and rt.kind == TypeKind.INT:
1519
+ right = f"_bj_time_threshold({right})" if self._numeric.time_threshold_needs_runtime_check(right_node) else f"((uint32_t)({right}))"
1520
+ elif rt.kind == TypeKind.TIME_U32 and lt.kind == TypeKind.INT:
1521
+ left = f"_bj_time_threshold({left})" if self._numeric.time_threshold_needs_runtime_check(left_node) else f"((uint32_t)({left}))"
1522
+ op = {ast.Eq: "==", ast.NotEq: "!=", ast.Lt: "<", ast.LtE: "<=", ast.Gt: ">", ast.GtE: ">="}[type(opnode)]
1523
+ return f"({left} {op} {right})"
1524
+
1525
+ def _comparison_expr(self, node: ast.Compare) -> str:
1526
+ # C++ does not guarantee Python's comparison operand evaluation order.
1527
+ # Materialize operands sequentially and stop before evaluating later
1528
+ # operands when an earlier comparison is false.
1529
+ values = [node.left, *node.comparators]
1530
+ temp_names = [self._new_temp("cmp") for _ in values]
1531
+ pieces: list[str] = [f"{self._cpp_expr_type(values[0])} {temp_names[0]} = {self._expr(values[0])};"]
1532
+ for index, (opnode, right_node) in enumerate(zip(node.ops, node.comparators)):
1533
+ pieces.append(f"{self._cpp_expr_type(right_node)} {temp_names[index + 1]} = {self._expr(right_node)};")
1534
+ cmp_expr = self._comparison_operand(values[index], right_node, opnode, temp_names[index], temp_names[index + 1])
1535
+ pieces.append(f"if (!({cmp_expr})) return false;")
1536
+ pieces.append("return true;")
1537
+ return "([&]() -> bool { " + " ".join(pieces) + " }())"
1538
+
1539
+ def _minmax_expr(self, node: ast.Call, *, want_min: bool) -> str:
1540
+ if self._typed is None:
1541
+ raise InternalCompilerError("Typed analysis is unavailable during min/max lowering.")
1542
+ result_type = self._typed.type_for_expr(node).cpp_type
1543
+ temp_names: list[str] = []
1544
+ parts: list[str] = []
1545
+ for arg in node.args:
1546
+ name = self._new_temp("minmax_arg")
1547
+ temp_names.append(name)
1548
+ parts.append(f"{self._cpp_expr_type(arg)} {name} = {self._expr(arg)};")
1549
+ result = self._new_temp("minmax")
1550
+ parts.append(f"{result_type} {result} = {temp_names[0]};")
1551
+ op = "<" if want_min else ">"
1552
+ for name in temp_names[1:]:
1553
+ parts.append(f"if ({name} {op} {result}) {result} = {name};")
1554
+ parts.append(f"return {result};")
1555
+ return f"([&]() -> {result_type} {{ " + " ".join(parts) + " }())"
1556
+
1557
+ # ---------- expressions ----------
1558
+ def _expr(self, node: ast.AST) -> str:
1559
+ if isinstance(node, ast.Constant):
1560
+ if node.value is True: return "true"
1561
+ if node.value is False: return "false"
1562
+ if node.value is None: raise CompileError("None is not supported in Arduino expressions.", node.lineno)
1563
+ if isinstance(node.value, str):
1564
+ return self._cpp_string_literal(node.value)
1565
+ return repr(node.value)
1566
+ if isinstance(node, ast.Name):
1567
+ return self._resolved_name(node)
1568
+ if isinstance(node, ast.Call):
1569
+ if isinstance(node.func, ast.Attribute):
1570
+ owner = self._expr(node.func.value)
1571
+ arg_values = [self._expr(a) for a in node.args]
1572
+ return self._ordered_call(node, f"{owner}.{node.func.attr}", arg_values)
1573
+ if not isinstance(node.func, ast.Name):
1574
+ raise CompileError("Unsupported call form.", node.lineno)
1575
+ name = node.func.id
1576
+ args = [self._expr(a) for a in node.args]
1577
+ if name == "round":
1578
+ arg_type = self._typed.type_for_expr(node.args[0]) if self._typed is not None else None
1579
+ if len(args) == 1:
1580
+ if arg_type is not None and arg_type.kind == TypeKind.INT:
1581
+ return args[0]
1582
+ return f"_bj_round_i32((float)({args[0]}))"
1583
+ ndigits = self._literal_int(node.args[1])
1584
+ if ndigits is None:
1585
+ raise InternalCompilerError("round() ndigits reached code generation without a compile-time value.", line=node.lineno)
1586
+ if arg_type is not None and arg_type.kind == TypeKind.INT:
1587
+ return f"_bj_round_i32_digits({args[0]}, {ndigits})"
1588
+ return f"_bj_round_f32_digits((float)({args[0]}), {ndigits})"
1589
+ if name == "min":
1590
+ return self._minmax_expr(node, want_min=True)
1591
+ if name == "max":
1592
+ return self._minmax_expr(node, want_min=False)
1593
+ if name == "int":
1594
+ if self._typed is not None and node.args:
1595
+ arg_type = self._typed.type_for_expr(node.args[0])
1596
+ if arg_type.kind == TypeKind.FLOAT:
1597
+ return f"_bj_float_to_i32((float)({args[0]}))"
1598
+ return f"((int32_t)({args[0]}))"
1599
+ if name == "float": return f"((float)({args[0]}))"
1600
+ if name == "bool": return f"((bool)({args[0]}))"
1601
+ if name == "abs" and self._typed is not None and node.args:
1602
+ if self._typed.type_for_expr(node.args[0]).kind == TypeKind.INT:
1603
+ return f"_bj_i32_abs({args[0]})"
1604
+ return f"fabsf((float)({args[0]}))"
1605
+ if name == "len" and node.args and self._typed is not None:
1606
+ list_type = self._typed.type_for_expr(node.args[0])
1607
+ if list_type.kind != TypeKind.FIXED_LIST or list_type.length is None:
1608
+ raise InternalCompilerError("len() reached backend without fixed-list type.", line=node.lineno)
1609
+ return str(list_type.length)
1610
+ if name == "sum" and node.args and self._typed is not None:
1611
+ arg = node.args[0]
1612
+ if not isinstance(arg, ast.Name):
1613
+ raise InternalCompilerError("sum() reached backend without a fixed-list name.", line=node.lineno)
1614
+ list_type = self._typed.type_for_expr(arg)
1615
+ if list_type.kind != TypeKind.FIXED_LIST or list_type.length is None or list_type.element_type is None:
1616
+ raise InternalCompilerError("sum() reached backend without fixed-list type.", line=node.lineno)
1617
+ list_name = self._resolved_name(arg)
1618
+ idx = self._new_temp("sum_i")
1619
+ acc = self._new_temp("sum_acc")
1620
+ if list_type.element_type.kind == TypeKind.INT:
1621
+ return f"([&]() -> int32_t {{ int32_t {acc} = 0; for (int32_t {idx} = 0; {idx} < {list_type.length}; ++{idx}) {acc} = _bj_i32_add({acc}, {list_name}[{idx}]); return {acc}; }}())"
1622
+ return f"([&]() -> float {{ float {acc} = 0.0f; for (int32_t {idx} = 0; {idx} < {list_type.length}; ++{idx}) {acc} += {list_name}[{idx}]; return {acc}; }}())"
1623
+ if name == "analogWrite" and len(node.args) >= 2 and self._numeric is not None:
1624
+ if self._numeric.pwm_arg_needs_runtime_check(node.args[1]):
1625
+ args[1] = f"_bj_pwm_value({args[1]})"
1626
+ callee = self._resolved_name(node.func)
1627
+ return self._ordered_call(node, callee, args)
1628
+ if isinstance(node, ast.UnaryOp):
1629
+ operand = self._expr(node.operand)
1630
+ if isinstance(node.op, ast.USub):
1631
+ if isinstance(node.operand, ast.Constant) and node.operand.value == 2**31:
1632
+ return "(-2147483647L - 1L)"
1633
+ if self._numeric is not None and self._numeric.int_expr_needs_runtime_check(node):
1634
+ return f"_bj_i32_neg({operand})"
1635
+ return f"(-{operand})"
1636
+ return f"(!{operand})"
1637
+ if isinstance(node, ast.BinOp):
1638
+ return self._binary_expr(node)
1639
+ if isinstance(node, ast.Compare):
1640
+ return self._comparison_expr(node)
1641
+ if isinstance(node, ast.BoolOp):
1642
+ op = " && " if isinstance(node.op, ast.And) else " || "
1643
+ return "(" + op.join(self._expr(v) for v in node.values) + ")"
1644
+ if isinstance(node, ast.IfExp):
1645
+ return f"({self._expr(node.test)} ? {self._expr(node.body)} : {self._expr(node.orelse)})"
1646
+ if isinstance(node, ast.List):
1647
+ return "{" + ", ".join(self._expr(x) for x in node.elts) + "}"
1648
+ if isinstance(node, ast.Subscript):
1649
+ return self._subscript_lvalue(node)
1650
+ raise CompileError(f"Cannot generate expression {type(node).__name__}.", getattr(node, "lineno", None))
1651
+
1652
+ def _declaration_line(self, symbol) -> str:
1653
+ if self._typed is None:
1654
+ raise InternalCompilerError("Typed analysis is unavailable during declaration generation.")
1655
+ typ = self._typed.type_for_symbol(symbol)
1656
+ if typ.kind == TypeKind.FIXED_LIST:
1657
+ if typ.element_type is None or typ.length is None:
1658
+ raise InternalCompilerError("Incomplete fixed-list type reached declaration generation.")
1659
+ return f"{typ.element_type.cpp_type} {symbol.cpp_name}[{typ.length}];"
1660
+ return f"{typ.cpp_type} {symbol.cpp_name};"
1661
+
1662
+ def _subscript_lvalue(self, node: ast.Subscript) -> str:
1663
+ if self._typed is None:
1664
+ raise InternalCompilerError("Typed analysis is unavailable during list indexing.")
1665
+ if not isinstance(node.value, ast.Name):
1666
+ raise InternalCompilerError("V1 fixed-list indexing reached backend without a list name.", line=getattr(node, "lineno", None))
1667
+ list_type = self._typed.type_for_expr(node.value)
1668
+ if list_type.kind != TypeKind.FIXED_LIST or list_type.length is None:
1669
+ raise InternalCompilerError("Indexed value is not a fixed list.", line=getattr(node, "lineno", None))
1670
+ name = self._resolved_name(node.value)
1671
+ literal = self._literal_int(node.slice)
1672
+ if literal is not None:
1673
+ return f"{name}[{literal}]"
1674
+ return f"{name}[_bj_list_index({self._expr(node.slice)}, {list_type.length})]"
1675
+
1676
+ def _infer_expr_type(self, node: ast.AST) -> str:
1677
+ if self._typed is None:
1678
+ raise InternalCompilerError("Typed analysis is unavailable during expression generation.")
1679
+ try:
1680
+ return self._typed.type_for_expr(node).cpp_type
1681
+ except KeyError:
1682
+ raise InternalCompilerError(
1683
+ "An expression reached code generation without a resolved type.",
1684
+ line=getattr(node, "lineno", None),
1685
+ details=type(node).__name__,
1686
+ ) from None
1687
+
1688
+ # ---------- helpers ----------
1689
+ @staticmethod
1690
+ def _literal_python_value(value: ast.AST):
1691
+ if isinstance(value, ast.Constant): return value.value
1692
+ if isinstance(value, ast.UnaryOp) and isinstance(value.op, ast.USub) and isinstance(value.operand, ast.Constant):
1693
+ return -value.operand.value
1694
+ return None
1695
+
1696
+ def _literal_pin(self, node: ast.AST) -> int | None:
1697
+ if isinstance(node, ast.Constant) and isinstance(node.value, int): return node.value
1698
+ if isinstance(node, ast.Name):
1699
+ if node.id in self.board.analog_aliases: return self.board.analog_aliases[node.id]
1700
+ v = self._global_values.get(node.id)
1701
+ return v if isinstance(v, int) else None
1702
+ return None
1703
+
1704
+ def _parse_range(self, node: ast.AST, owner: ast.AST, p: Program):
1705
+ if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name) or node.func.id != "range":
1706
+ raise CompileError("V1 for-loops use 'for x in range(...)'.", getattr(owner, "lineno", None))
1707
+ if node.keywords or not (1 <= len(node.args) <= 3):
1708
+ raise CompileError("range() accepts 1 to 3 positional values.", getattr(owner, "lineno", None))
1709
+ zero, one = ast.Constant(value=0), ast.Constant(value=1)
1710
+ if len(node.args) == 1: start, stop, step = zero, node.args[0], one
1711
+ elif len(node.args) == 2: start, stop, step = node.args[0], node.args[1], one
1712
+ else: start, stop, step = node.args
1713
+ for expr in (start, stop, step): self._validate_expr(expr, p)
1714
+ return start, stop, step
1715
+
1716
+ @staticmethod
1717
+ def _literal_int(node: ast.AST) -> int | None:
1718
+ if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool): return node.value
1719
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Constant) and isinstance(node.operand.value, int):
1720
+ return -node.operand.value
1721
+ return None