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,754 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+
7
+ from .errors import CompileError, NameResolutionError, UnsupportedFeatureError
8
+ from .frontend import is_noop_stmt
9
+ from .symbols import ResolvedModule, Symbol, SymbolId, SymbolKind
10
+
11
+
12
+ class StorageClass(Enum):
13
+ MODULE_CONSTANT = "module_constant"
14
+ PERSISTENT_MODULE = "persistent_module"
15
+ SETUP_LOCAL = "setup_local"
16
+ LOOP_TEMPORARY = "loop_temporary"
17
+ HARDWARE_PERSISTENT = "hardware_persistent"
18
+ FUNCTION_LOCAL = "function_local"
19
+ PARAMETER = "parameter"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CFGNode:
24
+ node_id: int
25
+ kind: str
26
+ line: int | None
27
+ successors: tuple[int, ...] = ()
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ControlFlowGraph:
32
+ name: str
33
+ entry_id: int
34
+ exit_id: int
35
+ nodes: tuple[CFGNode, ...]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class SymbolFlowFacts:
40
+ symbol: Symbol
41
+ storage: StorageClass
42
+ read_regions: frozenset[str] = frozenset()
43
+ write_regions: frozenset[str] = frozenset()
44
+ assignment_count: int = 0
45
+ loop_reads_before_write: bool = False
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class FunctionFlowFacts:
50
+ name: str
51
+ definitely_assigned_at_exit: frozenset[SymbolId]
52
+ has_value_return: bool
53
+ has_bare_return: bool
54
+ can_fall_through: bool
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class FlowAnalyzedModule:
59
+ resolved: ResolvedModule
60
+ setup_graph: ControlFlowGraph
61
+ loop_graph: ControlFlowGraph
62
+ function_graphs: dict[str, ControlFlowGraph]
63
+ symbol_facts: dict[SymbolId, SymbolFlowFacts]
64
+ function_facts: dict[str, FunctionFlowFacts]
65
+ setup_exit_assigned: frozenset[SymbolId]
66
+ loop_exit_assigned: frozenset[SymbolId]
67
+
68
+ def facts_for(self, symbol: Symbol) -> SymbolFlowFacts:
69
+ return self.symbol_facts[symbol.symbol_id]
70
+
71
+ def storage_for(self, symbol: Symbol) -> StorageClass:
72
+ return self.facts_for(symbol).storage
73
+
74
+
75
+ @dataclass
76
+ class _Outcome:
77
+ normal: set[SymbolId] | None
78
+ breaks: list[set[SymbolId]] = field(default_factory=list)
79
+ continues: list[set[SymbolId]] = field(default_factory=list)
80
+ value_returns: int = 0
81
+ bare_returns: int = 0
82
+
83
+ def merge_transfers(self, other: "_Outcome") -> None:
84
+ self.breaks.extend(other.breaks)
85
+ self.continues.extend(other.continues)
86
+ self.value_returns += other.value_returns
87
+ self.bare_returns += other.bare_returns
88
+
89
+
90
+ class _CFGBuilder:
91
+ """Small explicit CFG snapshot for diagnostics/tests.
92
+
93
+ Definite-assignment itself is a structured fixed-point-safe analysis below;
94
+ this graph records the same region's control structure without pretending
95
+ that C++ scope is Python scope.
96
+ """
97
+
98
+ def __init__(self, name: str) -> None:
99
+ self.name = name
100
+ self._next = 0
101
+ self._nodes: dict[int, tuple[str, int | None, list[int]]] = {}
102
+ self.entry = self._new("entry", None)
103
+ self.exit = self._new("exit", None)
104
+
105
+ def _new(self, kind: str, line: int | None) -> int:
106
+ nid = self._next
107
+ self._next += 1
108
+ self._nodes[nid] = (kind, line, [])
109
+ return nid
110
+
111
+ def _edge(self, source: int, target: int) -> None:
112
+ succ = self._nodes[source][2]
113
+ if target not in succ:
114
+ succ.append(target)
115
+
116
+ def build(self, body: list[ast.stmt]) -> ControlFlowGraph:
117
+ tails = [self.entry]
118
+ tails = self._block(body, tails)
119
+ for tail in tails:
120
+ self._edge(tail, self.exit)
121
+ nodes = tuple(
122
+ CFGNode(nid, kind, line, tuple(succ))
123
+ for nid, (kind, line, succ) in sorted(self._nodes.items())
124
+ )
125
+ return ControlFlowGraph(self.name, self.entry, self.exit, nodes)
126
+
127
+ def _block(self, body: list[ast.stmt], incoming: list[int]) -> list[int]:
128
+ tails = incoming
129
+ for stmt in body:
130
+ if not tails:
131
+ break
132
+ nid = self._new(type(stmt).__name__, getattr(stmt, "lineno", None))
133
+ for tail in tails:
134
+ self._edge(tail, nid)
135
+ if isinstance(stmt, ast.If):
136
+ yes = self._block(stmt.body, [nid]) or [nid]
137
+ no = self._block(stmt.orelse, [nid]) if stmt.orelse else [nid]
138
+ tails = yes + no
139
+ elif isinstance(stmt, (ast.While, ast.For)):
140
+ body_tails = self._block(stmt.body, [nid])
141
+ for tail in body_tails:
142
+ self._edge(tail, nid)
143
+ tails = [nid]
144
+ elif isinstance(stmt, (ast.Return, ast.Break, ast.Continue)):
145
+ tails = []
146
+ else:
147
+ tails = [nid]
148
+ return tails
149
+
150
+
151
+ class FlowAnalyzer:
152
+ def __init__(self, resolved: ResolvedModule) -> None:
153
+ self.resolved = resolved
154
+ self.frontend = resolved.frontend
155
+ self._reads: dict[SymbolId, set[str]] = {}
156
+ self._writes: dict[SymbolId, set[str]] = {}
157
+ self._assign_count: dict[SymbolId, int] = {}
158
+ self._loop_reads_before_write: set[SymbolId] = set()
159
+
160
+ def analyze(self) -> FlowAnalyzedModule:
161
+ setup_body = self._setup_runtime_body()
162
+ loop_body = list(self.frontend.main_loop.body)
163
+
164
+ setup_graph = _CFGBuilder("setup").build(setup_body)
165
+ loop_graph = _CFGBuilder("main_loop").build(loop_body)
166
+ function_graphs = {
167
+ fn.name: _CFGBuilder(f"function:{fn.name}").build(fn.body)
168
+ for fn in self.frontend.tree.body if isinstance(fn, ast.FunctionDef)
169
+ }
170
+
171
+ # Module execution begins with no user variable value available. API,
172
+ # builtins and function definitions are compile-time bindings, not data
173
+ # values tracked in the definite-assignment set.
174
+ setup_out = self._analyze_block(setup_body, set(), "setup", loop_stack=[])
175
+ if setup_out.normal is None:
176
+ # Setup-time return is structurally impossible; an infinite setup
177
+ # loop is valid Python but leaves no path to Arduino loop().
178
+ setup_exit: set[SymbolId] = set()
179
+ else:
180
+ setup_exit = set(setup_out.normal)
181
+
182
+ # The special top-level while True is represented by Arduino loop().
183
+ # Its first logical iteration starts with values established by setup.
184
+ loop_out = self._analyze_block(loop_body, set(setup_exit), "loop", loop_stack=["main"])
185
+ loop_exit = set(loop_out.normal or setup_exit)
186
+ if loop_out.breaks:
187
+ # Flow analysis should already reject a break targeting main.
188
+ raise CompileError("The main Arduino loop cannot be terminated with break.")
189
+
190
+ function_facts: dict[str, FunctionFlowFacts] = {}
191
+ for fn in self.frontend.tree.body:
192
+ if not isinstance(fn, ast.FunctionDef):
193
+ continue
194
+ scope = self.resolved.function_scopes[fn.name]
195
+ initial = {
196
+ sym.symbol_id for sym in scope.symbols.values()
197
+ if sym.kind == SymbolKind.PARAMETER
198
+ }
199
+ out = self._analyze_block(fn.body, initial, f"function:{fn.name}", loop_stack=[])
200
+ has_value = out.value_returns > 0
201
+ has_bare = out.bare_returns > 0
202
+ can_fall = out.normal is not None
203
+ if has_value and has_bare:
204
+ raise CompileError(
205
+ f"Function '{fn.name}' mixes 'return value' and bare 'return'. Use one return style.",
206
+ fn.lineno,
207
+ )
208
+ if has_value and can_fall:
209
+ raise CompileError(
210
+ f"Function '{fn.name}' returns a value on some paths but can finish without returning a value.",
211
+ fn.lineno,
212
+ )
213
+ function_facts[fn.name] = FunctionFlowFacts(
214
+ fn.name,
215
+ frozenset(out.normal or set()),
216
+ has_value,
217
+ has_bare,
218
+ can_fall,
219
+ )
220
+
221
+ self._mark_loop_reads_before_write(loop_body)
222
+ facts = self._classify_storage(setup_exit)
223
+
224
+ return FlowAnalyzedModule(
225
+ resolved=self.resolved,
226
+ setup_graph=setup_graph,
227
+ loop_graph=loop_graph,
228
+ function_graphs=function_graphs,
229
+ symbol_facts=facts,
230
+ function_facts=function_facts,
231
+ setup_exit_assigned=frozenset(setup_exit),
232
+ loop_exit_assigned=frozenset(loop_exit),
233
+ )
234
+
235
+ # ---------- source regions ----------
236
+ def _setup_runtime_body(self) -> list[ast.stmt]:
237
+ body: list[ast.stmt] = []
238
+ for stmt in self.frontend.tree.body:
239
+ if isinstance(stmt, (ast.ImportFrom, ast.FunctionDef)):
240
+ continue
241
+ if stmt is self.frontend.main_loop:
242
+ break
243
+ if is_noop_stmt(stmt, docstring_allowed=True):
244
+ continue
245
+ body.append(stmt)
246
+ return body
247
+
248
+ # ---------- definite assignment ----------
249
+ def _analyze_block(
250
+ self,
251
+ body: list[ast.stmt],
252
+ assigned: set[SymbolId],
253
+ region: str,
254
+ *,
255
+ loop_stack: list[str],
256
+ ) -> _Outcome:
257
+ normal: set[SymbolId] | None = set(assigned)
258
+ result = _Outcome(normal)
259
+ for stmt in body:
260
+ if normal is None:
261
+ # Statements after return/break/continue/infinite loops never run,
262
+ # and Python accepts them. They are still emitted as C++, so their
263
+ # variable accesses must still decide storage (otherwise a variable
264
+ # used only there gets no declaration in the right C++ scope).
265
+ self._record_unreachable_accesses(stmt, region)
266
+ continue
267
+ out = self._analyze_stmt(stmt, normal, region, loop_stack=loop_stack)
268
+ result.breaks.extend(out.breaks)
269
+ result.continues.extend(out.continues)
270
+ result.value_returns += out.value_returns
271
+ result.bare_returns += out.bare_returns
272
+ normal = out.normal
273
+ result.normal = normal
274
+ return result
275
+
276
+ def _analyze_stmt(
277
+ self,
278
+ stmt: ast.stmt,
279
+ assigned: set[SymbolId],
280
+ region: str,
281
+ *,
282
+ loop_stack: list[str],
283
+ ) -> _Outcome:
284
+ if is_noop_stmt(stmt, docstring_allowed=True) or isinstance(stmt, ast.Global):
285
+ return _Outcome(set(assigned))
286
+
287
+ if isinstance(stmt, ast.Expr):
288
+ self._check_expr(stmt.value, assigned, region)
289
+ return _Outcome(set(assigned))
290
+
291
+ if isinstance(stmt, ast.Assign):
292
+ self._check_expr(stmt.value, assigned, region)
293
+ out = set(assigned)
294
+ for target in stmt.targets:
295
+ if isinstance(target, ast.Subscript):
296
+ # Element mutation reads the existing list and index; it
297
+ # does not create/rebind the list variable itself.
298
+ self._check_expr(target.value, assigned, region)
299
+ self._check_expr(target.slice, assigned, region)
300
+ if isinstance(target.value, ast.Name):
301
+ symbol = self.resolved.symbol_for_name(target.value)
302
+ self._record_write(symbol, region)
303
+ self._assign_count[symbol.symbol_id] = self._assign_count.get(symbol.symbol_id, 0) + 1
304
+ continue
305
+ self._assign_target(target, out, region)
306
+ return _Outcome(out)
307
+
308
+ if isinstance(stmt, ast.AugAssign):
309
+ self._require_target_assigned(stmt.target, assigned, region)
310
+ self._check_expr(stmt.value, assigned, region)
311
+ out = set(assigned)
312
+ self._assign_target(stmt.target, out, region)
313
+ return _Outcome(out)
314
+
315
+ if isinstance(stmt, ast.If):
316
+ self._check_expr(stmt.test, assigned, region)
317
+ yes = self._analyze_block(stmt.body, set(assigned), region, loop_stack=loop_stack)
318
+ no = self._analyze_block(stmt.orelse, set(assigned), region, loop_stack=loop_stack) if stmt.orelse else _Outcome(set(assigned))
319
+ normals = [x.normal for x in (yes, no) if x.normal is not None]
320
+ normal = self._intersection(normals) if normals else None
321
+ out = _Outcome(normal)
322
+ out.merge_transfers(yes)
323
+ out.merge_transfers(no)
324
+ return out
325
+
326
+ if isinstance(stmt, ast.For):
327
+ self._check_expr(stmt.iter, assigned, region)
328
+ targets = self._symbols_for_loop_target(stmt.target)
329
+ body_in = set(assigned)
330
+ for target_symbol in targets:
331
+ body_in.add(target_symbol.symbol_id)
332
+ self._record_write(target_symbol, region)
333
+ body = self._analyze_block(stmt.body, body_in, region, loop_stack=[*loop_stack, "nested"])
334
+ guaranteed_nonempty = self._for_iter_is_statically_nonempty(stmt.iter)
335
+ normal = set(assigned)
336
+ if guaranteed_nonempty:
337
+ normal.update(sym.symbol_id for sym in targets)
338
+ out = _Outcome(normal)
339
+ out.value_returns = body.value_returns
340
+ out.bare_returns = body.bare_returns
341
+ return out
342
+
343
+ if isinstance(stmt, ast.While):
344
+ self._check_expr(stmt.test, assigned, region)
345
+ body = self._analyze_block(stmt.body, set(assigned), region, loop_stack=[*loop_stack, "nested"])
346
+ if self._is_true(stmt.test):
347
+ # A normal infinite while has no fallthrough unless a nested
348
+ # break targets it. break states are the only normal exits.
349
+ normal = self._intersection(body.breaks) if body.breaks else None
350
+ else:
351
+ normal = set(assigned)
352
+ if body.breaks:
353
+ normal &= self._intersection(body.breaks)
354
+ out = _Outcome(normal)
355
+ out.value_returns = body.value_returns
356
+ out.bare_returns = body.bare_returns
357
+ return out
358
+
359
+ if isinstance(stmt, ast.Break):
360
+ if not loop_stack:
361
+ raise CompileError("break may only be used inside a loop.", stmt.lineno)
362
+ if loop_stack[-1] == "main":
363
+ raise UnsupportedFeatureError(
364
+ "break cannot target the top-level Arduino 'while True:' loop because Arduino loop() is called repeatedly.",
365
+ line=stmt.lineno,
366
+ column=(getattr(stmt, "col_offset", 0) + 1),
367
+ )
368
+ return _Outcome(None, breaks=[set(assigned)])
369
+
370
+ if isinstance(stmt, ast.Continue):
371
+ if not loop_stack:
372
+ raise CompileError("continue may only be used inside a loop.", stmt.lineno)
373
+ return _Outcome(None, continues=[set(assigned)])
374
+
375
+ if isinstance(stmt, ast.Return):
376
+ if stmt.value is not None:
377
+ self._check_expr(stmt.value, assigned, region)
378
+ return _Outcome(None, value_returns=1)
379
+ return _Outcome(None, bare_returns=1)
380
+
381
+ raise CompileError(
382
+ f"Control-flow analysis does not support '{type(stmt).__name__}' yet.",
383
+ getattr(stmt, "lineno", None),
384
+ )
385
+
386
+ def _check_expr(self, expr: ast.AST, assigned: set[SymbolId], region: str) -> None:
387
+ for node in ast.walk(expr):
388
+ if not isinstance(node, ast.Name) or not isinstance(node.ctx, ast.Load):
389
+ continue
390
+ try:
391
+ symbol = self.resolved.symbol_for_name(node)
392
+ except KeyError:
393
+ continue
394
+ self._record_read(symbol, region)
395
+ if symbol.kind in {SymbolKind.API, SymbolKind.BUILTIN, SymbolKind.FUNCTION}:
396
+ continue
397
+ if symbol.kind == SymbolKind.PARAMETER:
398
+ if symbol.symbol_id not in assigned:
399
+ self._unbound(symbol, node, local=True)
400
+ continue
401
+ if symbol.kind == SymbolKind.LOCAL:
402
+ if symbol.symbol_id not in assigned:
403
+ self._unbound(symbol, node, local=True)
404
+ continue
405
+ # Module values are tracked during setup/main flow. Inside a user
406
+ # function, Step-5 call-order checks establish module availability;
407
+ # function-local assignment remains the concern here.
408
+ if region.startswith("function:"):
409
+ continue
410
+ if symbol.symbol_id not in assigned:
411
+ self._unbound(symbol, node, local=False)
412
+
413
+ def _require_target_assigned(self, target: ast.AST, assigned: set[SymbolId], region: str) -> None:
414
+ if isinstance(target, ast.Name):
415
+ symbol = self.resolved.symbol_for_name(target)
416
+ self._record_read(symbol, region)
417
+ if region.startswith("function:") and symbol.scope_name == "<module>":
418
+ return
419
+ if symbol.symbol_id not in assigned and symbol.kind not in {SymbolKind.API, SymbolKind.BUILTIN, SymbolKind.FUNCTION}:
420
+ self._unbound(symbol, target, local=symbol.kind in {SymbolKind.LOCAL, SymbolKind.PARAMETER})
421
+
422
+ def _assign_target(self, target: ast.AST, assigned: set[SymbolId], region: str) -> None:
423
+ if isinstance(target, ast.Name):
424
+ symbol = self.resolved.symbol_for_name(target)
425
+ assigned.add(symbol.symbol_id)
426
+ self._record_write(symbol, region)
427
+ return
428
+ if isinstance(target, (ast.Tuple, ast.List)):
429
+ for elt in target.elts:
430
+ self._assign_target(elt, assigned, region)
431
+
432
+ def _symbols_for_loop_target(self, target: ast.AST) -> list[Symbol]:
433
+ if isinstance(target, ast.Name):
434
+ return [self.resolved.symbol_for_name(target)]
435
+ if isinstance(target, (ast.Tuple, ast.List)):
436
+ result: list[Symbol] = []
437
+ for elt in target.elts:
438
+ if not isinstance(elt, ast.Name):
439
+ raise CompileError("Loop target must use simple names.", getattr(target, "lineno", None))
440
+ result.append(self.resolved.symbol_for_name(elt))
441
+ return result
442
+ raise CompileError("Loop target must use simple names.", getattr(target, "lineno", None))
443
+
444
+ def _unbound(self, symbol: Symbol, node: ast.AST, *, local: bool) -> None:
445
+ wording = "local variable" if local else "variable"
446
+ raise NameResolutionError(
447
+ f"Possibly unbound {wording} '{symbol.name}' may be read before it has a value.",
448
+ line=getattr(node, "lineno", None),
449
+ column=(getattr(node, "col_offset", 0) + 1),
450
+ end_line=getattr(node, "end_lineno", None),
451
+ end_column=getattr(node, "end_col_offset", None),
452
+ )
453
+
454
+ @staticmethod
455
+ def _intersection(sets: list[set[SymbolId]]) -> set[SymbolId]:
456
+ if not sets:
457
+ return set()
458
+ out = set(sets[0])
459
+ for item in sets[1:]:
460
+ out &= item
461
+ return out
462
+
463
+ def _record_unreachable_accesses(self, stmt: ast.stmt, region: str) -> None:
464
+ for node in ast.walk(stmt):
465
+ if isinstance(node, ast.Name):
466
+ try:
467
+ symbol = self.resolved.symbol_for_name(node)
468
+ except KeyError:
469
+ continue
470
+ if isinstance(node.ctx, ast.Store):
471
+ self._record_write(symbol, region)
472
+ else:
473
+ self._record_read(symbol, region)
474
+ elif isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name):
475
+ try:
476
+ self._record_read(self.resolved.symbol_for_name(node.target), region)
477
+ except KeyError:
478
+ pass
479
+
480
+ # ---------- access facts / storage ----------
481
+ def _record_read(self, symbol: Symbol, region: str) -> None:
482
+ if symbol.is_user_symbol:
483
+ self._reads.setdefault(symbol.symbol_id, set()).add(region)
484
+
485
+ def _record_write(self, symbol: Symbol, region: str) -> None:
486
+ if symbol.is_user_symbol:
487
+ self._writes.setdefault(symbol.symbol_id, set()).add(region)
488
+ self._assign_count[symbol.symbol_id] = self._assign_count.get(symbol.symbol_id, 0) + 1
489
+
490
+ def _mark_loop_reads_before_write(self, body: list[ast.stmt]) -> None:
491
+ # Per-symbol definite assignment from the start of a logical main-loop
492
+ # iteration, intentionally ignoring setup values. A read before a
493
+ # current-iteration write means the previous value can be observed.
494
+ for symbol in self.resolved.module_scope.symbols.values():
495
+ if symbol.kind not in {SymbolKind.MODULE, SymbolKind.HARDWARE}:
496
+ continue
497
+ if symbol.kind == SymbolKind.HARDWARE:
498
+ continue
499
+ if self._reads_before_write_block(body, symbol.symbol_id, False)[1]:
500
+ self._loop_reads_before_write.add(symbol.symbol_id)
501
+
502
+ def _reads_before_write_block(self, body: list[ast.stmt], sid: SymbolId, assigned: bool) -> tuple[bool, bool]:
503
+ read_before = False
504
+ current = assigned
505
+ for stmt in body:
506
+ if isinstance(stmt, ast.Assign):
507
+ if self._expr_reads_symbol(stmt.value, sid) and not current:
508
+ read_before = True
509
+ if any(self._target_is_symbol(t, sid) for t in stmt.targets):
510
+ current = True
511
+ elif isinstance(stmt, ast.AugAssign):
512
+ if self._target_is_symbol(stmt.target, sid) and not current:
513
+ read_before = True
514
+ if self._expr_reads_symbol(stmt.value, sid) and not current:
515
+ read_before = True
516
+ if self._target_is_symbol(stmt.target, sid):
517
+ current = True
518
+ elif isinstance(stmt, ast.If):
519
+ if self._expr_reads_symbol(stmt.test, sid) and not current:
520
+ read_before = True
521
+ y_assigned, y_read = self._reads_before_write_block(stmt.body, sid, current)
522
+ n_assigned, n_read = self._reads_before_write_block(stmt.orelse, sid, current) if stmt.orelse else (current, False)
523
+ read_before |= y_read or n_read
524
+ current = y_assigned and n_assigned
525
+ elif isinstance(stmt, ast.For):
526
+ if self._expr_reads_symbol(stmt.iter, sid) and not current:
527
+ read_before = True
528
+ target_here = self._target_is_symbol(stmt.target, sid)
529
+ body_assigned, body_read = self._reads_before_write_block(stmt.body, sid, current or target_here)
530
+ read_before |= body_read
531
+ if target_here and self._range_is_statically_nonempty(stmt.iter):
532
+ current = True
533
+ elif isinstance(stmt, ast.While):
534
+ if self._expr_reads_symbol(stmt.test, sid) and not current:
535
+ read_before = True
536
+ _, body_read = self._reads_before_write_block(stmt.body, sid, current)
537
+ read_before |= body_read
538
+ else:
539
+ for node in ast.walk(stmt):
540
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
541
+ try:
542
+ sym = self.resolved.symbol_for_name(node)
543
+ except KeyError:
544
+ continue
545
+ if sym.symbol_id == sid and not current:
546
+ read_before = True
547
+ return current, read_before
548
+
549
+ def _expr_reads_symbol(self, expr: ast.AST, sid: SymbolId) -> bool:
550
+ for node in ast.walk(expr):
551
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
552
+ try:
553
+ if self.resolved.symbol_for_name(node).symbol_id == sid:
554
+ return True
555
+ except KeyError:
556
+ pass
557
+ return False
558
+
559
+ def _target_is_symbol(self, target: ast.AST, sid: SymbolId) -> bool:
560
+ if isinstance(target, ast.Name):
561
+ try:
562
+ return self.resolved.symbol_for_name(target).symbol_id == sid
563
+ except KeyError:
564
+ return False
565
+ if isinstance(target, (ast.Tuple, ast.List)):
566
+ return any(self._target_is_symbol(x, sid) for x in target.elts)
567
+ return False
568
+
569
+ def _classify_storage(self, setup_exit: set[SymbolId]) -> dict[SymbolId, SymbolFlowFacts]:
570
+ facts: dict[SymbolId, SymbolFlowFacts] = {}
571
+ assignment_sites = self._module_assignment_sites()
572
+ function_regions = {
573
+ region for regions in (*self._reads.values(), *self._writes.values())
574
+ for region in regions if region.startswith("function:")
575
+ }
576
+ _ = function_regions # documentary: function access is handled per-symbol below.
577
+
578
+ for symbol in self.resolved.module_scope.symbols.values():
579
+ if not symbol.is_user_symbol:
580
+ continue
581
+ reads = frozenset(self._reads.get(symbol.symbol_id, set()))
582
+ writes = frozenset(self._writes.get(symbol.symbol_id, set()))
583
+ count = self._assign_count.get(symbol.symbol_id, 0)
584
+ rbw = symbol.symbol_id in self._loop_reads_before_write
585
+
586
+ if symbol.kind == SymbolKind.FUNCTION:
587
+ # Functions have code identity, not variable storage.
588
+ continue
589
+ if symbol.kind == SymbolKind.HARDWARE:
590
+ storage = StorageClass.HARDWARE_PERSISTENT
591
+ else:
592
+ sites = assignment_sites.get(symbol.symbol_id, [])
593
+ one_pure = len(sites) == 1 and self._is_pure_compile_time_expr(sites[0][1])
594
+ mutated_in_function = any(r.startswith("function:") for r in writes)
595
+ if one_pure and not mutated_in_function:
596
+ storage = StorageClass.MODULE_CONSTANT
597
+ elif any(r.startswith("function:") for r in reads | writes):
598
+ storage = StorageClass.PERSISTENT_MODULE
599
+ elif "loop" in reads or "loop" in writes:
600
+ if "setup" in writes or rbw:
601
+ storage = StorageClass.PERSISTENT_MODULE
602
+ else:
603
+ storage = StorageClass.LOOP_TEMPORARY
604
+ else:
605
+ storage = StorageClass.SETUP_LOCAL
606
+
607
+ facts[symbol.symbol_id] = SymbolFlowFacts(
608
+ symbol=symbol,
609
+ storage=storage,
610
+ read_regions=reads,
611
+ write_regions=writes,
612
+ assignment_count=count,
613
+ loop_reads_before_write=rbw,
614
+ )
615
+
616
+ for scope in self.resolved.function_scopes.values():
617
+ for symbol in scope.symbols.values():
618
+ storage = StorageClass.PARAMETER if symbol.kind == SymbolKind.PARAMETER else StorageClass.FUNCTION_LOCAL
619
+ facts[symbol.symbol_id] = SymbolFlowFacts(
620
+ symbol=symbol,
621
+ storage=storage,
622
+ read_regions=frozenset(self._reads.get(symbol.symbol_id, set())),
623
+ write_regions=frozenset(self._writes.get(symbol.symbol_id, set())),
624
+ assignment_count=self._assign_count.get(symbol.symbol_id, 0),
625
+ )
626
+ return facts
627
+
628
+ def _module_assignment_sites(self) -> dict[SymbolId, list[tuple[ast.AST, ast.AST]]]:
629
+ sites: dict[SymbolId, list[tuple[ast.AST, ast.AST]]] = {}
630
+ analyzer = self
631
+
632
+ class Visitor(ast.NodeVisitor):
633
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
634
+ return
635
+
636
+ def visit_Assign(self, node: ast.Assign) -> None:
637
+ for target in node.targets:
638
+ if isinstance(target, ast.Subscript) and isinstance(target.value, ast.Name):
639
+ try:
640
+ sym = analyzer.resolved.symbol_for_name(target.value)
641
+ except KeyError:
642
+ sym = None
643
+ if sym is not None and sym.scope_name == "<module>":
644
+ sites.setdefault(sym.symbol_id, []).append((target.value, node.value))
645
+ self.visit(target.slice)
646
+ continue
647
+ for t, value in analyzer._target_value_pairs(target, node.value):
648
+ if not isinstance(t, ast.Name):
649
+ continue
650
+ try:
651
+ sym = analyzer.resolved.symbol_for_name(t)
652
+ except KeyError:
653
+ continue
654
+ if sym.scope_name == "<module>":
655
+ sites.setdefault(sym.symbol_id, []).append((t, value))
656
+ self.generic_visit(node.value)
657
+
658
+ def visit_AugAssign(self, node: ast.AugAssign) -> None:
659
+ if isinstance(node.target, ast.Name):
660
+ try:
661
+ sym = analyzer.resolved.symbol_for_name(node.target)
662
+ except KeyError:
663
+ sym = None
664
+ if sym is not None and sym.scope_name == "<module>":
665
+ sites.setdefault(sym.symbol_id, []).append((node.target, node.value))
666
+ self.generic_visit(node.value)
667
+
668
+ def visit_For(self, node: ast.For) -> None:
669
+ # Every loop target (including enumerate tuple targets) is a
670
+ # write site; missing one would misclassify the name as a constant.
671
+ targets = [node.target] if isinstance(node.target, ast.Name) else [
672
+ elt for elt in getattr(node.target, "elts", ()) if isinstance(elt, ast.Name)
673
+ ]
674
+ for target in targets:
675
+ try:
676
+ sym = analyzer.resolved.symbol_for_name(target)
677
+ except KeyError:
678
+ sym = None
679
+ if sym is not None and sym.scope_name == "<module>":
680
+ sites.setdefault(sym.symbol_id, []).append((target, node.iter))
681
+ self.visit(node.iter)
682
+ for child in node.body:
683
+ self.visit(child)
684
+
685
+ Visitor().visit(self.frontend.tree)
686
+ return sites
687
+
688
+ @staticmethod
689
+ def _target_value_pairs(target: ast.AST, value: ast.AST) -> list[tuple[ast.AST, ast.AST]]:
690
+ if isinstance(target, ast.Name):
691
+ return [(target, value)]
692
+ if isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (ast.Tuple, ast.List)):
693
+ return list(zip(target.elts, value.elts))
694
+ return []
695
+
696
+ @staticmethod
697
+ def _is_pure_compile_time_expr(node: ast.AST) -> bool:
698
+ if isinstance(node, ast.Constant):
699
+ return isinstance(node.value, (bool, int, float, str))
700
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
701
+ return FlowAnalyzer._is_pure_compile_time_expr(node.operand)
702
+ if isinstance(node, ast.List) and node.elts:
703
+ return all(FlowAnalyzer._is_pure_compile_time_expr(item) for item in node.elts)
704
+ return False
705
+
706
+ @staticmethod
707
+ def _is_true(node: ast.AST) -> bool:
708
+ return isinstance(node, ast.Constant) and node.value is True
709
+
710
+ @staticmethod
711
+ def _literal_int(node: ast.AST) -> int | None:
712
+ if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool):
713
+ return int(node.value)
714
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
715
+ value = FlowAnalyzer._literal_int(node.operand)
716
+ return -value if value is not None else None
717
+ return None
718
+
719
+ @classmethod
720
+ def _for_iter_is_statically_nonempty(cls, node: ast.AST) -> bool:
721
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "range":
722
+ return cls._range_is_statically_nonempty(node)
723
+ # Fixed-list literals are non-empty by frontend rule. A named fixed list
724
+ # also has a compile-time positive length, even though the type is not
725
+ # available in this phase; V1 never permits an empty fixed list.
726
+ if isinstance(node, (ast.Name, ast.List)):
727
+ return True
728
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "enumerate":
729
+ return bool(node.args) and isinstance(node.args[0], (ast.Name, ast.List))
730
+ return False
731
+
732
+ @classmethod
733
+ def _range_is_statically_nonempty(cls, node: ast.AST) -> bool:
734
+ if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name) or node.func.id != "range":
735
+ return False
736
+ vals = [cls._literal_int(arg) for arg in node.args]
737
+ if any(v is None for v in vals):
738
+ return False
739
+ nums = [int(v) for v in vals if v is not None]
740
+ if len(nums) == 1:
741
+ start, stop, step = 0, nums[0], 1
742
+ elif len(nums) == 2:
743
+ start, stop, step = nums[0], nums[1], 1
744
+ elif len(nums) == 3:
745
+ start, stop, step = nums
746
+ else:
747
+ return False
748
+ if step == 0:
749
+ return False
750
+ return (step > 0 and start < stop) or (step < 0 and start > stop)
751
+
752
+
753
+ def analyze_flow(resolved: ResolvedModule) -> FlowAnalyzedModule:
754
+ return FlowAnalyzer(resolved).analyze()