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,917 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+
7
+ from .errors import TypeCheckError, UnsupportedFeatureError
8
+ from .flow import FlowAnalyzedModule
9
+ from .symbols import ResolvedModule, Symbol, SymbolId, SymbolKind
10
+ from .api_registry import CORE_FUNCTIONS, HARDWARE_CLASSES, SINGLETONS, SignatureSpec, matching_signatures
11
+
12
+
13
+ class TypeKind(Enum):
14
+ UNKNOWN = "Unknown"
15
+ BOOL = "Bool"
16
+ INT = "Int"
17
+ FLOAT = "Float"
18
+ STRING = "String"
19
+ VOID = "Void"
20
+ TIME_U32 = "TimeU32"
21
+ HARDWARE = "Hardware"
22
+ FIXED_LIST = "FixedList"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class BarmajaType:
27
+ kind: TypeKind
28
+ hardware_class: str | None = None
29
+ element_type: "BarmajaType | None" = None
30
+ length: int | None = None
31
+
32
+ @property
33
+ def display_name(self) -> str:
34
+ if self.kind == TypeKind.HARDWARE:
35
+ return f"Hardware[{self.hardware_class}]"
36
+ if self.kind == TypeKind.FIXED_LIST:
37
+ inner = self.element_type.display_name if self.element_type is not None else "?"
38
+ return f"FixedList[{inner},{self.length}]"
39
+ return self.kind.value
40
+
41
+ @property
42
+ def cpp_type(self) -> str:
43
+ mapping = {
44
+ TypeKind.BOOL: "bool",
45
+ TypeKind.INT: "int32_t",
46
+ TypeKind.FLOAT: "float",
47
+ TypeKind.STRING: "const char*",
48
+ TypeKind.VOID: "void",
49
+ TypeKind.TIME_U32: "uint32_t",
50
+ }
51
+ if self.kind == TypeKind.HARDWARE:
52
+ return self.hardware_class or "/*hardware*/"
53
+ if self.kind == TypeKind.FIXED_LIST:
54
+ if self.element_type is None or self.length is None:
55
+ raise ValueError("Incomplete FixedList type")
56
+ return f"{self.element_type.cpp_type}[{self.length}]"
57
+ if self.kind == TypeKind.UNKNOWN:
58
+ raise ValueError("Unknown type has no C++ representation")
59
+ return mapping[self.kind]
60
+
61
+
62
+ UNKNOWN = BarmajaType(TypeKind.UNKNOWN)
63
+ BOOL = BarmajaType(TypeKind.BOOL)
64
+ INT = BarmajaType(TypeKind.INT)
65
+ FLOAT = BarmajaType(TypeKind.FLOAT)
66
+ STRING = BarmajaType(TypeKind.STRING)
67
+ VOID = BarmajaType(TypeKind.VOID)
68
+ TIME_U32 = BarmajaType(TypeKind.TIME_U32)
69
+
70
+
71
+ def hardware_type(name: str) -> BarmajaType:
72
+ return BarmajaType(TypeKind.HARDWARE, name)
73
+
74
+
75
+ def fixed_list_type(element_type: BarmajaType, length: int) -> BarmajaType:
76
+ return BarmajaType(TypeKind.FIXED_LIST, element_type=element_type, length=length)
77
+
78
+
79
+ NUMERIC_KINDS = {TypeKind.INT, TypeKind.FLOAT}
80
+ SCALAR_KINDS = {TypeKind.BOOL, TypeKind.INT, TypeKind.FLOAT, TypeKind.STRING, TypeKind.TIME_U32}
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class FunctionSignature:
85
+ name: str
86
+ symbol: Symbol
87
+ parameters: tuple[Symbol, ...]
88
+ parameter_types: tuple[BarmajaType, ...]
89
+ return_type: BarmajaType
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class TypedModule:
94
+ flow: FlowAnalyzedModule
95
+ symbol_types: dict[SymbolId, BarmajaType]
96
+ expression_types: dict[int, BarmajaType]
97
+ function_signatures: dict[str, FunctionSignature]
98
+
99
+ @property
100
+ def resolved(self) -> ResolvedModule:
101
+ return self.flow.resolved
102
+
103
+ def type_for_symbol(self, symbol: Symbol) -> BarmajaType:
104
+ return self.symbol_types[symbol.symbol_id]
105
+
106
+ def type_for_expr(self, node: ast.AST) -> BarmajaType:
107
+ return self.expression_types[id(node)]
108
+
109
+ def signature_for(self, name: str) -> FunctionSignature:
110
+ return self.function_signatures[name]
111
+
112
+
113
+ class TypeAnalyzer:
114
+ """Step-7 fixed-point scalar/function type inference.
115
+
116
+ This phase owns language types. The backend receives resolved Barmaja
117
+ types and must not infer C++ storage types from AST spelling/shape.
118
+ Numeric overflow/range proofs are intentionally Step 8.
119
+ """
120
+
121
+ def __init__(self, flow: FlowAnalyzedModule) -> None:
122
+ self.flow = flow
123
+ self.resolved = flow.resolved
124
+ self.frontend = self.resolved.frontend
125
+ self.symbol_types: dict[SymbolId, BarmajaType] = {}
126
+ self.expression_types: dict[int, BarmajaType] = {}
127
+ self._function_nodes: dict[str, ast.FunctionDef] = {
128
+ node.name: node for node in self.frontend.tree.body if isinstance(node, ast.FunctionDef)
129
+ }
130
+ self._param_symbols: dict[str, list[Symbol]] = {}
131
+ self._return_types: dict[str, BarmajaType] = {}
132
+ self._hardware_classes: dict[SymbolId, str] = {}
133
+ self._changed = False
134
+
135
+ def analyze(self) -> TypedModule:
136
+ self._reject_recursion()
137
+ self._seed_symbols()
138
+
139
+ # Calls and return expressions can constrain one another across source
140
+ # order. Iterate to a fixed point instead of using function order.
141
+ for _ in range(64):
142
+ self._changed = False
143
+ self.expression_types.clear()
144
+ self._scan_module()
145
+ if not self._changed:
146
+ break
147
+ else:
148
+ raise TypeCheckError("Function/type inference did not converge.")
149
+
150
+ # One strict pass after convergence rejects unresolved/invalid forms.
151
+ self.expression_types.clear()
152
+ self._scan_module(strict=True)
153
+ self._require_resolved_user_symbols()
154
+
155
+ signatures: dict[str, FunctionSignature] = {}
156
+ for name, fn in self._function_nodes.items():
157
+ fn_symbol = self.resolved.symbol_for_function(fn)
158
+ params = tuple(self._param_symbols[name])
159
+ signatures[name] = FunctionSignature(
160
+ name=name,
161
+ symbol=fn_symbol,
162
+ parameters=params,
163
+ parameter_types=tuple(self.symbol_types[p.symbol_id] for p in params),
164
+ return_type=self._return_types[name],
165
+ )
166
+
167
+ return TypedModule(
168
+ flow=self.flow,
169
+ symbol_types=dict(self.symbol_types),
170
+ expression_types=dict(self.expression_types),
171
+ function_signatures=signatures,
172
+ )
173
+
174
+ def _reject_recursion(self) -> None:
175
+ names = set(self._function_nodes)
176
+ graph: dict[str, set[str]] = {name: set() for name in names}
177
+ for name, fn in self._function_nodes.items():
178
+ for node in ast.walk(fn):
179
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in names:
180
+ graph[name].add(node.func.id)
181
+
182
+ visiting: set[str] = set()
183
+ visited: set[str] = set()
184
+
185
+ def visit(name: str, path: list[str]) -> None:
186
+ if name in visiting:
187
+ start = path.index(name) if name in path else 0
188
+ cycle = path[start:] + [name]
189
+ fn = self._function_nodes[name]
190
+ raise UnsupportedFeatureError(
191
+ "Recursive function calls are not supported on Arduino Uno in Barmaja PyArduino V1: "
192
+ + " -> ".join(cycle),
193
+ line=getattr(fn, "lineno", None),
194
+ )
195
+ if name in visited:
196
+ return
197
+ visiting.add(name)
198
+ path.append(name)
199
+ for other in sorted(graph[name]):
200
+ visit(other, path)
201
+ path.pop()
202
+ visiting.remove(name)
203
+ visited.add(name)
204
+
205
+ for name in sorted(graph):
206
+ visit(name, [])
207
+
208
+ def _seed_symbols(self) -> None:
209
+ # Every user data symbol starts unknown; functions are represented by
210
+ # signatures rather than scalar symbol values.
211
+ for symbol in self.resolved.module_scope.symbols.values():
212
+ if symbol.kind in {SymbolKind.MODULE, SymbolKind.HARDWARE}:
213
+ self.symbol_types[symbol.symbol_id] = UNKNOWN
214
+ for name, scope in self.resolved.function_scopes.items():
215
+ params: list[Symbol] = []
216
+ for symbol in scope.symbols.values():
217
+ if symbol.kind in {SymbolKind.PARAMETER, SymbolKind.LOCAL}:
218
+ self.symbol_types[symbol.symbol_id] = UNKNOWN
219
+ if symbol.kind == SymbolKind.PARAMETER:
220
+ params.append(symbol)
221
+ # Preserve Python parameter order rather than dict assumptions.
222
+ fn = self._function_nodes[name]
223
+ ordered: list[Symbol] = []
224
+ for arg in fn.args.args:
225
+ ordered.append(self.resolved.symbol_for_parameter(arg))
226
+ self._param_symbols[name] = ordered
227
+ self._return_types[name] = VOID if not self.flow.function_facts[name].has_value_return else UNKNOWN
228
+
229
+ # Hardware object constructor assignments are a direct type source.
230
+ for stmt in self.frontend.tree.body:
231
+ if self._is_hardware_assignment(stmt):
232
+ target = stmt.targets[0]
233
+ sym = self.resolved.symbol_for_name(target)
234
+ cls = stmt.value.func.id
235
+ typ = hardware_type(cls)
236
+ self.symbol_types[sym.symbol_id] = typ
237
+ self._hardware_classes[sym.symbol_id] = cls
238
+
239
+ @staticmethod
240
+ def _is_hardware_assignment(node: ast.AST) -> bool:
241
+ return (
242
+ isinstance(node, ast.Assign)
243
+ and len(node.targets) == 1
244
+ and isinstance(node.targets[0], ast.Name)
245
+ and isinstance(node.value, ast.Call)
246
+ and isinstance(node.value.func, ast.Name)
247
+ and node.value.func.id in HARDWARE_CLASSES
248
+ )
249
+
250
+ def _scan_module(self, strict: bool = False) -> None:
251
+ for stmt in self.frontend.tree.body:
252
+ if isinstance(stmt, (ast.ImportFrom, ast.FunctionDef)):
253
+ continue
254
+ if self._is_hardware_assignment(stmt):
255
+ # Still type-check constructor arguments.
256
+ self._expr_type(stmt.value, strict=strict, value_required=True)
257
+ continue
258
+ if isinstance(stmt, ast.While) and isinstance(stmt.test, ast.Constant) and stmt.test.value is True:
259
+ self._scan_block(stmt.body, strict=strict, function_name=None)
260
+ continue
261
+ self._scan_stmt(stmt, strict=strict, function_name=None)
262
+
263
+ for name, fn in self._function_nodes.items():
264
+ self._scan_block(fn.body, strict=strict, function_name=name)
265
+
266
+ def _scan_block(self, body: list[ast.stmt], *, strict: bool, function_name: str | None) -> None:
267
+ for stmt in body:
268
+ self._scan_stmt(stmt, strict=strict, function_name=function_name)
269
+
270
+ def _scan_stmt(self, stmt: ast.stmt, *, strict: bool, function_name: str | None) -> None:
271
+ if isinstance(stmt, (ast.Pass, ast.Global)):
272
+ return
273
+ if isinstance(stmt, ast.Expr):
274
+ if isinstance(stmt.value, ast.Constant) and (stmt.value.value is Ellipsis or isinstance(stmt.value.value, str)):
275
+ return
276
+ self._expr_type(stmt.value, strict=strict, value_required=False)
277
+ return
278
+ if isinstance(stmt, ast.Assign):
279
+ # Supported tuple/list unpacking is pair-wise and fixed at compile
280
+ # time. Type each element directly instead of inventing a general
281
+ # Tuple runtime type.
282
+ if (
283
+ len(stmt.targets) == 1
284
+ and isinstance(stmt.targets[0], (ast.Tuple, ast.List))
285
+ and isinstance(stmt.value, (ast.Tuple, ast.List))
286
+ ):
287
+ for target, value in zip(stmt.targets[0].elts, stmt.value.elts):
288
+ value_type = self._expr_type(value, strict=strict, value_required=True)
289
+ self._assign_target(target, value_type, stmt, strict)
290
+ return
291
+ value_type = self._expr_type(stmt.value, strict=strict, value_required=True)
292
+ if value_type.kind == TypeKind.VOID:
293
+ raise self._error("A Void call cannot be assigned to a variable.", stmt.value)
294
+ if strict and value_type.kind == TypeKind.FIXED_LIST and not isinstance(stmt.value, ast.List):
295
+ # Python would make both names share ONE list object; a fixed Uno array
296
+ # cannot alias another array, so a copy would silently change behaviour.
297
+ raise self._error(
298
+ "A list variable can only be given a list literal such as [1, 2, 3] in Barmaja PyArduino V1. "
299
+ "Assigning one list to another name (sharing or copying a list) is not supported; "
300
+ "copy the elements one by one instead.",
301
+ stmt.value,
302
+ )
303
+ for target in stmt.targets:
304
+ if isinstance(target, ast.Subscript):
305
+ list_type = self._expr_type(target.value, strict=strict, value_required=True)
306
+ index_type = self._expr_type(target.slice, strict=strict, value_required=True)
307
+ self._require_kind(index_type, {TypeKind.INT}, target.slice, "List index must be Int.", strict)
308
+ if strict and list_type.kind != TypeKind.FIXED_LIST:
309
+ raise self._error("Indexed assignment requires a fixed list.", target.value)
310
+ if list_type.kind == TypeKind.FIXED_LIST and list_type.element_type is not None:
311
+ joined = self._join(list_type.element_type, value_type, stmt.value, strict)
312
+ if (
313
+ strict
314
+ and value_type.kind != TypeKind.UNKNOWN
315
+ and joined != list_type.element_type
316
+ ):
317
+ # A fixed list keeps one element type. The only implicit
318
+ # element conversion is the lossless Int -> Float store; a
319
+ # Float value must never be silently truncated into an Int list.
320
+ raise self._error(
321
+ f"Cannot store a {value_type.display_name} value in a list of "
322
+ f"{list_type.element_type.display_name} values; fixed lists keep one element type. "
323
+ "Use int(...) or round(...) explicitly, or make the list hold Float values such as [1.0, 2.0].",
324
+ stmt.value,
325
+ )
326
+ self._validate_static_index(target.slice, list_type.length or 0, strict)
327
+ continue
328
+ self._assign_target(target, value_type, stmt, strict)
329
+ return
330
+ if isinstance(stmt, ast.AugAssign):
331
+ left = self._expr_type(stmt.target, strict=strict, value_required=True)
332
+ right = self._expr_type(stmt.value, strict=strict, value_required=True)
333
+ result = self._numeric_binop_type(left, right, stmt, division=isinstance(stmt.op, ast.Div), strict=strict)
334
+ sym = self.resolved.symbol_for_name(stmt.target)
335
+ self._merge_symbol(sym, result, stmt, strict)
336
+ return
337
+ if isinstance(stmt, ast.If):
338
+ cond = self._expr_type(stmt.test, strict=strict, value_required=True)
339
+ self._require_bool(cond, stmt.test, strict)
340
+ self._scan_block(stmt.body, strict=strict, function_name=function_name)
341
+ self._scan_block(stmt.orelse, strict=strict, function_name=function_name)
342
+ return
343
+ if isinstance(stmt, ast.While):
344
+ cond = self._expr_type(stmt.test, strict=strict, value_required=True)
345
+ self._require_bool(cond, stmt.test, strict)
346
+ self._scan_block(stmt.body, strict=strict, function_name=function_name)
347
+ return
348
+ if isinstance(stmt, ast.For):
349
+ iterator_type = self._expr_type(stmt.iter, strict=strict, value_required=False)
350
+ if isinstance(stmt.iter, ast.Call) and isinstance(stmt.iter.func, ast.Name) and stmt.iter.func.id == "range":
351
+ if isinstance(stmt.target, ast.Name):
352
+ self._merge_symbol(self.resolved.symbol_for_name(stmt.target), INT, stmt.target, strict)
353
+ self.expression_types[id(stmt.target)] = INT
354
+ elif isinstance(stmt.iter, ast.Call) and isinstance(stmt.iter.func, ast.Name) and stmt.iter.func.id == "enumerate":
355
+ if stmt.iter.args:
356
+ list_type = self._expr_type(stmt.iter.args[0], strict=strict, value_required=True)
357
+ if strict and list_type.kind != TypeKind.FIXED_LIST:
358
+ raise self._error("enumerate() requires a fixed list.", stmt.iter.args[0])
359
+ if len(stmt.iter.args) == 2:
360
+ start_type = self._expr_type(stmt.iter.args[1], strict=strict, value_required=True)
361
+ self._require_kind(start_type, {TypeKind.INT}, stmt.iter.args[1], "enumerate() start must be Int.", strict)
362
+ if list_type.kind == TypeKind.FIXED_LIST and isinstance(stmt.target, (ast.Tuple, ast.List)) and len(stmt.target.elts) == 2:
363
+ first, second = stmt.target.elts
364
+ if isinstance(first, ast.Name):
365
+ self._merge_symbol(self.resolved.symbol_for_name(first), INT, first, strict)
366
+ self.expression_types[id(first)] = INT
367
+ if isinstance(second, ast.Name) and list_type.element_type is not None:
368
+ self._merge_symbol(self.resolved.symbol_for_name(second), list_type.element_type, second, strict)
369
+ self.expression_types[id(second)] = list_type.element_type
370
+ else:
371
+ if strict and iterator_type.kind != TypeKind.FIXED_LIST:
372
+ raise self._error("for iteration requires range() or a fixed list in V1.", stmt.iter)
373
+ if iterator_type.kind == TypeKind.FIXED_LIST and isinstance(stmt.target, ast.Name) and iterator_type.element_type is not None:
374
+ self._merge_symbol(self.resolved.symbol_for_name(stmt.target), iterator_type.element_type, stmt.target, strict)
375
+ self.expression_types[id(stmt.target)] = iterator_type.element_type
376
+ self._scan_block(stmt.body, strict=strict, function_name=function_name)
377
+ return
378
+ if isinstance(stmt, ast.Return):
379
+ if function_name is None:
380
+ return
381
+ typ = VOID if stmt.value is None else self._expr_type(stmt.value, strict=strict, value_required=True)
382
+ if typ.kind == TypeKind.VOID and stmt.value is not None:
383
+ raise self._error("A function cannot return a Void call as a value.", stmt.value)
384
+ self._merge_return(function_name, typ, stmt, strict)
385
+ return
386
+ if isinstance(stmt, (ast.Break, ast.Continue)):
387
+ return
388
+
389
+ def _assign_target(self, target: ast.AST, value_type: BarmajaType, owner: ast.AST, strict: bool) -> None:
390
+ if isinstance(target, ast.Name):
391
+ sym = self.resolved.symbol_for_name(target)
392
+ self._merge_symbol(sym, value_type, owner, strict)
393
+ self.expression_types[id(target)] = self.symbol_types[sym.symbol_id]
394
+ return
395
+ if isinstance(target, (ast.Tuple, ast.List)):
396
+ # The current closed frontend only reaches tuple/list unpacking for
397
+ # compile-time literal pairs. Keep pair typing exact.
398
+ if isinstance(owner, ast.Assign) and isinstance(owner.value, (ast.Tuple, ast.List)):
399
+ for t, v in zip(target.elts, owner.value.elts):
400
+ self._assign_target(t, self._expr_type(v, strict=strict, value_required=True), owner, strict)
401
+ return
402
+ if strict:
403
+ raise self._error("This assignment target cannot be typed in V1.", target)
404
+
405
+ def _expr_type(self, node: ast.AST, *, strict: bool, value_required: bool) -> BarmajaType:
406
+ typ = self._expr_type_uncached(node, strict=strict, value_required=value_required)
407
+ self.expression_types[id(node)] = typ
408
+ if value_required and typ.kind == TypeKind.VOID:
409
+ raise self._error("A Void call cannot be used where a value is required.", node)
410
+ return typ
411
+
412
+ def _expr_type_uncached(self, node: ast.AST, *, strict: bool, value_required: bool) -> BarmajaType:
413
+ if isinstance(node, ast.Constant):
414
+ if isinstance(node.value, bool):
415
+ return BOOL
416
+ if isinstance(node.value, int):
417
+ return INT
418
+ if isinstance(node.value, float):
419
+ return FLOAT
420
+ if isinstance(node.value, str):
421
+ return STRING
422
+ if node.value is Ellipsis:
423
+ return VOID
424
+
425
+ if isinstance(node, ast.Name):
426
+ sym = self.resolved.symbol_for_name(node)
427
+ if sym.kind == SymbolKind.API:
428
+ if node.id in {"HIGH", "LOW", "INPUT", "OUTPUT", "INPUT_PULLUP", "A0", "A1", "A2", "A3", "A4", "A5", "DHT11", "DHT22", "CM", "INC"}:
429
+ return INT
430
+ if strict:
431
+ raise self._error(f"API name '{node.id}' is not a value.", node)
432
+ return UNKNOWN
433
+ if sym.kind == SymbolKind.BUILTIN:
434
+ if strict:
435
+ raise self._error(f"Builtin '{node.id}' must be called, not used as a value.", node)
436
+ return UNKNOWN
437
+ if sym.kind == SymbolKind.FUNCTION:
438
+ if strict:
439
+ raise self._error("Function objects are not first-class values in V1.", node)
440
+ return UNKNOWN
441
+ typ = self.symbol_types.get(sym.symbol_id, UNKNOWN)
442
+ if strict and typ.kind == TypeKind.UNKNOWN:
443
+ raise self._error(f"Could not infer a type for '{sym.name}'.", node)
444
+ return typ
445
+
446
+ if isinstance(node, ast.List):
447
+ if not node.elts:
448
+ if strict:
449
+ raise self._error("Empty fixed lists are not supported in V1.", node)
450
+ return UNKNOWN
451
+ element = UNKNOWN
452
+ for item in node.elts:
453
+ item_type = self._expr_type(item, strict=strict, value_required=True)
454
+ if item_type.kind in {TypeKind.VOID, TypeKind.HARDWARE, TypeKind.FIXED_LIST, TypeKind.TIME_U32}:
455
+ if strict:
456
+ raise self._error("Fixed-list elements must be Bool, Int, Float, or String values.", item)
457
+ return UNKNOWN
458
+ element = self._join(element, item_type, item, strict)
459
+ return fixed_list_type(element, len(node.elts)) if element.kind != TypeKind.UNKNOWN else UNKNOWN
460
+
461
+ if isinstance(node, ast.Subscript):
462
+ container = self._expr_type(node.value, strict=strict, value_required=True)
463
+ index = self._expr_type(node.slice, strict=strict, value_required=True)
464
+ self._require_kind(index, {TypeKind.INT}, node.slice, "List index must be Int.", strict)
465
+ if container.kind == TypeKind.FIXED_LIST and container.element_type is not None:
466
+ self._validate_static_index(node.slice, container.length or 0, strict)
467
+ return container.element_type
468
+ if strict:
469
+ raise self._error("Indexing is supported only on fixed lists in V1.", node.value)
470
+ return UNKNOWN
471
+
472
+ if isinstance(node, ast.JoinedStr):
473
+ for part in node.values:
474
+ if isinstance(part, ast.FormattedValue):
475
+ inner = self._expr_type(part.value, strict=strict, value_required=True)
476
+ if strict and inner.kind not in SCALAR_KINDS:
477
+ raise self._error("f-string values must be supported scalar values.", part.value)
478
+ return STRING
479
+
480
+ if isinstance(node, ast.UnaryOp):
481
+ operand = self._expr_type(node.operand, strict=strict, value_required=True)
482
+ if isinstance(node.op, ast.Not):
483
+ self._require_bool(operand, node.operand, strict)
484
+ return BOOL
485
+ if isinstance(node.op, ast.USub):
486
+ self._require_kind(operand, NUMERIC_KINDS, node.operand, "Unary '-' requires Int or Float.", strict)
487
+ return operand
488
+
489
+ if isinstance(node, ast.BinOp):
490
+ left = self._expr_type(node.left, strict=strict, value_required=True)
491
+ right = self._expr_type(node.right, strict=strict, value_required=True)
492
+ return self._numeric_binop_type(left, right, node, division=isinstance(node.op, ast.Div), strict=strict)
493
+
494
+ if isinstance(node, ast.BoolOp):
495
+ for value in node.values:
496
+ self._require_bool(self._expr_type(value, strict=strict, value_required=True), value, strict)
497
+ return BOOL
498
+
499
+ if isinstance(node, ast.Compare):
500
+ values = [node.left, *node.comparators]
501
+ types = [self._expr_type(x, strict=strict, value_required=True) for x in values]
502
+ if strict and all(t.kind != TypeKind.UNKNOWN for t in types):
503
+ for op, a, b in zip(node.ops, types, types[1:]):
504
+ if not self._comparison_compatible(a, b):
505
+ raise self._error(f"Cannot compare {a.display_name} with {b.display_name}.", node)
506
+ # V1 intentionally supports String/Bool equality only; do
507
+ # not silently inherit C++ pointer ordering or bool-as-int.
508
+ if (a.kind == TypeKind.STRING or b.kind == TypeKind.STRING) and not isinstance(op, (ast.Eq, ast.NotEq)):
509
+ raise self._error("String ordering comparisons are not supported in Barmaja PyArduino V1.", node)
510
+ if a.kind == TypeKind.BOOL and b.kind == TypeKind.BOOL and not isinstance(op, (ast.Eq, ast.NotEq)):
511
+ raise self._error("Bool ordering comparisons are not supported in Barmaja PyArduino V1.", node)
512
+ return BOOL
513
+
514
+ if isinstance(node, ast.IfExp):
515
+ self._require_bool(self._expr_type(node.test, strict=strict, value_required=True), node.test, strict)
516
+ a = self._expr_type(node.body, strict=strict, value_required=True)
517
+ b = self._expr_type(node.orelse, strict=strict, value_required=True)
518
+ return self._join(a, b, node, strict)
519
+
520
+ if isinstance(node, ast.Call):
521
+ return self._call_type(node, strict=strict)
522
+
523
+ if strict:
524
+ raise self._error(f"Expression '{type(node).__name__}' has no V1 type rule.", node)
525
+ return UNKNOWN
526
+
527
+ def _call_type(self, node: ast.Call, *, strict: bool) -> BarmajaType:
528
+ if isinstance(node.func, ast.Name):
529
+ name = node.func.id
530
+ args = [self._expr_type(arg, strict=strict, value_required=True) for arg in node.args]
531
+
532
+ if name in self._function_nodes:
533
+ params = self._param_symbols[name]
534
+ for param, arg_type, arg_node in zip(params, args, node.args):
535
+ self._merge_symbol(param, arg_type, arg_node, strict)
536
+ return self._return_types[name]
537
+
538
+ if name in HARDWARE_CLASSES:
539
+ return self._registry_constructor_type(name, args, node.args, strict)
540
+ if name == "print":
541
+ for typ, arg in zip(args, node.args):
542
+ if strict and typ.kind not in SCALAR_KINDS:
543
+ raise self._error("print() accepts supported scalar values in V1.", arg)
544
+ return VOID
545
+ if name in CORE_FUNCTIONS:
546
+ return self._registry_function_type(name, args, node.args, strict)
547
+ if name in {"int", "float", "bool"}:
548
+ return self._conversion_type(name, args[0] if args else UNKNOWN, node, strict)
549
+ if name == "round":
550
+ if not args:
551
+ return UNKNOWN
552
+ self._require_kind(args[0], NUMERIC_KINDS, node.args[0], "round() requires Int or Float.", strict)
553
+ if len(args) == 2:
554
+ self._require_kind(args[1], {TypeKind.INT}, node.args[1], "round() ndigits must be Int.", strict)
555
+ if strict:
556
+ ndigits = self._literal_int(node.args[1])
557
+ if ndigits is None or not (-6 <= ndigits <= 6):
558
+ raise self._error("round() ndigits must be a compile-time Int from -6 to 6 in V1.", node.args[1])
559
+ return args[0] if args[0].kind != TypeKind.UNKNOWN else UNKNOWN
560
+ return INT if args[0].kind == TypeKind.FLOAT else args[0]
561
+ if name == "abs":
562
+ self._require_kind(args[0], NUMERIC_KINDS, node.args[0], "abs() requires Int or Float.", strict)
563
+ return args[0]
564
+ if name in {"min", "max"}:
565
+ current = UNKNOWN
566
+ for typ, arg in zip(args, node.args):
567
+ self._require_kind(typ, NUMERIC_KINDS, arg, f"{name}() requires numeric arguments.", strict)
568
+ current = self._join(current, typ, node, strict)
569
+ return current
570
+ if name == "len":
571
+ if not args:
572
+ return UNKNOWN
573
+ if strict and args[0].kind != TypeKind.FIXED_LIST:
574
+ raise self._error("len() requires a fixed list in V1.", node.args[0])
575
+ return INT
576
+ if name == "sum":
577
+ if not args:
578
+ return UNKNOWN
579
+ if strict and args[0].kind != TypeKind.FIXED_LIST:
580
+ raise self._error("sum() requires a fixed list in V1.", node.args[0])
581
+ if args[0].kind == TypeKind.FIXED_LIST and args[0].element_type is not None:
582
+ self._require_kind(args[0].element_type, NUMERIC_KINDS, node.args[0], "sum() requires an Int or Float fixed list.", strict)
583
+ return args[0].element_type
584
+ return UNKNOWN
585
+ if name == "enumerate":
586
+ if args and strict and args[0].kind != TypeKind.FIXED_LIST:
587
+ raise self._error("enumerate() requires a fixed list in V1.", node.args[0])
588
+ if len(args) == 2:
589
+ self._require_kind(args[1], {TypeKind.INT}, node.args[1], "enumerate() start must be Int.", strict)
590
+ return VOID
591
+ if name == "range":
592
+ for typ, arg in zip(args, node.args):
593
+ self._require_kind(typ, {TypeKind.INT}, arg, "range() values must be Int.", strict)
594
+ return VOID # only valid as for-iterator, never a V1 value
595
+
596
+ if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
597
+ owner_node = node.func.value
598
+ owner_sym = self.resolved.symbol_for_name(owner_node)
599
+ args = [self._expr_type(arg, strict=strict, value_required=True) for arg in node.args]
600
+ if owner_node.id in SINGLETONS and owner_sym.kind == SymbolKind.API:
601
+ return self._registry_singleton_method_type(owner_node.id, node.func.attr, args, node.args, strict)
602
+
603
+ owner_type = self.symbol_types.get(owner_sym.symbol_id, UNKNOWN)
604
+ if owner_type.kind != TypeKind.HARDWARE:
605
+ if strict:
606
+ raise self._error(f"'{owner_node.id}' is not a hardware object.", owner_node)
607
+ return UNKNOWN
608
+ return self._registry_hardware_method_type(owner_type.hardware_class or "", node.func.attr, args, node.args, strict)
609
+
610
+ if strict:
611
+ raise self._error("Unsupported call form for type analysis.", node)
612
+ return UNKNOWN
613
+
614
+ def _conversion_type(self, name: str, arg: BarmajaType, node: ast.AST, strict: bool) -> BarmajaType:
615
+ allowed = {TypeKind.BOOL, TypeKind.INT, TypeKind.FLOAT}
616
+ if name == "float":
617
+ # Explicit, non-narrowing conversion of millis() time, e.g. float(millis()) / 1000.
618
+ allowed = allowed | {TypeKind.TIME_U32}
619
+ message = "float() accepts Bool, Int, Float, or millis() time in V1." if name == "float" else f"{name}() accepts Bool, Int, or Float in V1."
620
+ self._require_kind(arg, allowed, node, message, strict)
621
+ return {"int": INT, "float": FLOAT, "bool": BOOL}[name]
622
+
623
+ def _registry_constructor_type(self, name: str, args: list[BarmajaType], nodes: list[ast.AST], strict: bool) -> BarmajaType:
624
+ spec = HARDWARE_CLASSES[name]
625
+ sig = self._validate_registry_call(f"{name}()", spec.constructors, args, nodes, strict)
626
+ return self._type_from_registry_token(sig.returns, name)
627
+
628
+ def _registry_function_type(self, name: str, args: list[BarmajaType], nodes: list[ast.AST], strict: bool) -> BarmajaType:
629
+ spec = CORE_FUNCTIONS[name]
630
+ sig = self._validate_registry_call(f"{name}()", spec.signatures, args, nodes, strict)
631
+ return self._type_from_registry_token(sig.returns)
632
+
633
+ def _registry_hardware_method_type(self, cls: str, method: str, args: list[BarmajaType], nodes: list[ast.AST], strict: bool) -> BarmajaType:
634
+ try:
635
+ spec = HARDWARE_CLASSES[cls].methods[method]
636
+ except KeyError:
637
+ if strict:
638
+ node = nodes[0] if nodes else self._function_nodes[next(iter(self._function_nodes))] if self._function_nodes else self.frontend.tree
639
+ raise self._error(f"{cls}.{method}() is not registered in Barmaja PyArduino V1.", node)
640
+ return UNKNOWN
641
+ sig = self._validate_registry_call(f"{cls}.{method}()", spec.signatures, args, nodes, strict)
642
+ return self._type_from_registry_token(sig.returns)
643
+
644
+ def _registry_singleton_method_type(self, owner: str, method: str, args: list[BarmajaType], nodes: list[ast.AST], strict: bool) -> BarmajaType:
645
+ try:
646
+ spec = SINGLETONS[owner].methods[method]
647
+ except KeyError:
648
+ if strict:
649
+ node = nodes[0] if nodes else self.frontend.tree
650
+ raise self._error(f"{owner}.{method}() is not registered in Barmaja PyArduino V1.", node)
651
+ return UNKNOWN
652
+ sig = self._validate_registry_call(f"{owner}.{method}()", spec.signatures, args, nodes, strict)
653
+ return self._type_from_registry_token(sig.returns)
654
+
655
+ def _validate_registry_call(
656
+ self,
657
+ label: str,
658
+ signatures: tuple[SignatureSpec, ...],
659
+ args: list[BarmajaType],
660
+ nodes: list[ast.AST],
661
+ strict: bool,
662
+ ) -> SignatureSpec:
663
+ candidates = matching_signatures(signatures, len(args))
664
+ if not candidates:
665
+ # Frontend owns arity diagnostics; retain a defensive type-phase guard.
666
+ node = nodes[0] if nodes else self.frontend.tree
667
+ if strict:
668
+ allowed = ", ".join(str(len(sig.args)) for sig in signatures)
669
+ raise self._error(f"{label} accepts registered argument counts: {allowed}.", node)
670
+ return signatures[0]
671
+ if not strict:
672
+ return candidates[0]
673
+ for sig in candidates:
674
+ if all(self._registry_token_accepts(token, typ, node, strict=False) for token, typ, node in zip(sig.args, args, nodes)):
675
+ # Strict pass gives specialized constant-token diagnostics.
676
+ for token, typ, node in zip(sig.args, args, nodes):
677
+ self._registry_token_accepts(token, typ, node, strict=True)
678
+ return sig
679
+ expected = " or ".join("(" + ", ".join(sig.args) + ")" for sig in candidates)
680
+ actual = "(" + ", ".join(t.display_name for t in args) + ")"
681
+ node = nodes[0] if nodes else self.frontend.tree
682
+ raise self._error(f"{label} expects {expected}, not {actual}.", node)
683
+
684
+ def _registry_token_accepts(self, token: str, typ: BarmajaType, node: ast.AST, *, strict: bool) -> bool:
685
+ if typ.kind == TypeKind.UNKNOWN:
686
+ return not strict
687
+ allowed: set[TypeKind] | None = {
688
+ "Int": {TypeKind.INT},
689
+ "Bool": {TypeKind.BOOL},
690
+ "Float": {TypeKind.FLOAT},
691
+ "String": {TypeKind.STRING},
692
+ "IntOrBool": {TypeKind.INT, TypeKind.BOOL},
693
+ "Scalar": SCALAR_KINDS,
694
+ "NumericOrBool": {TypeKind.INT, TypeKind.FLOAT, TypeKind.BOOL},
695
+ "UltrasonicUnit": {TypeKind.INT},
696
+ "DHTType": {TypeKind.INT},
697
+ }.get(token)
698
+ if allowed is None:
699
+ return True
700
+ if typ.kind not in allowed:
701
+ return False
702
+ if token == "UltrasonicUnit":
703
+ ok = isinstance(node, ast.Name) and node.id in {"CM", "INC"}
704
+ if strict and not ok:
705
+ raise self._error("Ultrasonic.read() unit must be CM or INC.", node)
706
+ return ok
707
+ if token == "DHTType":
708
+ ok = isinstance(node, ast.Name) and node.id in {"DHT11", "DHT22"}
709
+ if strict and not ok:
710
+ raise self._error("DHT() sensor type must be DHT11 or DHT22.", node)
711
+ return ok
712
+ return True
713
+
714
+ @staticmethod
715
+ def _type_from_registry_token(token: str, hardware_class: str | None = None) -> BarmajaType:
716
+ mapping = {
717
+ "Void": VOID,
718
+ "Bool": BOOL,
719
+ "Int": INT,
720
+ "Float": FLOAT,
721
+ "String": STRING,
722
+ "TimeU32": TIME_U32,
723
+ }
724
+ if token.startswith("Hardware["):
725
+ name = token[len("Hardware["):-1]
726
+ return hardware_type(name)
727
+ return mapping.get(token, hardware_type(hardware_class) if hardware_class else UNKNOWN)
728
+
729
+ def _require_all_int(self, args: list[BarmajaType], nodes: list[ast.AST], label: str, strict: bool) -> None:
730
+ for typ, node in zip(args, nodes):
731
+ self._require_kind(typ, {TypeKind.INT}, node, f"{label} arguments must be Int.", strict)
732
+
733
+ def _numeric_binop_type(self, left: BarmajaType, right: BarmajaType, node: ast.AST, *, division: bool, strict: bool) -> BarmajaType:
734
+ # Arduino wrap-safe elapsed time: TimeU32 - TimeU32 remains TimeU32.
735
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Sub) and left.kind == TypeKind.TIME_U32 and right.kind == TypeKind.TIME_U32:
736
+ return TIME_U32
737
+ # True division never narrows: millis() / 1000 is a Float number of seconds.
738
+ if (
739
+ division and isinstance(node, ast.BinOp)
740
+ and TypeKind.TIME_U32 in {left.kind, right.kind}
741
+ and {left.kind, right.kind} <= {TypeKind.TIME_U32, TypeKind.INT, TypeKind.FLOAT}
742
+ ):
743
+ return FLOAT
744
+ if left.kind == TypeKind.UNKNOWN or right.kind == TypeKind.UNKNOWN:
745
+ # If one side is a name and the other fixes the numeric family, use
746
+ # that as a constraint. A float counterpart widens the unknown.
747
+ known = right if left.kind == TypeKind.UNKNOWN else left
748
+ unknown_node = node.left if isinstance(node, ast.BinOp) and left.kind == TypeKind.UNKNOWN else (node.right if isinstance(node, ast.BinOp) else None)
749
+ if known.kind in NUMERIC_KINDS and isinstance(unknown_node, ast.Name):
750
+ sym = self.resolved.symbol_for_name(unknown_node)
751
+ self._merge_symbol(sym, known, unknown_node, strict=False)
752
+ left = self.symbol_types.get(sym.symbol_id, known) if unknown_node is getattr(node, "left", None) else left
753
+ right = self.symbol_types.get(sym.symbol_id, known) if unknown_node is getattr(node, "right", None) else right
754
+ if left.kind == TypeKind.UNKNOWN or right.kind == TypeKind.UNKNOWN:
755
+ return UNKNOWN
756
+ if left.kind not in NUMERIC_KINDS or right.kind not in NUMERIC_KINDS:
757
+ if strict:
758
+ raise self._error(f"Arithmetic requires Int/Float, not {left.display_name} and {right.display_name}.", node)
759
+ return UNKNOWN
760
+ if division:
761
+ return FLOAT
762
+ return FLOAT if TypeKind.FLOAT in {left.kind, right.kind} else INT
763
+
764
+ def _validate_static_index(self, node: ast.AST, length: int, strict: bool) -> None:
765
+ value = self._literal_int(node)
766
+ if value is None or not strict:
767
+ return
768
+ if value < 0:
769
+ raise self._error("Negative list indexing is not supported in Barmaja PyArduino V1.", node)
770
+ if value >= length:
771
+ raise self._error(f"List index {value} is out of range for fixed list of length {length}.", node)
772
+
773
+ @staticmethod
774
+ def _literal_int(node: ast.AST) -> int | None:
775
+ if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool):
776
+ return int(node.value)
777
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
778
+ value = TypeAnalyzer._literal_int(node.operand)
779
+ return -value if value is not None else None
780
+ return None
781
+
782
+ @staticmethod
783
+ def _comparison_compatible(a: BarmajaType, b: BarmajaType) -> bool:
784
+ if a.kind == b.kind:
785
+ return a.kind in SCALAR_KINDS
786
+ if a.kind in NUMERIC_KINDS and b.kind in NUMERIC_KINDS:
787
+ return True
788
+ if a.kind == TypeKind.TIME_U32 and b.kind == TypeKind.INT:
789
+ return True
790
+ if b.kind == TypeKind.TIME_U32 and a.kind == TypeKind.INT:
791
+ return True
792
+ return False
793
+
794
+ def _merge_symbol(self, symbol: Symbol, incoming: BarmajaType, node: ast.AST, strict: bool) -> None:
795
+ if incoming.kind in {TypeKind.UNKNOWN, TypeKind.VOID}:
796
+ if strict and incoming.kind == TypeKind.VOID:
797
+ raise self._error("Void cannot be stored in a variable.", node)
798
+ return
799
+ if incoming.kind == TypeKind.FIXED_LIST and symbol.kind == SymbolKind.PARAMETER:
800
+ # Python passes the list object itself (aliasing); a C++ array parameter
801
+ # is not expressible in the current backend, so reject it cleanly.
802
+ if strict:
803
+ raise self._error(
804
+ f"Lists cannot be passed to functions in Barmaja PyArduino V1 (parameter '{symbol.name}'). "
805
+ "Use the list's module-level name inside the function instead.",
806
+ node,
807
+ )
808
+ return
809
+ if incoming.kind == TypeKind.HARDWARE:
810
+ # Hardware objects are created exactly once by their top-level constructor
811
+ # assignment (seeded directly, not merged). Copying one into another
812
+ # variable/parameter would construct or copy a second C++ device object.
813
+ if strict:
814
+ raise self._error(
815
+ f"'{symbol.name}' cannot hold a {incoming.hardware_class} object. Hardware objects cannot be copied, "
816
+ "passed to functions, or assigned to other variables; use the original object name directly.",
817
+ node,
818
+ )
819
+ return
820
+ current = self.symbol_types.get(symbol.symbol_id, UNKNOWN)
821
+ if (
822
+ symbol.kind != SymbolKind.PARAMETER
823
+ and {current.kind, incoming.kind} == {TypeKind.INT, TypeKind.TIME_U32}
824
+ ):
825
+ # Arduino timer idiom: `last = 0` ... `last = millis()`. The variable
826
+ # stores time; Int stores are checked non-negative by the backend.
827
+ joined = TIME_U32
828
+ else:
829
+ joined = self._join(current, incoming, node, strict)
830
+ if joined != current:
831
+ self.symbol_types[symbol.symbol_id] = joined
832
+ self._changed = True
833
+
834
+ def _merge_return(self, name: str, incoming: BarmajaType, node: ast.AST, strict: bool) -> None:
835
+ if incoming.kind == TypeKind.FIXED_LIST:
836
+ if strict:
837
+ raise self._error(
838
+ f"Function '{name}' cannot return a list in Barmaja PyArduino V1. "
839
+ "Change the module-level list inside the function (using its name) or return one element.",
840
+ node,
841
+ )
842
+ return
843
+ if incoming.kind == TypeKind.HARDWARE:
844
+ if strict:
845
+ raise self._error(
846
+ f"Function '{name}' cannot return a {incoming.hardware_class} object. Use the hardware object name directly.",
847
+ node,
848
+ )
849
+ return
850
+ current = self._return_types[name]
851
+ if current.kind == TypeKind.VOID and incoming.kind != TypeKind.VOID:
852
+ current = UNKNOWN
853
+ joined = self._join(current, incoming, node, strict)
854
+ if joined != self._return_types[name]:
855
+ self._return_types[name] = joined
856
+ self._changed = True
857
+
858
+ def _join(self, a: BarmajaType, b: BarmajaType, node: ast.AST, strict: bool) -> BarmajaType:
859
+ if a.kind == TypeKind.UNKNOWN:
860
+ return b
861
+ if b.kind == TypeKind.UNKNOWN:
862
+ return a
863
+ if a == b:
864
+ return a
865
+ if a.kind in NUMERIC_KINDS and b.kind in NUMERIC_KINDS:
866
+ return FLOAT
867
+ if strict:
868
+ raise self._error(f"Incompatible types: {a.display_name} and {b.display_name}.", node)
869
+ # Do not hide an incompatible constraint during inference. The strict
870
+ # pass will report it at the source location.
871
+ return a
872
+
873
+ def _require_bool(self, typ: BarmajaType, node: ast.AST, strict: bool) -> None:
874
+ self._require_kind(typ, {TypeKind.BOOL}, node, "Condition/logical value must be Bool in Barmaja PyArduino V1.", strict)
875
+
876
+ def _require_kind(self, typ: BarmajaType, kinds: set[TypeKind], node: ast.AST, message: str, strict: bool) -> None:
877
+ if typ.kind == TypeKind.UNKNOWN and not strict:
878
+ return
879
+ if typ.kind not in kinds:
880
+ raise self._error(message, node)
881
+
882
+ def _require_resolved_user_symbols(self) -> None:
883
+ for symbol_id, typ in self.symbol_types.items():
884
+ if typ.kind == TypeKind.UNKNOWN:
885
+ symbol = self._symbol_by_id(symbol_id)
886
+ raise TypeCheckError(
887
+ f"Could not infer a type for '{symbol.name}'.",
888
+ line=symbol.declaration_line,
889
+ )
890
+ for name, typ in self._return_types.items():
891
+ if typ.kind == TypeKind.UNKNOWN:
892
+ fn = self._function_nodes[name]
893
+ raise self._error(f"Could not infer the return type of function '{name}'.", fn)
894
+
895
+ def _symbol_by_id(self, symbol_id: SymbolId) -> Symbol:
896
+ for symbol in self.resolved.module_scope.symbols.values():
897
+ if symbol.symbol_id == symbol_id:
898
+ return symbol
899
+ for scope in self.resolved.function_scopes.values():
900
+ for symbol in scope.symbols.values():
901
+ if symbol.symbol_id == symbol_id:
902
+ return symbol
903
+ raise KeyError(symbol_id)
904
+
905
+ @staticmethod
906
+ def _error(message: str, node: ast.AST) -> TypeCheckError:
907
+ return TypeCheckError(
908
+ message,
909
+ line=getattr(node, "lineno", None),
910
+ column=(getattr(node, "col_offset", None) + 1) if getattr(node, "col_offset", None) is not None else None,
911
+ end_line=getattr(node, "end_lineno", None),
912
+ end_column=getattr(node, "end_col_offset", None),
913
+ )
914
+
915
+
916
+ def analyze_types(flow: FlowAnalyzedModule) -> TypedModule:
917
+ return TypeAnalyzer(flow).analyze()