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