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