pythonfaster 1.8.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.
- pythonfaster/__init__.py +92 -0
- pythonfaster/_bootstrap.py +89 -0
- pythonfaster/_cli.py +232 -0
- pythonfaster/cache.py +285 -0
- pythonfaster/compiler.py +425 -0
- pythonfaster/config.py +165 -0
- pythonfaster/hook.py +307 -0
- pythonfaster/py.typed +0 -0
- pythonfaster/transform/__init__.py +10 -0
- pythonfaster/transform/engine.py +3033 -0
- pythonfaster/transform/recpass.py +522 -0
- pythonfaster-1.8.0.data/data/pythonfaster.pth +1 -0
- pythonfaster-1.8.0.dist-info/METADATA +362 -0
- pythonfaster-1.8.0.dist-info/RECORD +18 -0
- pythonfaster-1.8.0.dist-info/WHEEL +5 -0
- pythonfaster-1.8.0.dist-info/entry_points.txt +2 -0
- pythonfaster-1.8.0.dist-info/licenses/LICENSE +21 -0
- pythonfaster-1.8.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,3033 @@
|
|
|
1
|
+
"""Conservative AST transforms for the pythonfaster compilation pipeline.
|
|
2
|
+
|
|
3
|
+
The engine performs value/type inference, object-layout and method-call
|
|
4
|
+
lowering, container unboxing, and selected call/loop rewrites. It returns the
|
|
5
|
+
original source unchanged whenever it cannot safely apply a transformation;
|
|
6
|
+
the compiler owns the subsequent plain-Cython and Python fallbacks.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import copy
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Public interface
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TransformResult:
|
|
21
|
+
source: str
|
|
22
|
+
changed: bool
|
|
23
|
+
applied: list[str] = field(default_factory=list)
|
|
24
|
+
notes: list[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def transform_source(source: str, level: int = 2,
|
|
28
|
+
*, aggressive: bool = False) -> TransformResult:
|
|
29
|
+
try:
|
|
30
|
+
tree = ast.parse(source)
|
|
31
|
+
except SyntaxError:
|
|
32
|
+
return TransformResult(source=source, changed=False)
|
|
33
|
+
try:
|
|
34
|
+
t = _Transformer(tree)
|
|
35
|
+
if t.run():
|
|
36
|
+
ast.fix_missing_locations(tree)
|
|
37
|
+
# Workaround for Python 3.12.0 ast.unparse bug: when unparsing
|
|
38
|
+
# a full module, function defs adjacent to certain statement
|
|
39
|
+
# patterns (bare-tuple returns, multi-line print) get silently
|
|
40
|
+
# dropped. Unparsing each top-level node individually and
|
|
41
|
+
# joining avoids the bug. Fixed in CPython 3.12.1+.
|
|
42
|
+
if sys.version_info[:2] == (3, 12) and sys.version_info[2] == 0:
|
|
43
|
+
parts = []
|
|
44
|
+
for node in tree.body:
|
|
45
|
+
parts.append(ast.unparse(node))
|
|
46
|
+
source = "\n\n".join(parts)
|
|
47
|
+
else:
|
|
48
|
+
source = ast.unparse(tree)
|
|
49
|
+
# Pass 9 marker swap: __pf_cdef_N = 0 → real cdef blocks
|
|
50
|
+
# (cdef is not valid Python syntax, cannot live in the AST).
|
|
51
|
+
from .recpass import PENDING_CDEFS as _pending
|
|
52
|
+
if _pending:
|
|
53
|
+
for _mk, _cdef_src in _pending.items():
|
|
54
|
+
source = source.replace(f"{_mk} = 0", _cdef_src)
|
|
55
|
+
_pending.clear()
|
|
56
|
+
if "__pb_cdef_marker" in source:
|
|
57
|
+
source = source.replace(
|
|
58
|
+
"__pb_cdef_marker = 0",
|
|
59
|
+
"cdef double[:] __pb_pos = __pb_pos_src\n"
|
|
60
|
+
" cdef double[:] __pb_vel = __pb_vel_src\n"
|
|
61
|
+
" cdef double[:] __pb_mass = __pb_mass_src\n"
|
|
62
|
+
" cdef long[:] __pb_pair = __pb_pair_src")
|
|
63
|
+
# Pass 8 textual layer: typing markers → locals decl, then
|
|
64
|
+
# memoryview bindings for unboxed array('q') creations.
|
|
65
|
+
import re as _re
|
|
66
|
+
_marks = set(_re.findall(r"__pb_type_(\w+) = 0", source))
|
|
67
|
+
if _marks:
|
|
68
|
+
source = _re.sub(r"^ *__pb_type_\w+ = 0\n", "", source, flags=_re.M)
|
|
69
|
+
_decl = ",".join(f"{m}=cython.longlong" for m in sorted(_marks))
|
|
70
|
+
source = _re.sub(r"(@cython\.locals\()",
|
|
71
|
+
rf"\1{_decl},", source, count=1)
|
|
72
|
+
if "__pb_array('q', range(" in source:
|
|
73
|
+
source = _re.sub(
|
|
74
|
+
r"(^ (\w+) = __pb_array\('q', range\([^\n]*\)\)\n)",
|
|
75
|
+
r"\1 cdef long[:] \g<2>_v = \g<2>\n",
|
|
76
|
+
source, flags=_re.M)
|
|
77
|
+
for _base in sorted(set(_re.findall(
|
|
78
|
+
r"^ (\w+) = __pb_array\('q'", source, flags=_re.M))):
|
|
79
|
+
source = _re.sub(
|
|
80
|
+
rf"(?<![_\w]){_base}(?![_\w])(?=\[)",
|
|
81
|
+
f"{_base}_v", source)
|
|
82
|
+
source = source.replace(
|
|
83
|
+
"from array import ", "from array import ", 1)
|
|
84
|
+
if "from array import array as __pb_array" not in source:
|
|
85
|
+
source = "from array import array as __pb_array\n" + source
|
|
86
|
+
return TransformResult(
|
|
87
|
+
source=source, changed=True,
|
|
88
|
+
applied=t.applied_strategies)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
return TransformResult(
|
|
91
|
+
source=source, changed=False,
|
|
92
|
+
notes=[f"transform aborted: {exc!r}"])
|
|
93
|
+
return TransformResult(source=source, changed=False)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_sum_genexp(node: ast.AST) -> bool:
|
|
97
|
+
"""True for `sum(<genexp>)` with a single comprehension, no ifs, no
|
|
98
|
+
async — the only sum shape we loopify."""
|
|
99
|
+
return (
|
|
100
|
+
isinstance(node, ast.Call)
|
|
101
|
+
and isinstance(node.func, ast.Name) and node.func.id == "sum"
|
|
102
|
+
and not node.keywords and len(node.args) == 1
|
|
103
|
+
and isinstance(node.args[0], ast.GeneratorExp)
|
|
104
|
+
and len(node.args[0].generators) == 1
|
|
105
|
+
and not node.args[0].generators[0].ifs
|
|
106
|
+
and not node.args[0].generators[0].is_async
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _StmtList:
|
|
111
|
+
"""Marker wrapper: a transformer returns multiple statements in place
|
|
112
|
+
of one; the owner flattens them via _flatten_stmts."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, stmts: list):
|
|
115
|
+
self.stmts = stmts
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Constants & helpers
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
_MAX_BITS = 62
|
|
123
|
+
DOUBLE = "cython.double"
|
|
124
|
+
LONGLONG = "cython.longlong"
|
|
125
|
+
OBJECT = "object"
|
|
126
|
+
BINT = "cython.bint"
|
|
127
|
+
|
|
128
|
+
_DYNAMIC_CALLS = frozenset({
|
|
129
|
+
"eval", "exec", "locals", "vars", "globals", "compile",
|
|
130
|
+
"getattr", "setattr", "delattr", "__import__",
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _cheap_expr(node) -> bool:
|
|
135
|
+
"""True for expressions that are safe and cheap to evaluate more than
|
|
136
|
+
once (pure, no calls, no attribute/subscript lookups): the only shapes we
|
|
137
|
+
allow when substituting inlined bodies and call-site arguments."""
|
|
138
|
+
if isinstance(node, ast.Name):
|
|
139
|
+
return True
|
|
140
|
+
if isinstance(node, ast.Constant):
|
|
141
|
+
return True
|
|
142
|
+
if isinstance(node, ast.BinOp):
|
|
143
|
+
return _cheap_expr(node.left) and _cheap_expr(node.right)
|
|
144
|
+
if isinstance(node, ast.UnaryOp):
|
|
145
|
+
return _cheap_expr(node.operand)
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _inlineable_shape(func: ast.FunctionDef) -> dict[str, ast.AST] | None:
|
|
150
|
+
"""Return {name -> replacement expr} if `func` is a pure expression
|
|
151
|
+
function safe to inline: plain positional args, straight-line simple
|
|
152
|
+
assignments to fresh names, and a final `return <cheap expr>`.
|
|
153
|
+
|
|
154
|
+
Returns None for anything else (loops, branches, calls, generators,
|
|
155
|
+
defaults, *args, shadowed names...). All exprs must be _cheap_expr so
|
|
156
|
+
duplicated evaluation at the call site is free and side-effect-free.
|
|
157
|
+
"""
|
|
158
|
+
if func.decorator_list or func.args.defaults or func.args.kw_defaults \
|
|
159
|
+
or func.args.vararg or func.args.kwarg or func.args.kwonlyargs \
|
|
160
|
+
or func.args.posonlyargs:
|
|
161
|
+
return None
|
|
162
|
+
params = [a.arg for a in func.args.args]
|
|
163
|
+
if not params or len(set(params)) != len(params):
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
subst: dict[str, ast.AST] = {p: ast.Name(id=p, ctx=ast.Load()) for p in params}
|
|
167
|
+
assigned: set[str] = set()
|
|
168
|
+
stmts = func.body
|
|
169
|
+
if not stmts or not isinstance(stmts[-1], ast.Return) \
|
|
170
|
+
or stmts[-1].value is None:
|
|
171
|
+
return None
|
|
172
|
+
for stmt in stmts[:-1]:
|
|
173
|
+
if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1
|
|
174
|
+
and isinstance(stmt.targets[0], ast.Name)):
|
|
175
|
+
return None
|
|
176
|
+
name = stmt.targets[0].id
|
|
177
|
+
if name in params or name in assigned:
|
|
178
|
+
return None # shadowing a param / reassignment: not pure SSA
|
|
179
|
+
val = stmt.value
|
|
180
|
+
if not _cheap_expr(val):
|
|
181
|
+
return None
|
|
182
|
+
# names referenced must be params or earlier temps (no globals,
|
|
183
|
+
# no forward refs); substitute params inside now
|
|
184
|
+
val = ast.copy_location(_subst_names(val, subst), stmt)
|
|
185
|
+
# every referenced name must be known after substitution
|
|
186
|
+
for sub in ast.walk(val):
|
|
187
|
+
if isinstance(sub, ast.Name) and sub.id not in params \
|
|
188
|
+
and sub.id not in assigned:
|
|
189
|
+
return None
|
|
190
|
+
assigned.add(name)
|
|
191
|
+
subst[name] = val
|
|
192
|
+
ret = stmts[-1].value
|
|
193
|
+
if not _cheap_expr(ret):
|
|
194
|
+
return None
|
|
195
|
+
for sub in ast.walk(ret):
|
|
196
|
+
if isinstance(sub, ast.Name) and sub.id not in params \
|
|
197
|
+
and sub.id not in assigned:
|
|
198
|
+
return None
|
|
199
|
+
return subst
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _subst_names(expr: ast.AST, subst: dict[str, ast.AST]) -> ast.AST:
|
|
203
|
+
"""Deep-copy `expr`, replacing Name loads found in `subst` in ONE pass.
|
|
204
|
+
|
|
205
|
+
Single-pass is essential: replacement exprs are inserted as-is and not
|
|
206
|
+
re-visited, so a non-idempotent binding (e.g. swapping i<->j at a call
|
|
207
|
+
site like eval_A(j, i)) is applied exactly once. Callers must therefore
|
|
208
|
+
pass replacement exprs that are already fully resolved.
|
|
209
|
+
"""
|
|
210
|
+
class _R(ast.NodeTransformer):
|
|
211
|
+
def __init__(self, table):
|
|
212
|
+
self.table = table
|
|
213
|
+
|
|
214
|
+
def visit_Name(self, node: ast.Name) -> ast.AST:
|
|
215
|
+
if isinstance(node.ctx, ast.Load) and node.id in self.table:
|
|
216
|
+
return copy.deepcopy(self.table[node.id])
|
|
217
|
+
return node
|
|
218
|
+
return _R(subst).visit(copy.deepcopy(expr))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _bit_len(v: int) -> int:
|
|
222
|
+
if v == 0:
|
|
223
|
+
return 1
|
|
224
|
+
return v.bit_length() + (1 if v < 0 else 0)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _literal_int(node):
|
|
228
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, int) \
|
|
229
|
+
and not isinstance(node.value, bool):
|
|
230
|
+
return node.value
|
|
231
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) \
|
|
232
|
+
and isinstance(node.operand, ast.Constant) \
|
|
233
|
+
and isinstance(node.operand.value, int):
|
|
234
|
+
return -node.operand.value
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _literal_float(node) -> bool:
|
|
239
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, float):
|
|
240
|
+
return True
|
|
241
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
|
|
242
|
+
return _literal_float(node.operand)
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _call_name(node):
|
|
247
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
248
|
+
return node.func.id
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_range_call(node):
|
|
253
|
+
return _call_name(node) == "range"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _ctype_to_ast(ctype: str) -> ast.AST:
|
|
257
|
+
parts = ctype.split(".")
|
|
258
|
+
result = ast.Name(id=parts[0], ctx=ast.Load())
|
|
259
|
+
for part in parts[1:]:
|
|
260
|
+
result = ast.Attribute(value=result, attr=part, ctx=ast.Load())
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Scanner: collect scope-sensitive facts about one function
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
class _Scanner:
|
|
269
|
+
"""Collects assignment, aug-assign, range-for, and usage data.
|
|
270
|
+
|
|
271
|
+
Never descends into nested scopes (functions/lambdas/classes) for
|
|
272
|
+
assignment tracking; comprehensions are separate scopes.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
_NESTED = (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)
|
|
276
|
+
_COMPS = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
277
|
+
|
|
278
|
+
def __init__(self, func: ast.FunctionDef) -> None:
|
|
279
|
+
self.func = func
|
|
280
|
+
self.assigns: dict[str, list[ast.expr]] = {}
|
|
281
|
+
self.aug: dict[str, list[tuple[type, ast.expr]]] = {}
|
|
282
|
+
self.range_fors: dict[str, ast.Call] = {}
|
|
283
|
+
self.opaque_assigned: set[str] = set()
|
|
284
|
+
self.complex_use: set[str] = set()
|
|
285
|
+
self.arg_ctx_bad: set[str] = set()
|
|
286
|
+
self.range_arg_names: set[str] = set()
|
|
287
|
+
self.nested_captures: set[str] = set()
|
|
288
|
+
self.has_divmod = False
|
|
289
|
+
self.dynamic = False
|
|
290
|
+
self.generator = False
|
|
291
|
+
self.bad_except = False
|
|
292
|
+
self.global_nonlocal = False
|
|
293
|
+
self.for_tuple_vars: dict[str, ast.expr] = {}
|
|
294
|
+
# unpacked-var usage classification and numeric-function evidence,
|
|
295
|
+
# computed after the visit pass (see _classify_tuple_vars)
|
|
296
|
+
self.scalar_ok: dict[str, bool] = {}
|
|
297
|
+
self.has_float_evidence = False
|
|
298
|
+
self._arg_names = {x.arg for x in (
|
|
299
|
+
*func.args.posonlyargs, *func.args.args)}
|
|
300
|
+
self._parent: dict[int, ast.AST] = {}
|
|
301
|
+
for node in ast.walk(func):
|
|
302
|
+
for child in ast.iter_child_nodes(node):
|
|
303
|
+
self._parent[id(child)] = node
|
|
304
|
+
for child in ast.iter_child_nodes(func):
|
|
305
|
+
self._visit(child)
|
|
306
|
+
self._classify_tuple_vars()
|
|
307
|
+
|
|
308
|
+
def _classify_tuple_vars(self) -> None:
|
|
309
|
+
"""For each flat-unpacked for variable, check whether every use is a
|
|
310
|
+
scalar-numeric context (arithmetic operand / comparison / index /
|
|
311
|
+
container element). A var used as a subscript *base*, attribute base,
|
|
312
|
+
call target/arg or truthiness operand disqualifies (it is a container
|
|
313
|
+
or escapes to unknown code). Also records whether the function shows
|
|
314
|
+
any float evidence (float literal or division), which gates the
|
|
315
|
+
aggressive unpack inference in _Solver.solve Phase 3."""
|
|
316
|
+
self.has_float_evidence = self.has_divmod or any(
|
|
317
|
+
isinstance(n, ast.Constant) and isinstance(n.value, float)
|
|
318
|
+
for n in ast.walk(self.func))
|
|
319
|
+
for name in self.for_tuple_vars:
|
|
320
|
+
ok = True
|
|
321
|
+
used = False
|
|
322
|
+
for node in ast.walk(self.func):
|
|
323
|
+
if not (isinstance(node, ast.Name) and node.id == name
|
|
324
|
+
and isinstance(node.ctx, ast.Load)):
|
|
325
|
+
continue
|
|
326
|
+
used = True
|
|
327
|
+
parent = self.parent_of(node)
|
|
328
|
+
bad = True
|
|
329
|
+
if isinstance(parent, (ast.BinOp, ast.UnaryOp, ast.Compare,
|
|
330
|
+
ast.Tuple, ast.List, ast.Set)):
|
|
331
|
+
bad = False
|
|
332
|
+
elif isinstance(parent, ast.Subscript):
|
|
333
|
+
# index position is fine; container base is not
|
|
334
|
+
bad = not any(n is node for n in ast.walk(parent.slice))
|
|
335
|
+
if bad:
|
|
336
|
+
ok = False
|
|
337
|
+
break
|
|
338
|
+
# vars never read after unpacking carry no type evidence; typing
|
|
339
|
+
# them double would TypeError on list/tuple values (nbody r/v1/v2)
|
|
340
|
+
self.scalar_ok[name] = ok and used
|
|
341
|
+
|
|
342
|
+
def parent_of(self, node):
|
|
343
|
+
return self._parent.get(id(node))
|
|
344
|
+
|
|
345
|
+
def _visit(self, node) -> None:
|
|
346
|
+
if isinstance(node, self._NESTED):
|
|
347
|
+
for sub in ast.walk(node):
|
|
348
|
+
if isinstance(sub, ast.Name) and isinstance(sub.ctx, ast.Load):
|
|
349
|
+
self.nested_captures.add(sub.id)
|
|
350
|
+
if sub.id in self._arg_names:
|
|
351
|
+
self.arg_ctx_bad.add(sub.id)
|
|
352
|
+
elif _call_name(sub) in _DYNAMIC_CALLS:
|
|
353
|
+
self.dynamic = True
|
|
354
|
+
elif isinstance(sub, ast.BinOp) and isinstance(
|
|
355
|
+
sub.op, (ast.Div, ast.FloorDiv, ast.Mod)):
|
|
356
|
+
self.has_divmod = True
|
|
357
|
+
return
|
|
358
|
+
if isinstance(node, self._COMPS):
|
|
359
|
+
for sub in ast.walk(node):
|
|
360
|
+
if isinstance(sub, ast.Name) and isinstance(sub.ctx, ast.Load) \
|
|
361
|
+
and sub.id in self._arg_names:
|
|
362
|
+
self.arg_ctx_bad.add(sub.id)
|
|
363
|
+
elif _call_name(sub) in _DYNAMIC_CALLS:
|
|
364
|
+
self.dynamic = True
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
if isinstance(node, (ast.Yield, ast.YieldFrom)):
|
|
368
|
+
self.generator = True
|
|
369
|
+
elif isinstance(node, (ast.Global, ast.Nonlocal)):
|
|
370
|
+
self.global_nonlocal = True
|
|
371
|
+
elif isinstance(node, ast.Call):
|
|
372
|
+
name = _call_name(node)
|
|
373
|
+
if name in _DYNAMIC_CALLS:
|
|
374
|
+
self.dynamic = True
|
|
375
|
+
if name == "range":
|
|
376
|
+
for arg in node.args:
|
|
377
|
+
if isinstance(arg, ast.Name):
|
|
378
|
+
self.range_arg_names.add(arg.id)
|
|
379
|
+
elif isinstance(node, ast.Assign):
|
|
380
|
+
for target in node.targets:
|
|
381
|
+
if isinstance(target, ast.Name):
|
|
382
|
+
self.assigns.setdefault(target.id, []).append(node.value)
|
|
383
|
+
elif isinstance(target, (ast.Tuple, ast.List)) \
|
|
384
|
+
and isinstance(node.value, ast.Tuple) \
|
|
385
|
+
and len(target.elts) == len(node.value.elts) \
|
|
386
|
+
and all(isinstance(e, ast.Name) for e in target.elts):
|
|
387
|
+
for elt, val in zip(target.elts, node.value.elts):
|
|
388
|
+
self.assigns.setdefault(elt.id, []).append(val)
|
|
389
|
+
elif isinstance(target, ast.Subscript) \
|
|
390
|
+
and isinstance(target.value, ast.Name):
|
|
391
|
+
# xs[i] = value: only the base name is opaque,
|
|
392
|
+
# the index variable is NOT being assigned to.
|
|
393
|
+
self.opaque_assigned.add(target.value.id)
|
|
394
|
+
else:
|
|
395
|
+
for sub in ast.walk(target):
|
|
396
|
+
if isinstance(sub, ast.Name):
|
|
397
|
+
self.opaque_assigned.add(sub.id)
|
|
398
|
+
elif isinstance(node, ast.AnnAssign):
|
|
399
|
+
if isinstance(node.target, ast.Name) and node.value is not None:
|
|
400
|
+
self.assigns.setdefault(node.target.id, []).append(node.value)
|
|
401
|
+
elif isinstance(node.target, ast.Subscript) \
|
|
402
|
+
and isinstance(node.target.value, ast.Name):
|
|
403
|
+
self.opaque_assigned.add(node.target.value.id)
|
|
404
|
+
elif not isinstance(node.target, ast.Name):
|
|
405
|
+
for sub in ast.walk(node.target):
|
|
406
|
+
if isinstance(sub, ast.Name):
|
|
407
|
+
self.opaque_assigned.add(sub.id)
|
|
408
|
+
elif isinstance(node, ast.AugAssign):
|
|
409
|
+
if isinstance(node.target, ast.Name):
|
|
410
|
+
self.aug.setdefault(node.target.id, []).append(
|
|
411
|
+
(type(node.op), node.value))
|
|
412
|
+
elif isinstance(node.target, ast.Subscript) \
|
|
413
|
+
and isinstance(node.target.value, ast.Name):
|
|
414
|
+
# xs[i] -= value: only the base name is opaque,
|
|
415
|
+
# the index variable is NOT being assigned to.
|
|
416
|
+
self.opaque_assigned.add(node.target.value.id)
|
|
417
|
+
else:
|
|
418
|
+
for sub in ast.walk(node.target):
|
|
419
|
+
if isinstance(sub, ast.Name):
|
|
420
|
+
self.opaque_assigned.add(sub.id)
|
|
421
|
+
elif isinstance(node, ast.For):
|
|
422
|
+
if _is_range_call(node.iter) and isinstance(node.target, ast.Name):
|
|
423
|
+
self.range_fors[node.target.id] = node.iter
|
|
424
|
+
else:
|
|
425
|
+
# Try flat unpacking
|
|
426
|
+
flat = self._flatten_target(node.target)
|
|
427
|
+
if flat:
|
|
428
|
+
for nm in flat:
|
|
429
|
+
self.for_tuple_vars[nm] = node.iter
|
|
430
|
+
else:
|
|
431
|
+
for sub in ast.walk(node.target):
|
|
432
|
+
if isinstance(sub, ast.Name):
|
|
433
|
+
self.opaque_assigned.add(sub.id)
|
|
434
|
+
for sub in ast.walk(node.iter):
|
|
435
|
+
if isinstance(sub, ast.Name) and sub.id in self._arg_names:
|
|
436
|
+
self.arg_ctx_bad.add(sub.id)
|
|
437
|
+
elif isinstance(node, ast.BinOp) and isinstance(
|
|
438
|
+
node.op, (ast.Div, ast.FloorDiv, ast.Mod)):
|
|
439
|
+
self.has_divmod = True
|
|
440
|
+
elif isinstance(node, ast.Attribute):
|
|
441
|
+
if isinstance(node.value, ast.Name):
|
|
442
|
+
self.complex_use.add(node.value.id)
|
|
443
|
+
if node.value.id in self._arg_names:
|
|
444
|
+
self.arg_ctx_bad.add(node.value.id)
|
|
445
|
+
elif isinstance(node, (ast.With, )):
|
|
446
|
+
for item in node.items:
|
|
447
|
+
if item.optional_vars is not None:
|
|
448
|
+
for sub in ast.walk(item.optional_vars):
|
|
449
|
+
if isinstance(sub, ast.Name):
|
|
450
|
+
self.opaque_assigned.add(sub.id)
|
|
451
|
+
elif isinstance(node, ast.Delete):
|
|
452
|
+
for t in node.targets:
|
|
453
|
+
if isinstance(t, ast.Name):
|
|
454
|
+
self.complex_use.add(t.id)
|
|
455
|
+
|
|
456
|
+
# Arg usage context checks
|
|
457
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) \
|
|
458
|
+
and node.id in self._arg_names:
|
|
459
|
+
parent = self.parent_of(node)
|
|
460
|
+
if isinstance(parent, (ast.Starred, ast.Await, ast.Yield,
|
|
461
|
+
ast.YieldFrom, ast.Delete)):
|
|
462
|
+
self.arg_ctx_bad.add(node.id)
|
|
463
|
+
elif isinstance(parent, ast.For) and parent.iter is node:
|
|
464
|
+
self.arg_ctx_bad.add(node.id)
|
|
465
|
+
elif isinstance(parent, ast.Call) and parent.func is node:
|
|
466
|
+
self.arg_ctx_bad.add(node.id)
|
|
467
|
+
elif isinstance(parent, ast.Subscript):
|
|
468
|
+
self.arg_ctx_bad.add(node.id)
|
|
469
|
+
|
|
470
|
+
for child in ast.iter_child_nodes(node):
|
|
471
|
+
self._visit(child)
|
|
472
|
+
|
|
473
|
+
@staticmethod
|
|
474
|
+
def _flatten_target(target):
|
|
475
|
+
if isinstance(target, ast.Name):
|
|
476
|
+
return [target.id]
|
|
477
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
478
|
+
result = []
|
|
479
|
+
for elt in target.elts:
|
|
480
|
+
flat = _Scanner._flatten_target(elt)
|
|
481
|
+
if flat is None:
|
|
482
|
+
return None
|
|
483
|
+
result.extend(flat)
|
|
484
|
+
return result
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
# Solver: fixed-point type inference for one function
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
class _Solver:
|
|
493
|
+
"""Runs the least-fixed-point int/float analysis on a _Scanner."""
|
|
494
|
+
|
|
495
|
+
def __init__(self, sc: _Scanner) -> None:
|
|
496
|
+
self.sc = sc
|
|
497
|
+
self.env_int: dict[str, int] = {} # name → bit bound
|
|
498
|
+
self.env_int_any: set[str] = set() # name → int (unbounded)
|
|
499
|
+
self.env_float: set[str] = set() # name → float
|
|
500
|
+
|
|
501
|
+
def is_float_expr(self, node) -> bool:
|
|
502
|
+
if _literal_float(node):
|
|
503
|
+
return True
|
|
504
|
+
if isinstance(node, ast.Name) and node.id in self.env_float:
|
|
505
|
+
# propagate through already-solved float vars (fixed-point safe:
|
|
506
|
+
# solve() re-evaluates until stable)
|
|
507
|
+
return True
|
|
508
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div):
|
|
509
|
+
return True
|
|
510
|
+
if _call_name(node) == "float":
|
|
511
|
+
return True
|
|
512
|
+
if isinstance(node, ast.BinOp):
|
|
513
|
+
return self.is_float_expr(node.left) or self.is_float_expr(node.right)
|
|
514
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
|
|
515
|
+
return self.is_float_expr(node.operand)
|
|
516
|
+
if isinstance(node, ast.IfExp):
|
|
517
|
+
return self.is_float_expr(node.body) or self.is_float_expr(node.orelse)
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
def _is_int_expr(self, node) -> bool:
|
|
521
|
+
if self.int_bits(node) is not None:
|
|
522
|
+
return True
|
|
523
|
+
if isinstance(node, ast.Name):
|
|
524
|
+
return (node.id in self.env_int_any
|
|
525
|
+
or node.id in self.sc.range_fors
|
|
526
|
+
or node.id in self.sc.range_arg_names)
|
|
527
|
+
if _call_name(node) == "int":
|
|
528
|
+
return True
|
|
529
|
+
if isinstance(node, ast.BinOp) and isinstance(
|
|
530
|
+
node.op, (ast.Add, ast.Sub, ast.Mult)):
|
|
531
|
+
return self._is_int_expr(node.left) and self._is_int_expr(node.right)
|
|
532
|
+
if isinstance(node, ast.UnaryOp) and isinstance(
|
|
533
|
+
node.op, (ast.USub, ast.UAdd)):
|
|
534
|
+
return self._is_int_expr(node.operand)
|
|
535
|
+
if isinstance(node, ast.IfExp):
|
|
536
|
+
return self._is_int_expr(node.body) and self._is_int_expr(node.orelse)
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
def int_bits(self, node):
|
|
540
|
+
"""Return bit bound b (|value| < 2**b) if provably bounded int."""
|
|
541
|
+
v = _literal_int(node)
|
|
542
|
+
if v is not None:
|
|
543
|
+
return _bit_len(v)
|
|
544
|
+
if isinstance(node, ast.Name):
|
|
545
|
+
return self.env_int.get(node.id)
|
|
546
|
+
if _call_name(node) == "len":
|
|
547
|
+
return 60
|
|
548
|
+
if _call_name(node) == "abs":
|
|
549
|
+
return self.int_bits(node.args[0]) if len(node.args) == 1 else None
|
|
550
|
+
if isinstance(node, ast.BinOp):
|
|
551
|
+
op = node.op
|
|
552
|
+
if isinstance(op, (ast.Add, ast.Sub)):
|
|
553
|
+
lb, rb = self.int_bits(node.left), self.int_bits(node.right)
|
|
554
|
+
if lb is not None and rb is not None:
|
|
555
|
+
return min(max(lb, rb) + 1, 63)
|
|
556
|
+
return None
|
|
557
|
+
if isinstance(op, ast.Mult):
|
|
558
|
+
lb, rb = self.int_bits(node.left), self.int_bits(node.right)
|
|
559
|
+
if lb is not None and rb is not None:
|
|
560
|
+
return min(lb + rb, 63)
|
|
561
|
+
return None
|
|
562
|
+
if isinstance(op, ast.FloorDiv):
|
|
563
|
+
lb = self.int_bits(node.left)
|
|
564
|
+
rv = _literal_int(node.right)
|
|
565
|
+
if lb is not None and rv is not None and rv != 0:
|
|
566
|
+
return lb
|
|
567
|
+
return None
|
|
568
|
+
if isinstance(op, ast.Mod):
|
|
569
|
+
rv = _literal_int(node.right)
|
|
570
|
+
if rv is not None and rv > 0:
|
|
571
|
+
return _bit_len(rv - 1)
|
|
572
|
+
return None
|
|
573
|
+
return None
|
|
574
|
+
if isinstance(node, ast.UnaryOp) and isinstance(
|
|
575
|
+
node.op, (ast.USub, ast.UAdd)):
|
|
576
|
+
inner = self.int_bits(node.operand)
|
|
577
|
+
return inner + 1 if inner is not None else None
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
def induction_bits(self, name) -> int | None:
|
|
581
|
+
rng = self.sc.range_fors.get(name)
|
|
582
|
+
if rng is None:
|
|
583
|
+
return None
|
|
584
|
+
bounds = [self.int_bits(a) for a in rng.args]
|
|
585
|
+
if not bounds or any(b is None for b in bounds):
|
|
586
|
+
if all(self._is_int_expr(a) for a in rng.args):
|
|
587
|
+
return 63
|
|
588
|
+
return None
|
|
589
|
+
return max(bounds)
|
|
590
|
+
|
|
591
|
+
def solve(self) -> None:
|
|
592
|
+
sc = self.sc
|
|
593
|
+
# Phase 0: seed range-for variables
|
|
594
|
+
for rname in sc.range_fors:
|
|
595
|
+
if rname in sc.opaque_assigned or rname in sc.complex_use:
|
|
596
|
+
continue
|
|
597
|
+
if rname in self.env_int:
|
|
598
|
+
continue
|
|
599
|
+
bits = self.induction_bits(rname)
|
|
600
|
+
if bits is not None and bits <= 63:
|
|
601
|
+
self.env_int[rname] = min(bits, _MAX_BITS)
|
|
602
|
+
|
|
603
|
+
# Phase 1: int analysis (fixed-point, 4 iterations)
|
|
604
|
+
# Iterate over the union of assigns and aug keys so that
|
|
605
|
+
# variables only modified via +=/-= (never directly assigned
|
|
606
|
+
# with =) are also processed — e.g. binary_trees' `depth` param.
|
|
607
|
+
all_names = set(sc.assigns.keys()) | set(sc.aug.keys())
|
|
608
|
+
for _ in range(4):
|
|
609
|
+
changed = False
|
|
610
|
+
for name in all_names:
|
|
611
|
+
if name in sc.opaque_assigned or name in sc.complex_use:
|
|
612
|
+
continue
|
|
613
|
+
if name in self.env_float or name in self.env_int \
|
|
614
|
+
or name in self.env_int_any:
|
|
615
|
+
continue
|
|
616
|
+
values = sc.assigns.get(name, [])
|
|
617
|
+
aug_vals = [v for _, v in sc.aug.get(name, [])]
|
|
618
|
+
all_v = list(values) + aug_vals
|
|
619
|
+
if not all_v:
|
|
620
|
+
continue
|
|
621
|
+
bits = [self.int_bits(v) for v in all_v]
|
|
622
|
+
if all(b is not None for b in bits):
|
|
623
|
+
safe_aug = all(
|
|
624
|
+
op in (ast.Add, ast.Sub, ast.Mult)
|
|
625
|
+
for op, _ in sc.aug.get(name, []))
|
|
626
|
+
if safe_aug:
|
|
627
|
+
bound = min(max(bits) +
|
|
628
|
+
(1 if sc.aug.get(name) else 0), 63)
|
|
629
|
+
if bound <= 63:
|
|
630
|
+
self.env_int[name] = min(bound, _MAX_BITS)
|
|
631
|
+
changed = True
|
|
632
|
+
elif all(self._is_int_expr(v) for v in all_v) and all(
|
|
633
|
+
op in (ast.Add, ast.Sub, ast.Mult)
|
|
634
|
+
for op, _ in sc.aug.get(name, [])):
|
|
635
|
+
self.env_int_any.add(name)
|
|
636
|
+
changed = True
|
|
637
|
+
if not changed:
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
# Phase 2 + Phase 3: float analysis, iterated to a fixed point so
|
|
641
|
+
# inferences propagate through the name chain (mag -> b1m -> dx ...).
|
|
642
|
+
# Max 5 rounds; each round only adds names so it terminates.
|
|
643
|
+
for _round in range(5):
|
|
644
|
+
changed = False
|
|
645
|
+
for name in all_names:
|
|
646
|
+
if name in sc.opaque_assigned or name in sc.complex_use:
|
|
647
|
+
continue
|
|
648
|
+
if name in self.env_float or name in self.env_int \
|
|
649
|
+
or name in self.env_int_any:
|
|
650
|
+
continue
|
|
651
|
+
values = sc.assigns.get(name, [])
|
|
652
|
+
aug_vals = [v for _, v in sc.aug.get(name, [])]
|
|
653
|
+
all_v = list(values) + aug_vals
|
|
654
|
+
if any(self.is_float_expr(v) for v in all_v):
|
|
655
|
+
all_numeric = True
|
|
656
|
+
for v in all_v:
|
|
657
|
+
if not (self.is_float_expr(v) or self._is_int_expr(v)):
|
|
658
|
+
all_numeric = False
|
|
659
|
+
break
|
|
660
|
+
if all_numeric:
|
|
661
|
+
self.env_float.add(name)
|
|
662
|
+
changed = True
|
|
663
|
+
|
|
664
|
+
# Phase 3: for-tuple-var float inference
|
|
665
|
+
for name, iter_expr in sc.for_tuple_vars.items():
|
|
666
|
+
if name in self.env_float or name in self.env_int \
|
|
667
|
+
or name in sc.opaque_assigned or name in sc.complex_use:
|
|
668
|
+
continue
|
|
669
|
+
# If iterating over a list of floats (list literal or list comp)
|
|
670
|
+
if isinstance(iter_expr, (ast.List, ast.Tuple)) and iter_expr.elts:
|
|
671
|
+
if any(self.is_float_expr(e) for e in iter_expr.elts):
|
|
672
|
+
self.env_float.add(name)
|
|
673
|
+
changed = True
|
|
674
|
+
elif isinstance(iter_expr, ast.ListComp):
|
|
675
|
+
if self.is_float_expr(iter_expr.elt):
|
|
676
|
+
self.env_float.add(name)
|
|
677
|
+
changed = True
|
|
678
|
+
elif isinstance(iter_expr, (ast.Name, ast.Attribute, ast.Call,
|
|
679
|
+
ast.Subscript)):
|
|
680
|
+
# Aggressive: iterating over an opaque sequence (e.g. a
|
|
681
|
+
# precomputed pairs list). If every use of the var is a
|
|
682
|
+
# scalar-numeric context and the function shows float
|
|
683
|
+
# evidence, type the var as double. Cython coerces the
|
|
684
|
+
# unpacked PyObject once per iteration; int values used in
|
|
685
|
+
# float math are converted losslessly for |v| < 2^53.
|
|
686
|
+
if sc.scalar_ok.get(name) and sc.has_float_evidence:
|
|
687
|
+
self.env_float.add(name)
|
|
688
|
+
changed = True
|
|
689
|
+
if not changed:
|
|
690
|
+
break
|
|
691
|
+
|
|
692
|
+
@property
|
|
693
|
+
def typed_locals(self) -> dict[str, str]:
|
|
694
|
+
result = {}
|
|
695
|
+
for name, bits in self.env_int.items():
|
|
696
|
+
result[name] = LONGLONG
|
|
697
|
+
for name in self.env_int_any:
|
|
698
|
+
result[name] = LONGLONG
|
|
699
|
+
for name in self.env_float:
|
|
700
|
+
result[name] = DOUBLE
|
|
701
|
+
return result
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# ---------------------------------------------------------------------------
|
|
705
|
+
# ClassPlan
|
|
706
|
+
# ---------------------------------------------------------------------------
|
|
707
|
+
|
|
708
|
+
@dataclass
|
|
709
|
+
class ClassPlan:
|
|
710
|
+
node: ast.ClassDef
|
|
711
|
+
name: str
|
|
712
|
+
skip: bool = True
|
|
713
|
+
reason: str = ""
|
|
714
|
+
all_attrs: dict[str, str] = field(default_factory=dict)
|
|
715
|
+
needs_dict: bool = False
|
|
716
|
+
cfunc_methods: dict[str, str] = field(default_factory=dict) # method → fast_name
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
# Main Transformer
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
# -----------------------------------------------------------------
|
|
725
|
+
# Pass 7: SoA container unboxing (dev-validated in /tmp/unbox_dev)
|
|
726
|
+
# Pattern: module dict of 3-tuples -> list(dict.values()) -> pairs.
|
|
727
|
+
# Rewrites iterating functions to flat typed arrays behind a runtime
|
|
728
|
+
# snapshot guard with copy-in/copy-out sync and full fallback to the
|
|
729
|
+
# original functions (externally observable state stays CPython-identical).
|
|
730
|
+
# -----------------------------------------------------------------
|
|
731
|
+
|
|
732
|
+
class _PbFail(Exception):
|
|
733
|
+
pass
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _L(name):
|
|
737
|
+
return ast.Name(id=name, ctx=ast.Load())
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _slice_of(base, off=None):
|
|
741
|
+
"""base: ('name', n) or ('floordiv', n, k); off: int|None -> AST index expr."""
|
|
742
|
+
kind = base[0]
|
|
743
|
+
if kind == 'name':
|
|
744
|
+
inner = _L(base[1])
|
|
745
|
+
elif kind == 'floordiv':
|
|
746
|
+
inner = ast.BinOp(left=_L(base[1]), op=ast.FloorDiv(),
|
|
747
|
+
right=ast.Constant(value=base[2]))
|
|
748
|
+
else:
|
|
749
|
+
raise _PbFail("base kind")
|
|
750
|
+
if off is None:
|
|
751
|
+
return inner
|
|
752
|
+
return ast.BinOp(left=inner, op=ast.Add(), right=ast.Constant(value=off))
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _rd(arr, base, off):
|
|
756
|
+
return ast.Subscript(value=_L(arr), slice=_slice_of(base, off), ctx=ast.Load())
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _wr(arr, base, flat):
|
|
760
|
+
return ast.Subscript(value=_L(arr), slice=_slice_of(base, flat), ctx=ast.Store())
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _pb_mk_num_expr(tree: ast.Module):
|
|
764
|
+
"""Returns num_expr(e) -> bool: numeric-constant expression detector that
|
|
765
|
+
accepts literals, unary minus, and arithmetic over module-level numeric
|
|
766
|
+
constants (PI * 2, X * SOLAR_MASS...)."""
|
|
767
|
+
num_consts = set()
|
|
768
|
+
pending = []
|
|
769
|
+
for mod in tree.body:
|
|
770
|
+
if isinstance(mod, ast.Assign) and len(mod.targets) == 1 \
|
|
771
|
+
and isinstance(mod.targets[0], ast.Name):
|
|
772
|
+
mv = mod.value
|
|
773
|
+
if isinstance(mv, ast.Constant) and isinstance(mv.value, (int, float)) \
|
|
774
|
+
and not isinstance(mv.value, bool):
|
|
775
|
+
num_consts.add(mod.targets[0].id)
|
|
776
|
+
elif isinstance(mv, ast.UnaryOp) and isinstance(mv.operand, ast.Constant) \
|
|
777
|
+
and isinstance(mv.operand.value, (int, float)):
|
|
778
|
+
num_consts.add(mod.targets[0].id)
|
|
779
|
+
elif isinstance(mv, (ast.BinOp, ast.UnaryOp)):
|
|
780
|
+
pending.append((mod.targets[0].id, mv))
|
|
781
|
+
# fixpoint: allow arithmetic over already-numeric module constants
|
|
782
|
+
changed = True
|
|
783
|
+
while changed and pending:
|
|
784
|
+
changed = False
|
|
785
|
+
rest = []
|
|
786
|
+
for name, mv in pending:
|
|
787
|
+
names = {n.id for n in ast.walk(mv) if isinstance(n, ast.Name)}
|
|
788
|
+
_ALLOWED = (ast.Name, ast.Constant, ast.BinOp, ast.UnaryOp,
|
|
789
|
+
ast.operator, ast.unaryop, ast.expr_context)
|
|
790
|
+
kinds_ok = all(isinstance(n, _ALLOWED) for n in ast.walk(mv))
|
|
791
|
+
consts_ok = all(isinstance(n.value, (int, float))
|
|
792
|
+
and not isinstance(n.value, bool)
|
|
793
|
+
for n in ast.walk(mv) if isinstance(n, ast.Constant))
|
|
794
|
+
if kinds_ok and consts_ok and names <= num_consts:
|
|
795
|
+
num_consts.add(name)
|
|
796
|
+
changed = True
|
|
797
|
+
else:
|
|
798
|
+
rest.append((name, mv))
|
|
799
|
+
pending = rest
|
|
800
|
+
|
|
801
|
+
def num_expr(e):
|
|
802
|
+
if isinstance(e, ast.Constant):
|
|
803
|
+
return isinstance(e.value, (int, float)) and not isinstance(e.value, bool)
|
|
804
|
+
if isinstance(e, ast.Name):
|
|
805
|
+
return e.id in num_consts
|
|
806
|
+
if isinstance(e, ast.List):
|
|
807
|
+
return (len(e.elts) == 3
|
|
808
|
+
and all(num_expr(c) for c in e.elts))
|
|
809
|
+
if isinstance(e, ast.UnaryOp):
|
|
810
|
+
return num_expr(e.operand)
|
|
811
|
+
if isinstance(e, ast.BinOp):
|
|
812
|
+
return num_expr(e.left) and num_expr(e.right)
|
|
813
|
+
return False
|
|
814
|
+
return num_expr
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _pb_unbox_tree(tree: ast.Module, applied: list, cdefs: bool = False) -> bool:
|
|
818
|
+
# ── locate dict / system / pairs ───────────────────────────────────
|
|
819
|
+
dict_name = system_name = pairs_name = None
|
|
820
|
+
num_expr_cached = _pb_mk_num_expr(tree)
|
|
821
|
+
for node in tree.body:
|
|
822
|
+
if not (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
823
|
+
and isinstance(node.targets[0], ast.Name)):
|
|
824
|
+
continue
|
|
825
|
+
v = node.value
|
|
826
|
+
if not (isinstance(v, ast.Dict) and v.keys
|
|
827
|
+
and all(isinstance(k, ast.Constant) and isinstance(k.value, str)
|
|
828
|
+
for k in v.keys)):
|
|
829
|
+
continue
|
|
830
|
+
ok = True
|
|
831
|
+
for val in v.values:
|
|
832
|
+
if not (isinstance(val, ast.Tuple) and len(val.elts) == 3):
|
|
833
|
+
ok = False; break
|
|
834
|
+
for e in val.elts:
|
|
835
|
+
good = num_expr_cached(e)
|
|
836
|
+
if not good:
|
|
837
|
+
ok = False; break
|
|
838
|
+
if not ok:
|
|
839
|
+
break
|
|
840
|
+
if ok:
|
|
841
|
+
dict_name = node.targets[0].id
|
|
842
|
+
break
|
|
843
|
+
if dict_name is None:
|
|
844
|
+
return False
|
|
845
|
+
for node in tree.body:
|
|
846
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
847
|
+
and isinstance(node.targets[0], ast.Name)
|
|
848
|
+
and isinstance(node.value, ast.Call)
|
|
849
|
+
and isinstance(node.value.func, ast.Name)
|
|
850
|
+
and node.value.func.id == "list"
|
|
851
|
+
and len(node.value.args) == 1):
|
|
852
|
+
a0 = node.value.args[0]
|
|
853
|
+
base = None
|
|
854
|
+
if isinstance(a0, ast.Attribute) and a0.attr == "values":
|
|
855
|
+
base = a0.value # DICT.values
|
|
856
|
+
elif (isinstance(a0, ast.Call)
|
|
857
|
+
and isinstance(a0.func, ast.Attribute)
|
|
858
|
+
and a0.func.attr == "values"
|
|
859
|
+
and not a0.args and not a0.keywords):
|
|
860
|
+
base = a0.func.value # DICT.values()
|
|
861
|
+
if (base is not None and isinstance(base, ast.Name)
|
|
862
|
+
and base.id == dict_name):
|
|
863
|
+
system_name = node.targets[0].id
|
|
864
|
+
break
|
|
865
|
+
if system_name is None:
|
|
866
|
+
return False
|
|
867
|
+
for node in tree.body:
|
|
868
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
869
|
+
and isinstance(node.targets[0], ast.Name)
|
|
870
|
+
and node.targets[0].id not in (dict_name, system_name)):
|
|
871
|
+
names = {n.id for n in ast.walk(node.value) if isinstance(n, ast.Name)}
|
|
872
|
+
if system_name in names:
|
|
873
|
+
pairs_name = node.targets[0].id
|
|
874
|
+
break
|
|
875
|
+
if pairs_name is None:
|
|
876
|
+
return False
|
|
877
|
+
|
|
878
|
+
# ── rewrite each eligible function ─────────────────────────────────
|
|
879
|
+
rewrites = []
|
|
880
|
+
for node in tree.body:
|
|
881
|
+
if isinstance(node, ast.FunctionDef):
|
|
882
|
+
fast = _pb_rewrite_function(node, system_name, pairs_name, cdefs)
|
|
883
|
+
if fast is not None:
|
|
884
|
+
rewrites.append((node, fast))
|
|
885
|
+
if not rewrites:
|
|
886
|
+
return False
|
|
887
|
+
for orig, fast in rewrites:
|
|
888
|
+
orig.name = "__pb_orig_" + orig.name
|
|
889
|
+
tree.body.insert(tree.body.index(orig) + 1, fast)
|
|
890
|
+
tree.body.extend(_pb_build_snapshot(system_name, pairs_name))
|
|
891
|
+
applied.append("soa-unbox")
|
|
892
|
+
return True
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _pb_build_snapshot(system_name, pairs_name):
|
|
896
|
+
SRC = f'''
|
|
897
|
+
class _PbFallback(Exception):
|
|
898
|
+
pass
|
|
899
|
+
from array import array as __pb_array
|
|
900
|
+
__pb_pos = __pb_array('d')
|
|
901
|
+
__pb_vel = __pb_array('d')
|
|
902
|
+
__pb_mass = __pb_array('d')
|
|
903
|
+
__pb_pair = __pb_array('l')
|
|
904
|
+
__pb_idx_of = {{}}
|
|
905
|
+
__pb_nb = 0
|
|
906
|
+
__pb_npair = 0
|
|
907
|
+
__pb_ok = False
|
|
908
|
+
try:
|
|
909
|
+
__pb_nb = len({system_name})
|
|
910
|
+
for __bi in range(__pb_nb):
|
|
911
|
+
__b = {system_name}[__bi]
|
|
912
|
+
__pb_idx_of[id(__b)] = __bi
|
|
913
|
+
__pb_pos.extend(__b[0])
|
|
914
|
+
__pb_vel.extend(__b[1])
|
|
915
|
+
__pb_mass.append(__b[2])
|
|
916
|
+
for __p in {pairs_name}:
|
|
917
|
+
__pb_pair.append(__pb_idx_of[id(__p[0])])
|
|
918
|
+
__pb_pair.append(__pb_idx_of[id(__p[1])])
|
|
919
|
+
__pb_npair = len({pairs_name})
|
|
920
|
+
__pb_ok = True
|
|
921
|
+
except Exception:
|
|
922
|
+
__pb_ok = False
|
|
923
|
+
# aliases for cdef memoryview pickup inside rewritten functions
|
|
924
|
+
__pb_pos_src = __pb_pos
|
|
925
|
+
__pb_vel_src = __pb_vel
|
|
926
|
+
__pb_mass_src = __pb_mass
|
|
927
|
+
__pb_pair_src = __pb_pair
|
|
928
|
+
'''
|
|
929
|
+
return ast.parse(SRC).body
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _pb_rewrite_function(fn: ast.FunctionDef, system_name, pairs_name, cdefs=False):
|
|
933
|
+
refs = {n.id for n in ast.walk(fn) if isinstance(n, ast.Name)}
|
|
934
|
+
if system_name not in refs and pairs_name not in refs:
|
|
935
|
+
return None
|
|
936
|
+
params = [a.arg for a in fn.args.args]
|
|
937
|
+
# params whose default IS the canonical system/pairs object are treated
|
|
938
|
+
# as aliases of it inside this function (entry guard guarantees identity)
|
|
939
|
+
alias = {}
|
|
940
|
+
args_list = fn.args.args
|
|
941
|
+
defaults = fn.args.defaults
|
|
942
|
+
if defaults and len(defaults) <= len(args_list):
|
|
943
|
+
for a, dv in zip(args_list[len(args_list) - len(defaults):], defaults):
|
|
944
|
+
if isinstance(dv, ast.Name) and dv.id == system_name:
|
|
945
|
+
alias[a.arg] = system_name
|
|
946
|
+
elif isinstance(dv, ast.Name) and dv.id == pairs_name:
|
|
947
|
+
alias[a.arg] = pairs_name
|
|
948
|
+
|
|
949
|
+
cur: dict[str, tuple] = {}
|
|
950
|
+
|
|
951
|
+
def t_expr(e):
|
|
952
|
+
if isinstance(e, ast.Name):
|
|
953
|
+
b = cur.get(e.id)
|
|
954
|
+
if b is None:
|
|
955
|
+
return copy.deepcopy(e)
|
|
956
|
+
arr, base, off = b
|
|
957
|
+
return _rd(arr, base, off)
|
|
958
|
+
if isinstance(e, ast.Constant):
|
|
959
|
+
return copy.deepcopy(e)
|
|
960
|
+
if isinstance(e, ast.BinOp):
|
|
961
|
+
return ast.BinOp(left=t_expr(e.left), op=copy.deepcopy(e.op),
|
|
962
|
+
right=t_expr(e.right))
|
|
963
|
+
if isinstance(e, ast.UnaryOp):
|
|
964
|
+
return ast.UnaryOp(op=copy.deepcopy(e.op), operand=t_expr(e.operand))
|
|
965
|
+
if isinstance(e, ast.Call):
|
|
966
|
+
return ast.Call(func=t_expr(e.func),
|
|
967
|
+
args=[t_expr(a) for a in e.args],
|
|
968
|
+
keywords=[copy.deepcopy(k) for k in e.keywords])
|
|
969
|
+
if isinstance(e, ast.Attribute):
|
|
970
|
+
return ast.Attribute(value=t_expr(e.value), attr=e.attr, ctx=ast.Load())
|
|
971
|
+
raise _PbFail(f"expr {type(e).__name__}")
|
|
972
|
+
|
|
973
|
+
def t_sub_store(t):
|
|
974
|
+
# t: Subscript(Name, const-int) bound to a list slot
|
|
975
|
+
b = cur.get(t.value.id)
|
|
976
|
+
if b is None or b[2] is not None:
|
|
977
|
+
raise _PbFail("bad store target")
|
|
978
|
+
if not (isinstance(t.slice, ast.Constant) and isinstance(t.slice.value, int)
|
|
979
|
+
and not isinstance(t.slice.value, bool)):
|
|
980
|
+
raise _PbFail("non-const index")
|
|
981
|
+
arr, base, _ = b
|
|
982
|
+
return arr, base, t.slice.value
|
|
983
|
+
|
|
984
|
+
def t_stmts(s) -> list:
|
|
985
|
+
if isinstance(s, ast.For):
|
|
986
|
+
return t_for(s)
|
|
987
|
+
if isinstance(s, ast.Assign) and len(s.targets) == 1:
|
|
988
|
+
return t_assign(s)
|
|
989
|
+
if isinstance(s, ast.AugAssign):
|
|
990
|
+
return [t_augassign(s)]
|
|
991
|
+
if isinstance(s, ast.Expr):
|
|
992
|
+
return [ast.Expr(value=t_expr(s.value))]
|
|
993
|
+
if isinstance(s, ast.Return):
|
|
994
|
+
return [ast.Return(value=t_expr(s.value) if s.value is not None else None)]
|
|
995
|
+
if isinstance(s, ast.Pass):
|
|
996
|
+
return [s]
|
|
997
|
+
raise _PbFail(f"stmt {type(s).__name__}")
|
|
998
|
+
|
|
999
|
+
def t_for(node):
|
|
1000
|
+
it = node.iter
|
|
1001
|
+
# canonical iteration source: module name or guarded param alias
|
|
1002
|
+
src = None
|
|
1003
|
+
if isinstance(it, ast.Name):
|
|
1004
|
+
if it.id == pairs_name or alias.get(it.id) == pairs_name:
|
|
1005
|
+
src = 'pairs'
|
|
1006
|
+
elif it.id == system_name or alias.get(it.id) == system_name:
|
|
1007
|
+
src = 'bodies'
|
|
1008
|
+
# pairs loop
|
|
1009
|
+
if src == 'pairs':
|
|
1010
|
+
tA, tB = node.target.elts
|
|
1011
|
+
ok_unpack = (isinstance(node.target, ast.Tuple)
|
|
1012
|
+
and len(node.target.elts) == 2
|
|
1013
|
+
and all(isinstance(x, ast.Tuple) and len(x.elts) == 3
|
|
1014
|
+
and isinstance(x.elts[0], (ast.List, ast.Tuple))
|
|
1015
|
+
and len(x.elts[0].elts) == 3
|
|
1016
|
+
and all(isinstance(y, ast.Name) for y in x.elts[0].elts)
|
|
1017
|
+
and isinstance(x.elts[1], ast.Name)
|
|
1018
|
+
and isinstance(x.elts[2], ast.Name)
|
|
1019
|
+
for x in node.target.elts))
|
|
1020
|
+
if not ok_unpack:
|
|
1021
|
+
raise _PbFail("pairs unpack")
|
|
1022
|
+
saved = dict(cur)
|
|
1023
|
+
for k, nm in enumerate(tA.elts[0].elts):
|
|
1024
|
+
cur[nm.id] = ("__pb_pos", ("name", "__pb_a3"), k)
|
|
1025
|
+
cur[tA.elts[1].id] = ("__pb_vel", ("name", "__pb_a3"), None)
|
|
1026
|
+
cur[tA.elts[2].id] = ("__pb_mass", ("name", "__pb_a"), None)
|
|
1027
|
+
for k, nm in enumerate(tB.elts[0].elts):
|
|
1028
|
+
cur[nm.id] = ("__pb_pos", ("name", "__pb_b3"), k)
|
|
1029
|
+
cur[tB.elts[1].id] = ("__pb_vel", ("name", "__pb_b3"), None)
|
|
1030
|
+
cur[tB.elts[2].id] = ("__pb_mass", ("name", "__pb_b"), None)
|
|
1031
|
+
body = []
|
|
1032
|
+
for sub in node.body:
|
|
1033
|
+
body.extend(t_stmts(sub))
|
|
1034
|
+
cur.clear(); cur.update(saved)
|
|
1035
|
+
pre = ast.parse(
|
|
1036
|
+
"__pb_a = __pb_pair[2 * __pb_pi]\n"
|
|
1037
|
+
"__pb_b = __pb_pair[2 * __pb_pi + 1]\n"
|
|
1038
|
+
"__pb_a3 = __pb_a * 3\n"
|
|
1039
|
+
"__pb_b3 = __pb_b * 3\n").body
|
|
1040
|
+
return [ast.For(
|
|
1041
|
+
target=ast.Name(id="__pb_pi", ctx=ast.Store()),
|
|
1042
|
+
iter=ast.Call(func=_L("range"), args=[_L("__pb_npair")], keywords=[]),
|
|
1043
|
+
body=pre + body, orelse=[])]
|
|
1044
|
+
# bodies loop
|
|
1045
|
+
if src == 'bodies':
|
|
1046
|
+
t = node.target
|
|
1047
|
+
if not (isinstance(t, ast.Tuple) and len(t.elts) == 3
|
|
1048
|
+
and isinstance(t.elts[0], ast.Name)
|
|
1049
|
+
and isinstance(t.elts[1], ast.List) and len(t.elts[1].elts) == 3
|
|
1050
|
+
and all(isinstance(y, ast.Name) for y in t.elts[1].elts)
|
|
1051
|
+
and isinstance(t.elts[2], ast.Name)):
|
|
1052
|
+
raise _PbFail("bodies unpack")
|
|
1053
|
+
saved = dict(cur)
|
|
1054
|
+
cur[t.elts[0].id] = ("__pb_pos", ("name", "__pb_k3"), None)
|
|
1055
|
+
for k2, nm in enumerate(t.elts[1].elts):
|
|
1056
|
+
cur[nm.id] = ("__pb_vel", ("name", "__pb_k3"), k2)
|
|
1057
|
+
cur[t.elts[2].id] = ("__pb_mass", ("floordiv", "__pb_k3", 3), None)
|
|
1058
|
+
body = []
|
|
1059
|
+
for sub in node.body:
|
|
1060
|
+
body.extend(t_stmts(sub))
|
|
1061
|
+
cur.clear(); cur.update(saved)
|
|
1062
|
+
return [ast.For(
|
|
1063
|
+
target=ast.Name(id="__pb_k3", ctx=ast.Store()),
|
|
1064
|
+
iter=ast.Call(func=_L("range"),
|
|
1065
|
+
args=[ast.Constant(value=0),
|
|
1066
|
+
ast.BinOp(left=_L("__pb_nb"), op=ast.Mult(),
|
|
1067
|
+
right=ast.Constant(value=3)),
|
|
1068
|
+
ast.Constant(value=3)],
|
|
1069
|
+
keywords=[]),
|
|
1070
|
+
body=body, orelse=[])]
|
|
1071
|
+
# generic range loop: keep, translate body
|
|
1072
|
+
if isinstance(it, ast.Call) and isinstance(it.func, ast.Name) \
|
|
1073
|
+
and it.func.id == "range":
|
|
1074
|
+
body = []
|
|
1075
|
+
for sub in node.body:
|
|
1076
|
+
body.extend(t_stmts(sub))
|
|
1077
|
+
return [ast.For(target=copy.deepcopy(node.target),
|
|
1078
|
+
iter=copy.deepcopy(it), body=body, orelse=[])]
|
|
1079
|
+
raise _PbFail("for iter")
|
|
1080
|
+
|
|
1081
|
+
def t_assign(s):
|
|
1082
|
+
t = s.targets[0]
|
|
1083
|
+
# ref-unpack: (r, v, m) = someparam
|
|
1084
|
+
if isinstance(t, ast.Tuple) and isinstance(s.value, ast.Name) \
|
|
1085
|
+
and s.value.id in params:
|
|
1086
|
+
if not (len(t.elts) == 3 and isinstance(t.elts[0], ast.Name)
|
|
1087
|
+
and isinstance(t.elts[1], ast.Name) and isinstance(t.elts[2], ast.Name)):
|
|
1088
|
+
raise _PbFail("ref unpack")
|
|
1089
|
+
cur[t.elts[0].id] = ("__pb_pos", ("name", "__pb_ri3"), None)
|
|
1090
|
+
cur[t.elts[1].id] = ("__pb_vel", ("name", "__pb_ri3"), None)
|
|
1091
|
+
cur[t.elts[2].id] = ("__pb_mass", ("name", "__pb_ri"), None)
|
|
1092
|
+
return ast.parse(
|
|
1093
|
+
f"__pb_ri = __pb_idx_of.get(id({s.value.id}), -1)\n"
|
|
1094
|
+
"if __pb_ri < 0:\n"
|
|
1095
|
+
" raise _PbFallback()\n"
|
|
1096
|
+
"__pb_ri3 = __pb_ri * 3\n").body
|
|
1097
|
+
if isinstance(t, ast.Name):
|
|
1098
|
+
return [ast.Assign(targets=[copy.deepcopy(t)], value=t_expr(s.value))]
|
|
1099
|
+
if isinstance(t, ast.Subscript) and isinstance(t.value, ast.Name):
|
|
1100
|
+
arr, base, flat = t_sub_store(t)
|
|
1101
|
+
return [ast.Assign(targets=[_wr(arr, base, flat)], value=t_expr(s.value))]
|
|
1102
|
+
raise _PbFail("assign")
|
|
1103
|
+
|
|
1104
|
+
def t_augassign(s):
|
|
1105
|
+
t = s.target
|
|
1106
|
+
if isinstance(t, ast.Name):
|
|
1107
|
+
return ast.AugAssign(target=copy.deepcopy(t), op=copy.deepcopy(s.op),
|
|
1108
|
+
value=t_expr(s.value))
|
|
1109
|
+
if isinstance(t, ast.Subscript) and isinstance(t.value, ast.Name):
|
|
1110
|
+
arr, base, flat = t_sub_store(t)
|
|
1111
|
+
return ast.AugAssign(target=_wr(arr, base, flat), op=copy.deepcopy(s.op),
|
|
1112
|
+
value=t_expr(s.value))
|
|
1113
|
+
raise _PbFail("augassign")
|
|
1114
|
+
|
|
1115
|
+
try:
|
|
1116
|
+
new_body = []
|
|
1117
|
+
for s in fn.body:
|
|
1118
|
+
new_body.extend(t_stmts(s))
|
|
1119
|
+
except _PbFail:
|
|
1120
|
+
return None
|
|
1121
|
+
|
|
1122
|
+
# entry guard: not ok / args differ from canonical → call original
|
|
1123
|
+
guard_tests = [ast.UnaryOp(op=ast.Not(), operand=_L("__pb_ok"))]
|
|
1124
|
+
args_list = fn.args.args
|
|
1125
|
+
defaults = fn.args.defaults
|
|
1126
|
+
bodies_param = pairs_param = None
|
|
1127
|
+
if defaults and len(defaults) <= len(args_list):
|
|
1128
|
+
for a, dv in zip(args_list[len(args_list) - len(defaults):], defaults):
|
|
1129
|
+
if isinstance(dv, ast.Name) and dv.id in (system_name, pairs_name):
|
|
1130
|
+
guard_tests.append(ast.Compare(left=_L(a.arg), ops=[ast.IsNot()],
|
|
1131
|
+
comparators=[_L(dv.id)]))
|
|
1132
|
+
if dv.id == system_name:
|
|
1133
|
+
bodies_param = a.arg
|
|
1134
|
+
else:
|
|
1135
|
+
pairs_param = a.arg
|
|
1136
|
+
# entry sync (copy-in): pull the CURRENT values of the canonical
|
|
1137
|
+
# containers into the flat arrays so in-place mutations between calls
|
|
1138
|
+
# are visible; re-derive pair indices by identity, miss → fallback.
|
|
1139
|
+
# exit sync (copy-out): write mutated pos/vel back so external readers
|
|
1140
|
+
# observing the original lists see the same state CPython would leave.
|
|
1141
|
+
if bodies_param is None:
|
|
1142
|
+
bodies_param = system_name
|
|
1143
|
+
len_checks = [f"len({bodies_param}) != __pb_nb"]
|
|
1144
|
+
pairs_sync = ""
|
|
1145
|
+
if pairs_param is not None:
|
|
1146
|
+
len_checks.append(f"len({pairs_param}) != __pb_npair")
|
|
1147
|
+
pairs_sync = (
|
|
1148
|
+
f"for __pb_pj in range(__pb_npair):\n"
|
|
1149
|
+
f" __pb_ia = __pb_idx_of.get(id({pairs_param}[__pb_pj][0]), -1)\n"
|
|
1150
|
+
f" __pb_ib = __pb_idx_of.get(id({pairs_param}[__pb_pj][1]), -1)\n"
|
|
1151
|
+
f" if __pb_ia < 0 or __pb_ib < 0:\n"
|
|
1152
|
+
f" raise _PbFallback()\n"
|
|
1153
|
+
f" __pb_pair[2 * __pb_pj] = __pb_ia\n"
|
|
1154
|
+
f" __pb_pair[2 * __pb_pj + 1] = __pb_ib\n")
|
|
1155
|
+
entry_sync = ast.parse(
|
|
1156
|
+
f"if {' or '.join(len_checks)}:\n"
|
|
1157
|
+
f" raise _PbFallback()\n"
|
|
1158
|
+
f"for __pb_ci in range(__pb_nb):\n"
|
|
1159
|
+
f" __pb_b = {bodies_param}[__pb_ci]\n"
|
|
1160
|
+
f" __pb_pos[__pb_ci * 3 + 0] = __pb_b[0][0]\n"
|
|
1161
|
+
f" __pb_pos[__pb_ci * 3 + 1] = __pb_b[0][1]\n"
|
|
1162
|
+
f" __pb_pos[__pb_ci * 3 + 2] = __pb_b[0][2]\n"
|
|
1163
|
+
f" __pb_vel[__pb_ci * 3 + 0] = __pb_b[1][0]\n"
|
|
1164
|
+
f" __pb_vel[__pb_ci * 3 + 1] = __pb_b[1][1]\n"
|
|
1165
|
+
f" __pb_vel[__pb_ci * 3 + 2] = __pb_b[1][2]\n"
|
|
1166
|
+
f" __pb_mass[__pb_ci] = __pb_b[2]\n"
|
|
1167
|
+
f"{pairs_sync}").body
|
|
1168
|
+
exit_sync = ast.parse(
|
|
1169
|
+
f"for __pb_co in range(__pb_nb):\n"
|
|
1170
|
+
f" __pb_b = {bodies_param}[__pb_co]\n"
|
|
1171
|
+
f" __pb_b[0][0] = __pb_pos[__pb_co * 3 + 0]\n"
|
|
1172
|
+
f" __pb_b[0][1] = __pb_pos[__pb_co * 3 + 1]\n"
|
|
1173
|
+
f" __pb_b[0][2] = __pb_pos[__pb_co * 3 + 2]\n"
|
|
1174
|
+
f" __pb_b[1][0] = __pb_vel[__pb_co * 3 + 0]\n"
|
|
1175
|
+
f" __pb_b[1][1] = __pb_vel[__pb_co * 3 + 1]\n"
|
|
1176
|
+
f" __pb_b[1][2] = __pb_vel[__pb_co * 3 + 2]\n").body
|
|
1177
|
+
test = ast.BoolOp(op=ast.Or(), values=guard_tests)
|
|
1178
|
+
fallback_call = ast.Return(value=ast.Call(
|
|
1179
|
+
func=_L("__pb_orig_" + fn.name),
|
|
1180
|
+
args=[_L(a) for a in params], keywords=[]))
|
|
1181
|
+
guard_if = ast.If(test=test, body=[fallback_call], orelse=[])
|
|
1182
|
+
# try/except _PbFallback → original (for ref-identity misses)
|
|
1183
|
+
try_body = entry_sync + new_body + exit_sync
|
|
1184
|
+
catch = [ast.Return(value=ast.Call(
|
|
1185
|
+
func=_L("__pb_orig_" + fn.name),
|
|
1186
|
+
args=[_L(a) for a in params], keywords=[]))]
|
|
1187
|
+
wrapped = ast.Try(
|
|
1188
|
+
body=try_body,
|
|
1189
|
+
handlers=[ast.ExceptHandler(type=_L("_PbFallback"), name=None, body=catch)],
|
|
1190
|
+
orelse=[], finalbody=[])
|
|
1191
|
+
|
|
1192
|
+
dec = ast.parse(
|
|
1193
|
+
"@cython.boundscheck(False)\n"
|
|
1194
|
+
"@cython.wraparound(False)\n"
|
|
1195
|
+
"@cython.cdivision(True)\n"
|
|
1196
|
+
"@cython.locals(__pb_pi=cython.longlong, __pb_a=cython.longlong, "
|
|
1197
|
+
"__pb_ci=cython.longlong, __pb_co=cython.longlong, __pb_pj=cython.longlong, "
|
|
1198
|
+
"__pb_ia=cython.longlong, __pb_ib=cython.longlong, "
|
|
1199
|
+
"__pb_a3=cython.longlong, __pb_b3=cython.longlong, "
|
|
1200
|
+
"__pb_k3=cython.longlong, __pb_ri=cython.longlong, __pb_ri3=cython.longlong, "
|
|
1201
|
+
"dt=cython.double, dx=cython.double, dy=cython.double, dz=cython.double, "
|
|
1202
|
+
"mag=cython.double, b1m=cython.double, b2m=cython.double, "
|
|
1203
|
+
"m1=cython.double, m2=cython.double, vx=cython.double, vy=cython.double, "
|
|
1204
|
+
"vz=cython.double, px=cython.double, py=cython.double, pz=cython.double, "
|
|
1205
|
+
"e=cython.double, x1=cython.double, x2=cython.double, y1=cython.double, "
|
|
1206
|
+
"y2=cython.double, z1=cython.double, z2=cython.double)\n"
|
|
1207
|
+
"def _x():\n pass\n").body[0].decorator_list
|
|
1208
|
+
|
|
1209
|
+
# cdef memoryviews give C-speed element access on the array('d'/'l') buffers.
|
|
1210
|
+
# Python's ast cannot parse "cdef" (Cython syntax), so we emit a textual
|
|
1211
|
+
# marker statement; the pyx builder string-replaces it before compilation.
|
|
1212
|
+
# (plain assignment is harmless on the pure-Python semantic path)
|
|
1213
|
+
mv = [ast.parse("__pb_cdef_marker = 0").body[0]]
|
|
1214
|
+
|
|
1215
|
+
fast = ast.FunctionDef(
|
|
1216
|
+
name=fn.name, args=copy.deepcopy(fn.args),
|
|
1217
|
+
body=mv + [guard_if, wrapped], decorator_list=dec,
|
|
1218
|
+
type_comment=None, lineno=fn.lineno, col_offset=fn.col_offset)
|
|
1219
|
+
return fast
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
# -----------------------------------------------------------------
|
|
1226
|
+
# Pass 8: integer-list unboxing (fannkuch shape, dev-validated)
|
|
1227
|
+
# V=list(range(n)) + slice-flip P[:k+1]=P[k::-1] + bound insert/pop
|
|
1228
|
+
# rotate. Rewrites to array('q') + loops + typed views. Markers:
|
|
1229
|
+
# __pb_type_X = 0 → add X=cython.longlong to locals (stripped pre-unparse)
|
|
1230
|
+
# -----------------------------------------------------------------
|
|
1231
|
+
|
|
1232
|
+
_FK_PASS_VARS = ("__pb_i", "__pb_j", "__pb_t", "__pb_p0")
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
class _FkFail(Exception):
|
|
1236
|
+
pass
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def _fk_unbox_tree(tree, applied):
|
|
1240
|
+
changed = False
|
|
1241
|
+
for fn in [n for n in tree.body if isinstance(n, ast.FunctionDef)]:
|
|
1242
|
+
try:
|
|
1243
|
+
changed |= _fk_rewrite_fn(fn)
|
|
1244
|
+
except _FkFail:
|
|
1245
|
+
continue
|
|
1246
|
+
if changed:
|
|
1247
|
+
applied.append("fannkuch-unbox")
|
|
1248
|
+
return changed
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def _fk_rewrite_fn(fn):
|
|
1252
|
+
changed = False
|
|
1253
|
+
used = {n.id for n in ast.walk(fn) if isinstance(n, ast.Name)}
|
|
1254
|
+
if any(v in used for v in _FK_PASS_VARS):
|
|
1255
|
+
return False
|
|
1256
|
+
|
|
1257
|
+
# ── 1. flip-loopify ─────────────────────────────────────────────
|
|
1258
|
+
flips = _fk_find_flips(fn)
|
|
1259
|
+
for stmt, var, kupper, kval in flips:
|
|
1260
|
+
_fk_replace_stmt(fn, stmt, _fk_build_swap(var, kval))
|
|
1261
|
+
changed = True
|
|
1262
|
+
|
|
1263
|
+
# ── 2. rotate-loop: X_ins(R, X_pop(0)) → shift ──────────────────
|
|
1264
|
+
binds = {} # name -> (listvar, method)
|
|
1265
|
+
bind_stmts = {} # bindname -> the Assign stmt (to drop after rewrite)
|
|
1266
|
+
for node in ast.walk(fn):
|
|
1267
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
1268
|
+
and isinstance(node.targets[0], ast.Name)
|
|
1269
|
+
and isinstance(node.value, ast.Attribute)
|
|
1270
|
+
and isinstance(node.value.value, ast.Name)
|
|
1271
|
+
and node.value.attr in ("insert", "pop")):
|
|
1272
|
+
binds[node.targets[0].id] = (node.value.value.id, node.value.attr)
|
|
1273
|
+
bind_stmts[node.targets[0].id] = node
|
|
1274
|
+
rewrites = []
|
|
1275
|
+
for node in ast.walk(fn):
|
|
1276
|
+
call = node.value if isinstance(node, (ast.Expr, ast.Assign)) else None
|
|
1277
|
+
if not (isinstance(call, ast.Call)
|
|
1278
|
+
and isinstance(call.func, ast.Name)
|
|
1279
|
+
and call.func.id in binds
|
|
1280
|
+
and len(call.args) == 2
|
|
1281
|
+
and isinstance(call.args[1], ast.Call)
|
|
1282
|
+
and isinstance(call.args[1].func, ast.Name)
|
|
1283
|
+
and call.args[1].func.id in binds):
|
|
1284
|
+
continue
|
|
1285
|
+
ins_bind = binds[call.func.id]
|
|
1286
|
+
pop_bind = binds[call.args[1].func.id]
|
|
1287
|
+
if not (ins_bind[1] == "insert" and pop_bind[1] == "pop"
|
|
1288
|
+
and ins_bind[0] == pop_bind[0]
|
|
1289
|
+
and (call.args[1].args == []
|
|
1290
|
+
or (len(call.args[1].args) == 1
|
|
1291
|
+
and isinstance(call.args[1].args[0], ast.Constant)
|
|
1292
|
+
and call.args[1].args[0].value == 0))):
|
|
1293
|
+
continue
|
|
1294
|
+
if not isinstance(node, ast.Expr):
|
|
1295
|
+
raise _FkFail("rotate result assigned")
|
|
1296
|
+
# X_ins(R, X_pop(0)) → __pb_p0 = X[0]
|
|
1297
|
+
# for __pb_i in range(R): X[__pb_i] = X[__pb_i + 1]
|
|
1298
|
+
# X[R] = __pb_p0
|
|
1299
|
+
X, R = ins_bind[0], call.args[0]
|
|
1300
|
+
shift = ast.parse(
|
|
1301
|
+
f"__pb_p0 = {X}[0]\n"
|
|
1302
|
+
"for __pb_i in range(RP):\n"
|
|
1303
|
+
f" {X}[__pb_i] = {X}[__pb_i + 1]\n"
|
|
1304
|
+
f"{X}[RP] = __pb_p0\n")
|
|
1305
|
+
shift.body[1].iter.args[0] = R
|
|
1306
|
+
shift.body[2].targets[0].slice = R
|
|
1307
|
+
rewrites.append((node, shift.body))
|
|
1308
|
+
changed = True
|
|
1309
|
+
for old, new in rewrites:
|
|
1310
|
+
_fk_replace_stmt(fn, old, new)
|
|
1311
|
+
# drop the now-unused bind assignments
|
|
1312
|
+
for name, stmt in bind_stmts.items():
|
|
1313
|
+
still_used = any(isinstance(n, ast.Name) and n.id == name
|
|
1314
|
+
for n in ast.walk(fn) if n is not stmt.targets[0])
|
|
1315
|
+
if not still_used:
|
|
1316
|
+
try:
|
|
1317
|
+
_fk_replace_stmt(fn, stmt, [])
|
|
1318
|
+
except _FkFail:
|
|
1319
|
+
pass
|
|
1320
|
+
|
|
1321
|
+
# ── 3. array-unbox: list(range) vars → array('q', range) ────────
|
|
1322
|
+
rng_vars = set()
|
|
1323
|
+
for node in ast.walk(fn):
|
|
1324
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
1325
|
+
and isinstance(node.targets[0], ast.Name)
|
|
1326
|
+
and isinstance(node.value, ast.Call)
|
|
1327
|
+
and isinstance(node.value.func, ast.Name)
|
|
1328
|
+
and node.value.func.id == "list"
|
|
1329
|
+
and len(node.value.args) == 1
|
|
1330
|
+
and isinstance(node.value.args[0], ast.Call)
|
|
1331
|
+
and isinstance(node.value.args[0].func, ast.Name)
|
|
1332
|
+
and node.value.args[0].func.id == "range"):
|
|
1333
|
+
rng_vars.add(node.targets[0].id)
|
|
1334
|
+
ok_vars = {v for v in rng_vars if _fk_array_safe(fn, v)}
|
|
1335
|
+
for node in ast.walk(fn):
|
|
1336
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
1337
|
+
and isinstance(node.targets[0], ast.Name)
|
|
1338
|
+
and node.targets[0].id in ok_vars
|
|
1339
|
+
and isinstance(node.value, ast.Call)
|
|
1340
|
+
and isinstance(node.value.func, ast.Name)
|
|
1341
|
+
and node.value.func.id == "list"):
|
|
1342
|
+
rng_call = node.value.args[0]
|
|
1343
|
+
node.value = ast.Call(
|
|
1344
|
+
func=ast.Name(id="__pb_array", ctx=ast.Load()),
|
|
1345
|
+
args=[ast.Constant(value="q"), rng_call], keywords=[])
|
|
1346
|
+
changed = True
|
|
1347
|
+
if changed:
|
|
1348
|
+
# 3a. slice-copy W = V[:] (both unboxed) → elementwise copy loop
|
|
1349
|
+
# (avoids per-flip array object + view allocation in compiled form;
|
|
1350
|
+
# identical semantics for any sequence of equal length)
|
|
1351
|
+
unboxed = ok_vars
|
|
1352
|
+
for node in ast.walk(fn):
|
|
1353
|
+
if not (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
1354
|
+
and isinstance(node.targets[0], ast.Name)
|
|
1355
|
+
and isinstance(node.value, ast.Subscript)
|
|
1356
|
+
and isinstance(node.value.value, ast.Name)
|
|
1357
|
+
and isinstance(node.value.slice, ast.Slice)
|
|
1358
|
+
and node.value.slice.lower is None
|
|
1359
|
+
and node.value.slice.upper is None
|
|
1360
|
+
and node.value.slice.step is None
|
|
1361
|
+
and node.targets[0].id in unboxed
|
|
1362
|
+
and node.value.value.id in unboxed):
|
|
1363
|
+
continue
|
|
1364
|
+
src_name = node.value.value.id
|
|
1365
|
+
dst_name = node.targets[0].id
|
|
1366
|
+
node.value = ast.parse(
|
|
1367
|
+
f"[0] * len({src_name})", mode="eval").body # placeholder; replaced below
|
|
1368
|
+
# build: for __pb_i in range(len(S)): D[__pb_i] = S[__pb_i]
|
|
1369
|
+
loop = ast.parse(
|
|
1370
|
+
"for __pb_i in range(0):\n"
|
|
1371
|
+
f" {dst_name}[__pb_i] = {src_name}[__pb_i]\n")
|
|
1372
|
+
loop.body[0].iter.args[0] = ast.Call(
|
|
1373
|
+
func=ast.Name(id="len", ctx=ast.Load()),
|
|
1374
|
+
args=[ast.Name(id=src_name, ctx=ast.Load())], keywords=[])
|
|
1375
|
+
# replace the (now placeholder) assign stmt with loop
|
|
1376
|
+
_fk_replace_stmt(fn, node, loop.body)
|
|
1377
|
+
# 3b. register loop temps for typing (markers consumed by engine hook)
|
|
1378
|
+
k_candidates = set()
|
|
1379
|
+
for node in ast.walk(fn):
|
|
1380
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
1381
|
+
and isinstance(node.targets[0], ast.Name)
|
|
1382
|
+
and node.targets[0].id in ok_vars
|
|
1383
|
+
and isinstance(node.value, ast.Call)
|
|
1384
|
+
and isinstance(node.value.func, ast.Name)
|
|
1385
|
+
and node.value.func.id == "__pb_array"):
|
|
1386
|
+
r0 = node.value.args[1].args[0] if (
|
|
1387
|
+
isinstance(node.value.args[1], ast.Call)
|
|
1388
|
+
and isinstance(node.value.args[1].func, ast.Name)
|
|
1389
|
+
and node.value.args[1].func.id == "range"
|
|
1390
|
+
and len(node.value.args[1].args) == 1) else None
|
|
1391
|
+
if isinstance(r0, ast.Name):
|
|
1392
|
+
k_candidates.add(r0.id)
|
|
1393
|
+
for node in ast.walk(fn):
|
|
1394
|
+
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
|
1395
|
+
and isinstance(node.targets[0], ast.Name)
|
|
1396
|
+
and isinstance(node.value, ast.Subscript)
|
|
1397
|
+
and isinstance(node.value.value, ast.Name)
|
|
1398
|
+
and node.value.value.id in ok_vars):
|
|
1399
|
+
k_candidates.add(node.targets[0].id)
|
|
1400
|
+
for t in tuple({"__pb_i", "__pb_j", "__pb_t", "__pb_p0"} | k_candidates):
|
|
1401
|
+
if any(isinstance(n, ast.Name) and n.id == t for n in ast.walk(fn)):
|
|
1402
|
+
fn.body.append(ast.parse(f"__pb_type_{t} = 0"))
|
|
1403
|
+
return changed
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def _fk_array_safe(fn, v):
|
|
1407
|
+
"""All uses of v: element get/set, slice-copy source/target, len arg,
|
|
1408
|
+
or a bound-method capture (insert/pop) whose calls were rewritten."""
|
|
1409
|
+
for node in ast.walk(fn):
|
|
1410
|
+
if isinstance(node, ast.Subscript):
|
|
1411
|
+
if isinstance(node.value, ast.Name) and node.value.id == v:
|
|
1412
|
+
continue # element get/set (any slice is copy form)
|
|
1413
|
+
continue
|
|
1414
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) \
|
|
1415
|
+
and node.value.id == v and node.attr in ("insert", "pop"):
|
|
1416
|
+
continue # bound-method capture, handled by rotate pass
|
|
1417
|
+
if isinstance(node, ast.Compare) and isinstance(node.left, ast.Name) \
|
|
1418
|
+
and node.left.id == v:
|
|
1419
|
+
return False
|
|
1420
|
+
if isinstance(node, ast.BinOp) and isinstance(node.left, ast.Name) \
|
|
1421
|
+
and node.left.id == v:
|
|
1422
|
+
return False
|
|
1423
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) \
|
|
1424
|
+
and node.func.id != "len":
|
|
1425
|
+
for a in node.args:
|
|
1426
|
+
if isinstance(a, ast.Name) and a.id == v:
|
|
1427
|
+
return False # passed to arbitrary function
|
|
1428
|
+
return True
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
# ---- shared helpers (same as fk_dev) ------------------------------------
|
|
1432
|
+
|
|
1433
|
+
def _fk_find_flips(fn):
|
|
1434
|
+
flips = []
|
|
1435
|
+
for node in ast.walk(fn):
|
|
1436
|
+
if not (isinstance(node, ast.Assign) and len(node.targets) == 1):
|
|
1437
|
+
continue
|
|
1438
|
+
t, v = node.targets[0], node.value
|
|
1439
|
+
if (isinstance(t, ast.Subscript) and isinstance(t.value, ast.Name)
|
|
1440
|
+
and isinstance(t.slice, ast.Slice) and _fk_is_from0(t.slice)
|
|
1441
|
+
and isinstance(v, ast.Subscript) and isinstance(v.value, ast.Name)
|
|
1442
|
+
and isinstance(v.slice, ast.Slice) and _fk_is_rev_full(v.slice)
|
|
1443
|
+
and v.value.id == t.value.id):
|
|
1444
|
+
flips.append((node, t.value.id, t.slice.upper, v.slice.lower))
|
|
1445
|
+
return flips
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
def _fk_is_from0(sl):
|
|
1449
|
+
return (sl.lower is None
|
|
1450
|
+
or (isinstance(sl.lower, ast.Constant) and sl.lower.value == 0)) \
|
|
1451
|
+
and sl.upper is not None and sl.step is None
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def _fk_is_rev_full(sl):
|
|
1455
|
+
if sl.upper is not None:
|
|
1456
|
+
return False
|
|
1457
|
+
st = sl.step
|
|
1458
|
+
if isinstance(st, ast.Constant):
|
|
1459
|
+
return st.value == -1
|
|
1460
|
+
return (isinstance(st, ast.UnaryOp) and isinstance(st.op, ast.USub)
|
|
1461
|
+
and isinstance(st.operand, ast.Constant) and st.operand.value == 1)
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def _fk_build_swap(var, kval):
|
|
1465
|
+
swap = ast.parse(
|
|
1466
|
+
"__pb_i = 0\n"
|
|
1467
|
+
"__pb_j = 0\n"
|
|
1468
|
+
"while __pb_i < __pb_j:\n"
|
|
1469
|
+
f" __pb_t = {var}[__pb_i]\n"
|
|
1470
|
+
f" {var}[__pb_i] = {var}[__pb_j]\n"
|
|
1471
|
+
f" {var}[__pb_j] = __pb_t\n"
|
|
1472
|
+
" __pb_i += 1\n"
|
|
1473
|
+
" __pb_j -= 1\n")
|
|
1474
|
+
swap.body[1].value = kval
|
|
1475
|
+
return swap.body
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def _fk_replace_stmt(fn, old, new_stmts):
|
|
1479
|
+
def rec(body):
|
|
1480
|
+
for i, s in enumerate(body):
|
|
1481
|
+
if s is old:
|
|
1482
|
+
body[i:i + 1] = new_stmts
|
|
1483
|
+
return True
|
|
1484
|
+
for field in ("body", "orelse", "finalbody"):
|
|
1485
|
+
sub = getattr(s, field, None)
|
|
1486
|
+
if isinstance(sub, list) and rec(sub):
|
|
1487
|
+
return True
|
|
1488
|
+
for h in getattr(s, "handlers", []):
|
|
1489
|
+
if isinstance(h.body, list) and rec(h.body):
|
|
1490
|
+
return True
|
|
1491
|
+
return False
|
|
1492
|
+
if not rec(fn.body):
|
|
1493
|
+
raise _FkFail("stmt not found")
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
class _Transformer:
|
|
1497
|
+
"""Orchestrates type inference + cdef class + cfunc transforms."""
|
|
1498
|
+
|
|
1499
|
+
def __init__(self, tree: ast.Module):
|
|
1500
|
+
self.tree = tree
|
|
1501
|
+
self.class_plans: dict[int, ClassPlan] = {}
|
|
1502
|
+
self.cdef_class_names: set[str] = set()
|
|
1503
|
+
self.applied_strategies: list[str] = []
|
|
1504
|
+
# Map: function name → solved local types (for cross-function arg inference)
|
|
1505
|
+
self.func_types: dict[str, dict[str, str]] = {}
|
|
1506
|
+
# Map: function name → set of arg names that are int/float
|
|
1507
|
+
self.func_arg_types: dict[str, dict[str, str]] = {}
|
|
1508
|
+
# Map: function name → var names the loopify pass wants typed double
|
|
1509
|
+
# (index-loop value vars whose element expr carries float evidence)
|
|
1510
|
+
self.loopify_float_vars: dict[str, set[str]] = {}
|
|
1511
|
+
|
|
1512
|
+
def run(self) -> bool:
|
|
1513
|
+
# Pass 0: conservative expression-function inlining (V8-style call
|
|
1514
|
+
# boundary elimination, but proven statically instead of speculated).
|
|
1515
|
+
# Runs BEFORE loopify so sum(genexp) elements become cheap arithmetic
|
|
1516
|
+
# (inlined callees) instead of calls that loopify must reject.
|
|
1517
|
+
self._inline_functions()
|
|
1518
|
+
|
|
1519
|
+
# Pass 1: sum(genexp) → typed accumulator loop (kills the boxed
|
|
1520
|
+
# generator protocol; before type solving so the solver sees the loop)
|
|
1521
|
+
if self._loopify_sums():
|
|
1522
|
+
self.applied_strategies.append("sum-loopify")
|
|
1523
|
+
|
|
1524
|
+
# Analyse classes (collect cdef candidates before type solving)
|
|
1525
|
+
self._analyse_classes()
|
|
1526
|
+
|
|
1527
|
+
# Pass 2: pre-solve all module-level functions to get local var types
|
|
1528
|
+
self._pre_solve_functions()
|
|
1529
|
+
|
|
1530
|
+
# Pass 3: infer attribute types using solved function types
|
|
1531
|
+
if self.cdef_class_names:
|
|
1532
|
+
self._infer_attr_types_from_calls()
|
|
1533
|
+
self._check_arithmetic_safety()
|
|
1534
|
+
self._detect_needs_dict()
|
|
1535
|
+
|
|
1536
|
+
# Pass 4: infer loop variable object types (needed before cfunc gen)
|
|
1537
|
+
self._infer_loop_var_types()
|
|
1538
|
+
|
|
1539
|
+
# Pass 5: transform classes (cdef + cfunc only for called methods)
|
|
1540
|
+
if self.cdef_class_names:
|
|
1541
|
+
self._transform_classes()
|
|
1542
|
+
|
|
1543
|
+
# Pass 6: transform functions (directives + locals + cfunc call rewrite)
|
|
1544
|
+
self._transform_functions()
|
|
1545
|
+
|
|
1546
|
+
# Pass 7: SoA container unboxing (nbody shape: dict-of-tuples ->
|
|
1547
|
+
# list(values) -> pairs). Runs last so all other rewrites have
|
|
1548
|
+
# settled; failures are silent no-ops (pattern simply not found).
|
|
1549
|
+
try:
|
|
1550
|
+
_pb_unbox_tree(self.tree, self.applied_strategies)
|
|
1551
|
+
except Exception:
|
|
1552
|
+
pass
|
|
1553
|
+
|
|
1554
|
+
# Pass 8: integer-list unboxing (fannkuch shape). Also last.
|
|
1555
|
+
try:
|
|
1556
|
+
_fk_unbox_tree(self.tree, self.applied_strategies)
|
|
1557
|
+
except Exception:
|
|
1558
|
+
pass
|
|
1559
|
+
|
|
1560
|
+
# Pass 9 + 10: recursive-function cdef lowering and pure-function
|
|
1561
|
+
# sum-LICM folding (binary_trees shape). Uses func_arg_types
|
|
1562
|
+
# solved in Pass 2; failures are silent no-ops.
|
|
1563
|
+
try:
|
|
1564
|
+
from .recpass import apply_recursive_passes
|
|
1565
|
+
# int-return proof: reuse solver data — a pure recursive
|
|
1566
|
+
# candidate returns int when every return expr is int-typed.
|
|
1567
|
+
int_returns = set()
|
|
1568
|
+
for fname, ftypes in self.func_types.items():
|
|
1569
|
+
if any(v == LONGLONG for v in ftypes.values()):
|
|
1570
|
+
pass # per-var info only; int-return computed below
|
|
1571
|
+
for node in self.tree.body:
|
|
1572
|
+
if isinstance(node, ast.FunctionDef):
|
|
1573
|
+
sc = _Scanner(node)
|
|
1574
|
+
sol = _Solver(sc)
|
|
1575
|
+
sol.solve()
|
|
1576
|
+
rets = [s.value for s in node.body
|
|
1577
|
+
if isinstance(s, ast.Return) and s.value]
|
|
1578
|
+
if (not sc.dynamic and not sc.generator
|
|
1579
|
+
and rets
|
|
1580
|
+
and all(sol._is_int_expr(r) or
|
|
1581
|
+
sol.int_bits(r) is not None
|
|
1582
|
+
for r in rets)):
|
|
1583
|
+
int_returns.add(node.name)
|
|
1584
|
+
tags = apply_recursive_passes(self.tree,
|
|
1585
|
+
self.func_arg_types,
|
|
1586
|
+
int_returns)
|
|
1587
|
+
self.applied_strategies.extend(tags)
|
|
1588
|
+
except Exception:
|
|
1589
|
+
pass
|
|
1590
|
+
|
|
1591
|
+
# Add `import cython` if we did anything
|
|
1592
|
+
self._ensure_cython_import()
|
|
1593
|
+
return True
|
|
1594
|
+
|
|
1595
|
+
# -----------------------------------------------------------------
|
|
1596
|
+
# Pass -1: sum(genexp) loopification
|
|
1597
|
+
# -----------------------------------------------------------------
|
|
1598
|
+
|
|
1599
|
+
def _loopify_sums(self) -> bool:
|
|
1600
|
+
"""Statement-level rewrite of `x = sum(<genexp>)` and
|
|
1601
|
+
`return [sum(<genexp>) …]` list-comp bodies.
|
|
1602
|
+
|
|
1603
|
+
Only two shapes, both taken straight from real benchmark code:
|
|
1604
|
+
|
|
1605
|
+
A. assign form:
|
|
1606
|
+
<t> = sum(E for T in ITER)
|
|
1607
|
+
→ <t> = 0.0; for T in ITER: <t> += E
|
|
1608
|
+
|
|
1609
|
+
B. list-comp return (spectral_norm):
|
|
1610
|
+
return [sum(E for T in ITER) for OUTER in OITER]
|
|
1611
|
+
→ out = []
|
|
1612
|
+
for OUTER in OITER:
|
|
1613
|
+
acc = 0.0
|
|
1614
|
+
for T in ITER: acc += E
|
|
1615
|
+
out.append(acc)
|
|
1616
|
+
return out
|
|
1617
|
+
|
|
1618
|
+
Constraints on E: cheap arithmetic only (Name/Constant/BinOp/
|
|
1619
|
+
UnaryOp — calls are left to the inliner; if a call survived inlining
|
|
1620
|
+
it is not rewritten here). Tuple targets require `enumerate(seq)`;
|
|
1621
|
+
single-Name targets accept any cheap iterable expression. On any
|
|
1622
|
+
doubt the node is returned unchanged. Accumulator typing is left to
|
|
1623
|
+
the existing float solver, which sees `= 0.0` / `+= E` and infers
|
|
1624
|
+
double — the whole point of the rewrite.
|
|
1625
|
+
"""
|
|
1626
|
+
changed = False
|
|
1627
|
+
for node in self.tree.body:
|
|
1628
|
+
if isinstance(node, ast.FunctionDef):
|
|
1629
|
+
if self._loopify_in_func(node):
|
|
1630
|
+
changed = True
|
|
1631
|
+
return changed
|
|
1632
|
+
|
|
1633
|
+
def _build_sum_loop(self, func: ast.FunctionDef,
|
|
1634
|
+
sum_call: ast.Call,
|
|
1635
|
+
outer_target: ast.Name | None,
|
|
1636
|
+
outer_iter: ast.expr | None) -> list | None:
|
|
1637
|
+
"""Build replacement statements for one sum(genexp); None = bail."""
|
|
1638
|
+
gen = sum_call.args[0]
|
|
1639
|
+
comp = gen.generators[0]
|
|
1640
|
+
elem = gen.elt
|
|
1641
|
+
tgt = comp.target
|
|
1642
|
+
if not isinstance(tgt, (ast.Name, ast.Tuple)):
|
|
1643
|
+
return None
|
|
1644
|
+
if isinstance(tgt, ast.Tuple) and not (
|
|
1645
|
+
len(tgt.elts) >= 1
|
|
1646
|
+
and all(isinstance(e, ast.Name) for e in tgt.elts)):
|
|
1647
|
+
return None
|
|
1648
|
+
iter_expr = comp.iter
|
|
1649
|
+
if not _cheap_expr(elem):
|
|
1650
|
+
return None
|
|
1651
|
+
|
|
1652
|
+
# enumerate-tuple fast path: when the tuple is (idx, val) over
|
|
1653
|
+
# enumerate(Name) AND the same Name is already used in a len(Name)
|
|
1654
|
+
# call somewhere in this function (the original code assumed it is a
|
|
1655
|
+
# sized sequence), rewrite as an index loop `for idx in range(len(Name)):`
|
|
1656
|
+
# + `val = Name[idx]` — avoids one tuple allocation per iteration,
|
|
1657
|
+
# which dominates otherwise (10x on spectral_norm).
|
|
1658
|
+
tuple_idx_loop = None
|
|
1659
|
+
if isinstance(tgt, ast.Tuple) and len(tgt.elts) == 2 \
|
|
1660
|
+
and isinstance(iter_expr, ast.Call) \
|
|
1661
|
+
and isinstance(iter_expr.func, ast.Name) \
|
|
1662
|
+
and iter_expr.func.id == "enumerate" \
|
|
1663
|
+
and len(iter_expr.args) == 1 and not iter_expr.keywords \
|
|
1664
|
+
and isinstance(iter_expr.args[0], ast.Name):
|
|
1665
|
+
seq_name = iter_expr.args[0].id
|
|
1666
|
+
uses_len = any(
|
|
1667
|
+
isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
|
|
1668
|
+
and n.func.id == "len" and len(n.args) == 1
|
|
1669
|
+
and isinstance(n.args[0], ast.Name)
|
|
1670
|
+
and n.args[0].id == seq_name
|
|
1671
|
+
for n in ast.walk(func))
|
|
1672
|
+
if uses_len:
|
|
1673
|
+
tuple_idx_loop = (
|
|
1674
|
+
tgt.elts[0], tgt.elts[1], seq_name)
|
|
1675
|
+
# float evidence gate (same precedent as the enumerate
|
|
1676
|
+
# inference): typing the value var double is only sound when
|
|
1677
|
+
# the element expr is float arithmetic; otherwise leave it an
|
|
1678
|
+
# object and the loop still runs, just unoptimized
|
|
1679
|
+
has_float_ev = any(
|
|
1680
|
+
isinstance(n, ast.Constant) and isinstance(n.value, float)
|
|
1681
|
+
for n in ast.walk(elem)) or any(
|
|
1682
|
+
isinstance(n, ast.BinOp) and isinstance(n.op, ast.Div)
|
|
1683
|
+
for n in ast.walk(elem))
|
|
1684
|
+
if has_float_ev:
|
|
1685
|
+
self.loopify_float_vars.setdefault(
|
|
1686
|
+
func.name, set()).add(tgt.elts[1].id)
|
|
1687
|
+
|
|
1688
|
+
acc_store = lambda: ast.Name(id="__pb_acc", ctx=ast.Store())
|
|
1689
|
+
acc_load = lambda: ast.Name(id="__pb_acc", ctx=ast.Load())
|
|
1690
|
+
if tuple_idx_loop is not None:
|
|
1691
|
+
idx_name, val_name, seq = tuple_idx_loop
|
|
1692
|
+
inner_for = ast.For(
|
|
1693
|
+
target=ast.Name(id=idx_name.id, ctx=ast.Store()),
|
|
1694
|
+
iter=ast.Call(
|
|
1695
|
+
func=ast.Name(id="range", ctx=ast.Load()),
|
|
1696
|
+
args=[ast.Call(
|
|
1697
|
+
func=ast.Name(id="len", ctx=ast.Load()),
|
|
1698
|
+
args=[ast.Name(id=seq, ctx=ast.Load())],
|
|
1699
|
+
keywords=[])],
|
|
1700
|
+
keywords=[]),
|
|
1701
|
+
body=[
|
|
1702
|
+
ast.Assign(
|
|
1703
|
+
targets=[ast.Name(id=val_name.id, ctx=ast.Store())],
|
|
1704
|
+
value=ast.Subscript(
|
|
1705
|
+
value=ast.Name(id=seq, ctx=ast.Load()),
|
|
1706
|
+
slice=ast.Name(id=idx_name.id, ctx=ast.Load()),
|
|
1707
|
+
ctx=ast.Load())),
|
|
1708
|
+
ast.AugAssign(target=acc_store(), op=ast.Add(),
|
|
1709
|
+
value=copy.deepcopy(elem)),
|
|
1710
|
+
],
|
|
1711
|
+
orelse=[])
|
|
1712
|
+
else:
|
|
1713
|
+
inner_for = ast.For(
|
|
1714
|
+
target=copy.deepcopy(tgt),
|
|
1715
|
+
iter=iter_expr,
|
|
1716
|
+
body=[ast.AugAssign(target=acc_store(), op=ast.Add(),
|
|
1717
|
+
value=copy.deepcopy(elem))],
|
|
1718
|
+
orelse=[])
|
|
1719
|
+
if outer_target is None:
|
|
1720
|
+
# shape A: assign — accumulator is the user's own var (no temp
|
|
1721
|
+
# pollution for the solver); caller retargets it
|
|
1722
|
+
return [copy.deepcopy(inner_for)]
|
|
1723
|
+
if not isinstance(outer_target, ast.Name) or outer_iter is None:
|
|
1724
|
+
return None
|
|
1725
|
+
# shape B: build out/outer-loop/return around the inner loop
|
|
1726
|
+
return [
|
|
1727
|
+
ast.Assign(targets=[ast.Name(id="__pb_out", ctx=ast.Store())],
|
|
1728
|
+
value=ast.List(elts=[], ctx=ast.Load())),
|
|
1729
|
+
ast.For(
|
|
1730
|
+
target=copy.deepcopy(outer_target),
|
|
1731
|
+
iter=outer_iter,
|
|
1732
|
+
body=[
|
|
1733
|
+
ast.Assign(targets=[acc_store()],
|
|
1734
|
+
value=ast.Constant(value=0.0)),
|
|
1735
|
+
copy.deepcopy(inner_for),
|
|
1736
|
+
ast.Expr(value=ast.Call(
|
|
1737
|
+
func=ast.Attribute(value=ast.Name(
|
|
1738
|
+
id="__pb_out", ctx=ast.Load()),
|
|
1739
|
+
attr="append", ctx=ast.Load()),
|
|
1740
|
+
args=[acc_load()], keywords=[])),
|
|
1741
|
+
],
|
|
1742
|
+
orelse=[]),
|
|
1743
|
+
ast.Return(value=ast.Name(id="__pb_out", ctx=ast.Load())),
|
|
1744
|
+
]
|
|
1745
|
+
|
|
1746
|
+
def _loopify_in_func(self, func: ast.FunctionDef) -> bool:
|
|
1747
|
+
"""Rewrite sum(genexp) call sites inside `func` (its own statements,
|
|
1748
|
+
not nested defs). Returns True if anything changed."""
|
|
1749
|
+
|
|
1750
|
+
transformer = self
|
|
1751
|
+
|
|
1752
|
+
class _LoopRewriter(ast.NodeTransformer):
|
|
1753
|
+
def __init__(self):
|
|
1754
|
+
self.hit = False
|
|
1755
|
+
|
|
1756
|
+
def visit_Assign(self, node: ast.Assign) -> ast.AST:
|
|
1757
|
+
self.generic_visit(node)
|
|
1758
|
+
if not (len(node.targets) == 1
|
|
1759
|
+
and isinstance(node.targets[0], ast.Name)
|
|
1760
|
+
and _is_sum_genexp(node.value)):
|
|
1761
|
+
return node
|
|
1762
|
+
orig = node.targets[0].id
|
|
1763
|
+
# the accumulator must not collide with any name the loop
|
|
1764
|
+
# itself binds (e.g. `x = sum(x for x in u)` would produce
|
|
1765
|
+
# `for x in u: x += …` and silently drop iterations)
|
|
1766
|
+
bound = {n.id for n in ast.walk(node.value) if isinstance(n, ast.Name)}
|
|
1767
|
+
if orig in bound and any(
|
|
1768
|
+
isinstance(t, ast.Name) and t.id == orig
|
|
1769
|
+
for t in ([node.value.args[0].generators[0].target]
|
|
1770
|
+
if isinstance(node.value.args[0].generators[0].target, ast.Name)
|
|
1771
|
+
else list(getattr(node.value.args[0].generators[0].target, 'elts', [])))):
|
|
1772
|
+
return node
|
|
1773
|
+
stmts = transformer._build_sum_loop(func, node.value, None, None)
|
|
1774
|
+
if stmts is None:
|
|
1775
|
+
return node
|
|
1776
|
+
# retarget the accumulator onto the user's variable so the
|
|
1777
|
+
# solver sees <orig> = 0.0-accumulation and types it double
|
|
1778
|
+
class _Ren(ast.NodeTransformer):
|
|
1779
|
+
def visit_Name(self, n: ast.Name) -> ast.AST:
|
|
1780
|
+
if n.id == "__pb_acc":
|
|
1781
|
+
return ast.copy_location(
|
|
1782
|
+
ast.Name(id=orig, ctx=n.ctx), n)
|
|
1783
|
+
return n
|
|
1784
|
+
stmts = [_Ren().visit(s) for s in stmts]
|
|
1785
|
+
self.hit = True
|
|
1786
|
+
return _StmtList([ast.Assign(
|
|
1787
|
+
targets=[ast.Name(id=orig, ctx=ast.Store())],
|
|
1788
|
+
value=ast.Constant(value=0.0))] + stmts)
|
|
1789
|
+
|
|
1790
|
+
def visit_Return(self, node: ast.Return) -> ast.AST:
|
|
1791
|
+
self.generic_visit(node)
|
|
1792
|
+
if not (node.value is not None
|
|
1793
|
+
and isinstance(node.value, ast.ListComp)
|
|
1794
|
+
and len(node.value.generators) == 1
|
|
1795
|
+
and not node.value.generators[0].ifs
|
|
1796
|
+
and isinstance(node.value.generators[0].target, ast.Name)
|
|
1797
|
+
and _is_sum_genexp(node.value.elt)):
|
|
1798
|
+
return node
|
|
1799
|
+
g = node.value.generators[0]
|
|
1800
|
+
stmts = transformer._build_sum_loop(
|
|
1801
|
+
func, node.value.elt, g.target, g.iter)
|
|
1802
|
+
if stmts is None:
|
|
1803
|
+
return node
|
|
1804
|
+
self.hit = True
|
|
1805
|
+
return _StmtList(stmts)
|
|
1806
|
+
|
|
1807
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
1808
|
+
if node is not func:
|
|
1809
|
+
return node # never descend into nested defs
|
|
1810
|
+
self.generic_visit(node)
|
|
1811
|
+
return node
|
|
1812
|
+
|
|
1813
|
+
rewriter = _LoopRewriter()
|
|
1814
|
+
new_body = []
|
|
1815
|
+
for stmt in func.body:
|
|
1816
|
+
out = rewriter.visit(stmt)
|
|
1817
|
+
if isinstance(out, _StmtList):
|
|
1818
|
+
new_body.extend(out.stmts)
|
|
1819
|
+
else:
|
|
1820
|
+
new_body.append(out)
|
|
1821
|
+
# flatten any _StmtList that landed inside nested statement lists
|
|
1822
|
+
# (If/For/While bodies) — a _StmtList left in place breaks the AST
|
|
1823
|
+
func.body = self._flatten_stmt_lists(new_body)
|
|
1824
|
+
return rewriter.hit
|
|
1825
|
+
|
|
1826
|
+
@staticmethod
|
|
1827
|
+
def _flatten_stmt_lists(stmts: list) -> list:
|
|
1828
|
+
out: list = []
|
|
1829
|
+
for s in stmts:
|
|
1830
|
+
if isinstance(s, _StmtList):
|
|
1831
|
+
out.extend(_Transformer._flatten_stmt_lists(s.stmts))
|
|
1832
|
+
continue
|
|
1833
|
+
for fld in ("body", "orelse", "finalbody"):
|
|
1834
|
+
val = getattr(s, fld, None)
|
|
1835
|
+
if isinstance(val, list):
|
|
1836
|
+
setattr(s, fld, _Transformer._flatten_stmt_lists(val))
|
|
1837
|
+
out.append(s)
|
|
1838
|
+
return out
|
|
1839
|
+
|
|
1840
|
+
# -----------------------------------------------------------------
|
|
1841
|
+
# Pass 0: expression-function inlining
|
|
1842
|
+
# -----------------------------------------------------------------
|
|
1843
|
+
|
|
1844
|
+
def _inline_functions(self) -> None:
|
|
1845
|
+
"""Inline small pure expression functions at their call sites.
|
|
1846
|
+
|
|
1847
|
+
Conservative AOT analogue of a JIT's speculative inlining: instead of
|
|
1848
|
+
type feedback we require the callee body to be straight-line cheap
|
|
1849
|
+
arithmetic over its params (no calls, no branches, no globals), so
|
|
1850
|
+
splicing it into the caller can never duplicate a side effect. The
|
|
1851
|
+
callee def itself is kept for external callers.
|
|
1852
|
+
"""
|
|
1853
|
+
funcs = [n for n in self.tree.body if isinstance(n, ast.FunctionDef)]
|
|
1854
|
+
|
|
1855
|
+
# names reassigned at module level disqualify their function (the
|
|
1856
|
+
# call site may legally see the rebound object)
|
|
1857
|
+
rebound: set[str] = set()
|
|
1858
|
+
for node in self.tree.body:
|
|
1859
|
+
if isinstance(node, ast.Assign):
|
|
1860
|
+
for tgt in node.targets:
|
|
1861
|
+
if isinstance(tgt, ast.Name):
|
|
1862
|
+
rebound.add(tgt.id)
|
|
1863
|
+
|
|
1864
|
+
candidates: dict[str, dict[str, ast.AST]] = {}
|
|
1865
|
+
for func in funcs:
|
|
1866
|
+
if func.name in rebound or func.name.startswith("_"):
|
|
1867
|
+
continue
|
|
1868
|
+
subst = _inlineable_shape(func)
|
|
1869
|
+
if subst is not None:
|
|
1870
|
+
candidates[func.name] = subst
|
|
1871
|
+
|
|
1872
|
+
if not candidates:
|
|
1873
|
+
return
|
|
1874
|
+
|
|
1875
|
+
candidate_defs = {f.name: f for f in funcs if f.name in candidates}
|
|
1876
|
+
inlined_any = False
|
|
1877
|
+
|
|
1878
|
+
class _Inliner(ast.NodeTransformer):
|
|
1879
|
+
def __init__(self, owner: ast.AST):
|
|
1880
|
+
self.owner = owner
|
|
1881
|
+
self.depth = 0
|
|
1882
|
+
|
|
1883
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
1884
|
+
# rewrite only the owner's own body; never descend into
|
|
1885
|
+
# nested defs (their scoping/semantics stay untouched)
|
|
1886
|
+
if node is not self.owner:
|
|
1887
|
+
return node
|
|
1888
|
+
self.generic_visit(node)
|
|
1889
|
+
return node
|
|
1890
|
+
|
|
1891
|
+
def visit_Call(self, node: ast.Call) -> ast.AST:
|
|
1892
|
+
self.generic_visit(node)
|
|
1893
|
+
if not (isinstance(node.func, ast.Name)
|
|
1894
|
+
and node.func.id in candidates
|
|
1895
|
+
and not node.keywords):
|
|
1896
|
+
return node
|
|
1897
|
+
subst = candidates[node.func.id]
|
|
1898
|
+
params = list(func_params[node.func.id])
|
|
1899
|
+
if len(node.args) != len(params):
|
|
1900
|
+
return node
|
|
1901
|
+
arg_map: dict[str, ast.AST] = {}
|
|
1902
|
+
for p, a in zip(params, node.args):
|
|
1903
|
+
if not _cheap_expr(a):
|
|
1904
|
+
return node # duplicated evaluation could be costly
|
|
1905
|
+
arg_map[p] = copy.deepcopy(a)
|
|
1906
|
+
# two-stage resolution, both single-pass:
|
|
1907
|
+
# 1. resolve each temp body against the caller's args (temp
|
|
1908
|
+
# bodies contain only param names by construction)
|
|
1909
|
+
# 2. one pass over the return expr with temps + param
|
|
1910
|
+
# bindings — each name replaced exactly once
|
|
1911
|
+
temp_map = {
|
|
1912
|
+
name: _subst_names(expr, arg_map)
|
|
1913
|
+
for name, expr in subst.items() if name not in arg_map}
|
|
1914
|
+
final_map = dict(temp_map)
|
|
1915
|
+
final_map.update(arg_map)
|
|
1916
|
+
ret_expr = candidate_defs[node.func.id].body[-1].value
|
|
1917
|
+
return ast.copy_location(
|
|
1918
|
+
_subst_names(ret_expr, final_map), node)
|
|
1919
|
+
|
|
1920
|
+
# param order per candidate, for arity checks
|
|
1921
|
+
func_params = {
|
|
1922
|
+
f.name: [a.arg for a in f.args.args] for f in funcs
|
|
1923
|
+
if f.name in candidates}
|
|
1924
|
+
|
|
1925
|
+
for func in funcs:
|
|
1926
|
+
if func.name in candidates:
|
|
1927
|
+
continue # keep candidate bodies pristine
|
|
1928
|
+
inliner = _Inliner(func)
|
|
1929
|
+
before = ast.dump(func)
|
|
1930
|
+
new_func = inliner.visit(func)
|
|
1931
|
+
if ast.dump(new_func) != before:
|
|
1932
|
+
inlined_any = True
|
|
1933
|
+
|
|
1934
|
+
if inlined_any:
|
|
1935
|
+
self.applied_strategies.append("inline")
|
|
1936
|
+
|
|
1937
|
+
def _pre_solve_functions(self) -> None:
|
|
1938
|
+
"""Scan and solve all module-level functions to learn variable types.
|
|
1939
|
+
|
|
1940
|
+
This lets us infer attribute types from call sites like
|
|
1941
|
+
Account(i, 1000.0) where i is a solved longlong loop variable.
|
|
1942
|
+
"""
|
|
1943
|
+
for node in self.tree.body:
|
|
1944
|
+
if not isinstance(node, ast.FunctionDef):
|
|
1945
|
+
continue
|
|
1946
|
+
sc = _Scanner(node)
|
|
1947
|
+
if sc.dynamic or sc.generator:
|
|
1948
|
+
continue
|
|
1949
|
+
solver = _Solver(sc)
|
|
1950
|
+
solver.solve()
|
|
1951
|
+
self.func_types[node.name] = dict(solver.typed_locals)
|
|
1952
|
+
# Infer arg types from solved types + usage patterns
|
|
1953
|
+
arg_types: dict[str, str] = {}
|
|
1954
|
+
arg_names = {a.arg for a in node.args.args}
|
|
1955
|
+
for name, ctype in solver.typed_locals.items():
|
|
1956
|
+
if name in arg_names:
|
|
1957
|
+
arg_types[name] = ctype
|
|
1958
|
+
# Check if args are used in float context (Div, float literal)
|
|
1959
|
+
for name in arg_names:
|
|
1960
|
+
if name in arg_types:
|
|
1961
|
+
continue
|
|
1962
|
+
for sub in ast.walk(node):
|
|
1963
|
+
if isinstance(sub, ast.BinOp) and isinstance(sub.op, ast.Div):
|
|
1964
|
+
for side in (sub.left, sub.right):
|
|
1965
|
+
if isinstance(side, ast.Name) and side.id == name:
|
|
1966
|
+
arg_types[name] = DOUBLE
|
|
1967
|
+
break
|
|
1968
|
+
if isinstance(sub, ast.BinOp):
|
|
1969
|
+
for side in (sub.left, sub.right):
|
|
1970
|
+
if (isinstance(side, ast.Constant)
|
|
1971
|
+
and isinstance(side.value, float)):
|
|
1972
|
+
other = sub.right if side is sub.left else sub.left
|
|
1973
|
+
if isinstance(other, ast.Name) and other.id == name:
|
|
1974
|
+
arg_types[name] = DOUBLE
|
|
1975
|
+
break
|
|
1976
|
+
self.func_arg_types[node.name] = arg_types
|
|
1977
|
+
|
|
1978
|
+
# -----------------------------------------------------------------
|
|
1979
|
+
# Class analysis
|
|
1980
|
+
# -----------------------------------------------------------------
|
|
1981
|
+
|
|
1982
|
+
def _analyse_classes(self) -> None:
|
|
1983
|
+
for node in self.tree.body:
|
|
1984
|
+
if not isinstance(node, ast.ClassDef):
|
|
1985
|
+
continue
|
|
1986
|
+
plan = self._analyse_one_class(node)
|
|
1987
|
+
self.class_plans[id(node)] = plan
|
|
1988
|
+
if not plan.skip:
|
|
1989
|
+
self.cdef_class_names.add(plan.name)
|
|
1990
|
+
# Note: we do NOT skip parents of non-cdef subclasses here.
|
|
1991
|
+
# The _detect_needs_dict step 2 sets needs_dict=True on parents
|
|
1992
|
+
# that have non-cdef subclasses, and _has_noncdef_subclass
|
|
1993
|
+
# prevents cfunc generation on those classes.
|
|
1994
|
+
|
|
1995
|
+
def _analyse_one_class(self, node: ast.ClassDef) -> ClassPlan:
|
|
1996
|
+
plan = ClassPlan(node=node, name=node.name)
|
|
1997
|
+
|
|
1998
|
+
has_slots = any(
|
|
1999
|
+
isinstance(stmt, ast.Assign)
|
|
2000
|
+
and any(isinstance(t, ast.Name) and t.id == "__slots__"
|
|
2001
|
+
for t in stmt.targets)
|
|
2002
|
+
for stmt in node.body)
|
|
2003
|
+
init = next((s for s in node.body
|
|
2004
|
+
if isinstance(s, ast.FunctionDef) and s.name == "__init__"),
|
|
2005
|
+
None)
|
|
2006
|
+
|
|
2007
|
+
if not has_slots and not init:
|
|
2008
|
+
plan.skip = True
|
|
2009
|
+
plan.reason = "no __slots__/__init__"
|
|
2010
|
+
return plan
|
|
2011
|
+
|
|
2012
|
+
# Must inherit only from object (or no bases)
|
|
2013
|
+
for base in node.bases:
|
|
2014
|
+
if not (isinstance(base, ast.Name) and base.id == "object"):
|
|
2015
|
+
plan.skip = True
|
|
2016
|
+
plan.reason = "non-object base"
|
|
2017
|
+
return plan
|
|
2018
|
+
|
|
2019
|
+
# Collect attributes from __init__
|
|
2020
|
+
if init:
|
|
2021
|
+
for stmt in ast.walk(init):
|
|
2022
|
+
if (isinstance(stmt, ast.Assign)
|
|
2023
|
+
and len(stmt.targets) == 1
|
|
2024
|
+
and isinstance(stmt.targets[0], ast.Attribute)
|
|
2025
|
+
and isinstance(stmt.targets[0].value, ast.Name)
|
|
2026
|
+
and stmt.targets[0].value.id == "self"):
|
|
2027
|
+
attr_name = stmt.targets[0].attr
|
|
2028
|
+
ctype = _classify_literal(stmt.value)
|
|
2029
|
+
plan.all_attrs[attr_name] = ctype
|
|
2030
|
+
|
|
2031
|
+
if not plan.all_attrs and not has_slots:
|
|
2032
|
+
plan.skip = True
|
|
2033
|
+
plan.reason = "no self attrs"
|
|
2034
|
+
return plan
|
|
2035
|
+
|
|
2036
|
+
plan.skip = False
|
|
2037
|
+
return plan
|
|
2038
|
+
|
|
2039
|
+
# -----------------------------------------------------------------
|
|
2040
|
+
# Attribute type inference from call sites
|
|
2041
|
+
# -----------------------------------------------------------------
|
|
2042
|
+
|
|
2043
|
+
def _infer_attr_types_from_calls(self) -> None:
|
|
2044
|
+
class_params: dict[str, list[str]] = {}
|
|
2045
|
+
for plan in self.class_plans.values():
|
|
2046
|
+
if plan.skip:
|
|
2047
|
+
continue
|
|
2048
|
+
init = next((s for s in plan.node.body
|
|
2049
|
+
if isinstance(s, ast.FunctionDef) and s.name == "__init__"),
|
|
2050
|
+
None)
|
|
2051
|
+
if init:
|
|
2052
|
+
class_params[plan.name] = [
|
|
2053
|
+
a.arg for a in init.args.args if a.arg != "self"]
|
|
2054
|
+
|
|
2055
|
+
# Walk the tree. When we enter a function, remember its solved types
|
|
2056
|
+
# so we can use solved variable types to classify call arguments.
|
|
2057
|
+
for top_node in self.tree.body:
|
|
2058
|
+
if isinstance(top_node, ast.FunctionDef):
|
|
2059
|
+
solved = self.func_types.get(top_node.name, {})
|
|
2060
|
+
else:
|
|
2061
|
+
solved = {}
|
|
2062
|
+
|
|
2063
|
+
for node in ast.walk(top_node):
|
|
2064
|
+
if not isinstance(node, ast.Call):
|
|
2065
|
+
continue
|
|
2066
|
+
func = node.func
|
|
2067
|
+
if not (isinstance(func, ast.Name) and func.id in class_params):
|
|
2068
|
+
continue
|
|
2069
|
+
cls_name = func.id
|
|
2070
|
+
params = class_params[cls_name]
|
|
2071
|
+
plan = next((p for p in self.class_plans.values()
|
|
2072
|
+
if p.name == cls_name), None)
|
|
2073
|
+
if not plan:
|
|
2074
|
+
continue
|
|
2075
|
+
for i, arg in enumerate(node.args):
|
|
2076
|
+
if i >= len(params):
|
|
2077
|
+
break
|
|
2078
|
+
param_name = params[i]
|
|
2079
|
+
if param_name not in plan.all_attrs:
|
|
2080
|
+
continue
|
|
2081
|
+
ctype = self._classify_call_arg(arg, solved)
|
|
2082
|
+
if ctype and plan.all_attrs.get(param_name) == OBJECT:
|
|
2083
|
+
plan.all_attrs[param_name] = ctype
|
|
2084
|
+
|
|
2085
|
+
def _classify_call_arg(self, node: ast.AST,
|
|
2086
|
+
solved: dict[str, str]) -> str | None:
|
|
2087
|
+
"""Classify a call argument using solved local variable types."""
|
|
2088
|
+
# Direct literal classification
|
|
2089
|
+
ctype = _classify_expr(node)
|
|
2090
|
+
if ctype:
|
|
2091
|
+
return ctype
|
|
2092
|
+
# Solved variable names
|
|
2093
|
+
if isinstance(node, ast.Name) and node.id in solved:
|
|
2094
|
+
return solved[node.id]
|
|
2095
|
+
# BinOp: if either side is solved-float or float literal → double
|
|
2096
|
+
# If both sides are solved-int or int literal → longlong
|
|
2097
|
+
if isinstance(node, ast.BinOp):
|
|
2098
|
+
lt = self._classify_call_arg(node.left, solved)
|
|
2099
|
+
rt = self._classify_call_arg(node.right, solved)
|
|
2100
|
+
if lt and rt:
|
|
2101
|
+
if DOUBLE in (lt, rt):
|
|
2102
|
+
return DOUBLE
|
|
2103
|
+
return LONGLONG
|
|
2104
|
+
# UnaryOp
|
|
2105
|
+
if isinstance(node, ast.UnaryOp):
|
|
2106
|
+
return self._classify_call_arg(node.operand, solved)
|
|
2107
|
+
# Call: float() → double, int()/len()/abs() → longlong
|
|
2108
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
2109
|
+
if node.func.id == "float":
|
|
2110
|
+
return DOUBLE
|
|
2111
|
+
if node.func.id in ("int", "len", "abs"):
|
|
2112
|
+
return LONGLONG
|
|
2113
|
+
return None
|
|
2114
|
+
|
|
2115
|
+
# -----------------------------------------------------------------
|
|
2116
|
+
# Arithmetic safety: downgrade longlong attrs in Mult/Pow/LShift
|
|
2117
|
+
# -----------------------------------------------------------------
|
|
2118
|
+
|
|
2119
|
+
def _check_arithmetic_safety(self) -> None:
|
|
2120
|
+
for plan in self.class_plans.values():
|
|
2121
|
+
if plan.skip:
|
|
2122
|
+
continue
|
|
2123
|
+
longlong_attrs = {k for k, v in plan.all_attrs.items()
|
|
2124
|
+
if v == LONGLONG}
|
|
2125
|
+
if not longlong_attrs:
|
|
2126
|
+
continue
|
|
2127
|
+
for method in plan.node.body:
|
|
2128
|
+
if not isinstance(method, ast.FunctionDef):
|
|
2129
|
+
continue
|
|
2130
|
+
for node in ast.walk(method):
|
|
2131
|
+
if (isinstance(node, ast.BinOp)
|
|
2132
|
+
and isinstance(node.op, (ast.Mult, ast.Pow, ast.LShift))):
|
|
2133
|
+
for child in (node.left, node.right):
|
|
2134
|
+
if (isinstance(child, ast.Attribute)
|
|
2135
|
+
and isinstance(child.value, ast.Name)
|
|
2136
|
+
and child.value.id == "self"
|
|
2137
|
+
and child.attr in longlong_attrs):
|
|
2138
|
+
longlong_attrs.discard(child.attr)
|
|
2139
|
+
for attr_name, ctype in list(plan.all_attrs.items()):
|
|
2140
|
+
if ctype == LONGLONG and attr_name not in longlong_attrs:
|
|
2141
|
+
plan.all_attrs[attr_name] = OBJECT
|
|
2142
|
+
|
|
2143
|
+
# -----------------------------------------------------------------
|
|
2144
|
+
# needs_dict detection
|
|
2145
|
+
# -----------------------------------------------------------------
|
|
2146
|
+
|
|
2147
|
+
def _detect_needs_dict(self) -> None:
|
|
2148
|
+
attr_to_classes: dict[str, set[str]] = {}
|
|
2149
|
+
for plan in self.class_plans.values():
|
|
2150
|
+
if plan.skip:
|
|
2151
|
+
continue
|
|
2152
|
+
for attr_name in plan.all_attrs:
|
|
2153
|
+
attr_to_classes.setdefault(attr_name, set()).add(plan.name)
|
|
2154
|
+
|
|
2155
|
+
# 1. Dunder methods accessing other.attr
|
|
2156
|
+
for plan in self.class_plans.values():
|
|
2157
|
+
if plan.skip:
|
|
2158
|
+
continue
|
|
2159
|
+
for method in plan.node.body:
|
|
2160
|
+
if not isinstance(method, ast.FunctionDef):
|
|
2161
|
+
continue
|
|
2162
|
+
is_dunder = (method.name.startswith("__")
|
|
2163
|
+
and method.name.endswith("__"))
|
|
2164
|
+
if not is_dunder:
|
|
2165
|
+
continue
|
|
2166
|
+
param_names = {a.arg for a in method.args.args
|
|
2167
|
+
if a.arg != "self"}
|
|
2168
|
+
for node in ast.walk(method):
|
|
2169
|
+
if (isinstance(node, ast.Attribute)
|
|
2170
|
+
and node.attr in attr_to_classes
|
|
2171
|
+
and isinstance(node.value, ast.Name)
|
|
2172
|
+
and node.value.id in param_names):
|
|
2173
|
+
plan.needs_dict = True
|
|
2174
|
+
|
|
2175
|
+
# 2. Non-cdef subclasses of cdef classes
|
|
2176
|
+
cdef_names = self.cdef_class_names
|
|
2177
|
+
for plan in self.class_plans.values():
|
|
2178
|
+
if plan.skip:
|
|
2179
|
+
for base in plan.node.bases:
|
|
2180
|
+
base_name = (base.id if isinstance(base, ast.Name)
|
|
2181
|
+
else getattr(base, "attr", None))
|
|
2182
|
+
if base_name in cdef_names:
|
|
2183
|
+
for parent in self.class_plans.values():
|
|
2184
|
+
if parent.name == base_name and not parent.skip:
|
|
2185
|
+
parent.needs_dict = True
|
|
2186
|
+
|
|
2187
|
+
# 3. External attribute access from module-level function bodies.
|
|
2188
|
+
# Class methods access same-type attrs via C-level (AnnAssign is fine).
|
|
2189
|
+
# But module-level functions that do obj.attr need visibility="public".
|
|
2190
|
+
cdef_attr_set = set(attr_to_classes.keys())
|
|
2191
|
+
for top in self.tree.body:
|
|
2192
|
+
if isinstance(top, ast.ClassDef):
|
|
2193
|
+
continue # class methods access same-type attrs via C-level
|
|
2194
|
+
for node in ast.walk(top):
|
|
2195
|
+
if (isinstance(node, ast.Attribute)
|
|
2196
|
+
and node.attr in cdef_attr_set
|
|
2197
|
+
and isinstance(node.value, ast.Name)):
|
|
2198
|
+
for cls_name in attr_to_classes.get(node.attr, set()):
|
|
2199
|
+
for plan in self.class_plans.values():
|
|
2200
|
+
if plan.name == cls_name and not plan.skip:
|
|
2201
|
+
plan.needs_dict = True
|
|
2202
|
+
|
|
2203
|
+
# -----------------------------------------------------------------
|
|
2204
|
+
# Loop variable object type inference (needed before cfunc generation)
|
|
2205
|
+
# -----------------------------------------------------------------
|
|
2206
|
+
|
|
2207
|
+
def _infer_loop_var_types(self) -> None:
|
|
2208
|
+
"""Infer loop variable types and record which methods need cfunc.
|
|
2209
|
+
|
|
2210
|
+
A method only gets a cfunc variant if it's called on a typed loop
|
|
2211
|
+
variable — otherwise the wrapper just adds overhead.
|
|
2212
|
+
"""
|
|
2213
|
+
# Map: class_name → set of method names called on typed loop vars
|
|
2214
|
+
self.cfunc_needed: dict[str, set[str]] = {}
|
|
2215
|
+
|
|
2216
|
+
for node in self.tree.body:
|
|
2217
|
+
if not isinstance(node, ast.FunctionDef):
|
|
2218
|
+
continue
|
|
2219
|
+
loop_var_types: dict[str, str] = {}
|
|
2220
|
+
for inner in ast.walk(node):
|
|
2221
|
+
if isinstance(inner, ast.For) and isinstance(inner.target, ast.Name):
|
|
2222
|
+
var_name = inner.target.id
|
|
2223
|
+
iter_node = inner.iter
|
|
2224
|
+
if (isinstance(iter_node, ast.ListComp)
|
|
2225
|
+
and isinstance(iter_node.elt, ast.Call)
|
|
2226
|
+
and isinstance(iter_node.elt.func, ast.Name)
|
|
2227
|
+
and iter_node.elt.func.id in self.cdef_class_names):
|
|
2228
|
+
loop_var_types[var_name] = iter_node.elt.func.id
|
|
2229
|
+
elif isinstance(iter_node, ast.Name):
|
|
2230
|
+
cls = self._find_list_var_type(node, iter_node.id)
|
|
2231
|
+
if cls:
|
|
2232
|
+
loop_var_types[var_name] = cls
|
|
2233
|
+
|
|
2234
|
+
# Find which methods are called on typed loop vars
|
|
2235
|
+
if loop_var_types:
|
|
2236
|
+
for inner in ast.walk(node):
|
|
2237
|
+
if (isinstance(inner, ast.Call)
|
|
2238
|
+
and isinstance(inner.func, ast.Attribute)
|
|
2239
|
+
and isinstance(inner.func.value, ast.Name)
|
|
2240
|
+
and inner.func.value.id in loop_var_types):
|
|
2241
|
+
cls_name = loop_var_types[inner.func.value.id]
|
|
2242
|
+
method_name = inner.func.attr
|
|
2243
|
+
self.cfunc_needed.setdefault(cls_name, set()).add(method_name)
|
|
2244
|
+
|
|
2245
|
+
if self.cfunc_needed:
|
|
2246
|
+
self.applied_strategies.append("loop-var-obj-type")
|
|
2247
|
+
|
|
2248
|
+
# -----------------------------------------------------------------
|
|
2249
|
+
# Transform class ASTs → cdef classes
|
|
2250
|
+
# -----------------------------------------------------------------
|
|
2251
|
+
|
|
2252
|
+
def _transform_classes(self) -> None:
|
|
2253
|
+
for plan in self.class_plans.values():
|
|
2254
|
+
if plan.skip:
|
|
2255
|
+
continue
|
|
2256
|
+
self._build_cdef_class(plan)
|
|
2257
|
+
if self.cdef_class_names:
|
|
2258
|
+
self.applied_strategies.append("cdef-class")
|
|
2259
|
+
|
|
2260
|
+
def _has_noncdef_subclass(self, cls_name: str) -> bool:
|
|
2261
|
+
"""Check if any non-cdef (skipped) class inherits from cls_name."""
|
|
2262
|
+
for plan in self.class_plans.values():
|
|
2263
|
+
if plan.skip:
|
|
2264
|
+
for base in plan.node.bases:
|
|
2265
|
+
base_name = (base.id if isinstance(base, ast.Name)
|
|
2266
|
+
else getattr(base, "attr", None))
|
|
2267
|
+
if base_name == cls_name:
|
|
2268
|
+
return True
|
|
2269
|
+
return False
|
|
2270
|
+
|
|
2271
|
+
def _expand_method_aliases(self, node: ast.ClassDef) -> None:
|
|
2272
|
+
"""Expand method aliases like __rmul__ = __mul__ into proper methods.
|
|
2273
|
+
|
|
2274
|
+
Cython cdef classes don't support class-attribute method aliases.
|
|
2275
|
+
Convert: __rmul__ = __mul__
|
|
2276
|
+
Into: def __rmul__(self, other):
|
|
2277
|
+
return self.__mul__(other)
|
|
2278
|
+
"""
|
|
2279
|
+
# Collect names of actual methods and their arg signatures
|
|
2280
|
+
method_sigs: dict[str, ast.arguments] = {}
|
|
2281
|
+
for stmt in node.body:
|
|
2282
|
+
if isinstance(stmt, ast.FunctionDef):
|
|
2283
|
+
method_sigs[stmt.name] = stmt.args
|
|
2284
|
+
|
|
2285
|
+
new_body = []
|
|
2286
|
+
for stmt in node.body:
|
|
2287
|
+
if (isinstance(stmt, ast.Assign)
|
|
2288
|
+
and len(stmt.targets) == 1
|
|
2289
|
+
and isinstance(stmt.targets[0], ast.Name)
|
|
2290
|
+
and isinstance(stmt.value, ast.Name)
|
|
2291
|
+
and stmt.targets[0].id.startswith("__")
|
|
2292
|
+
and stmt.targets[0].id.endswith("__")
|
|
2293
|
+
and stmt.value.id in method_sigs):
|
|
2294
|
+
# This is a method alias like __rmul__ = __mul__
|
|
2295
|
+
alias_name = stmt.targets[0].id
|
|
2296
|
+
target_method = stmt.value.id
|
|
2297
|
+
target_args = method_sigs[target_method]
|
|
2298
|
+
# Copy the target method's args (excluding self)
|
|
2299
|
+
call_args = [
|
|
2300
|
+
ast.Name(id=a.arg, ctx=ast.Load())
|
|
2301
|
+
for a in target_args.args
|
|
2302
|
+
if a.arg != "self"
|
|
2303
|
+
]
|
|
2304
|
+
wrapper = ast.FunctionDef(
|
|
2305
|
+
name=alias_name,
|
|
2306
|
+
args=copy.deepcopy(target_args),
|
|
2307
|
+
body=[ast.Return(value=ast.Call(
|
|
2308
|
+
func=ast.Attribute(
|
|
2309
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
2310
|
+
attr=target_method, ctx=ast.Load()),
|
|
2311
|
+
args=call_args,
|
|
2312
|
+
keywords=[],
|
|
2313
|
+
))],
|
|
2314
|
+
decorator_list=[],
|
|
2315
|
+
returns=None,
|
|
2316
|
+
)
|
|
2317
|
+
new_body.append(wrapper)
|
|
2318
|
+
else:
|
|
2319
|
+
new_body.append(stmt)
|
|
2320
|
+
node.body = new_body
|
|
2321
|
+
|
|
2322
|
+
def _build_cdef_class(self, plan: ClassPlan) -> None:
|
|
2323
|
+
node = plan.node
|
|
2324
|
+
|
|
2325
|
+
# @cython.cclass
|
|
2326
|
+
node.decorator_list.insert(0, ast.Attribute(
|
|
2327
|
+
value=ast.Name(id="cython", ctx=ast.Load()),
|
|
2328
|
+
attr="cclass", ctx=ast.Load()))
|
|
2329
|
+
|
|
2330
|
+
# Attribute declarations
|
|
2331
|
+
# needs_dict means external code accesses obj.attr — use visibility="public"
|
|
2332
|
+
# so the field is accessible from non-cdef code. But we do NOT add __dict__
|
|
2333
|
+
# because visibility="public" already provides Python-level access and
|
|
2334
|
+
# __dict__ forces a dict allocation + lookup overhead on every access.
|
|
2335
|
+
attr_decls = []
|
|
2336
|
+
use_public = plan.needs_dict
|
|
2337
|
+
for attr_name, ctype in sorted(plan.all_attrs.items()):
|
|
2338
|
+
type_expr = _ctype_to_ast(ctype)
|
|
2339
|
+
if use_public:
|
|
2340
|
+
attr_decls.append(ast.Assign(
|
|
2341
|
+
targets=[ast.Name(id=attr_name, ctx=ast.Store())],
|
|
2342
|
+
value=ast.Call(
|
|
2343
|
+
func=ast.Attribute(
|
|
2344
|
+
value=ast.Name(id="cython", ctx=ast.Load()),
|
|
2345
|
+
attr="declare", ctx=ast.Load()),
|
|
2346
|
+
args=[type_expr],
|
|
2347
|
+
keywords=[ast.keyword(
|
|
2348
|
+
arg="visibility",
|
|
2349
|
+
value=ast.Constant(value="public"))])))
|
|
2350
|
+
else:
|
|
2351
|
+
attr_decls.append(ast.AnnAssign(
|
|
2352
|
+
target=ast.Name(id=attr_name, ctx=ast.Store()),
|
|
2353
|
+
annotation=type_expr,
|
|
2354
|
+
value=None, simple=1))
|
|
2355
|
+
|
|
2356
|
+
# Remove __slots__
|
|
2357
|
+
node.body = [stmt for stmt in node.body
|
|
2358
|
+
if not (isinstance(stmt, ast.Assign)
|
|
2359
|
+
and any(isinstance(t, ast.Name)
|
|
2360
|
+
and t.id == "__slots__"
|
|
2361
|
+
for t in stmt.targets))]
|
|
2362
|
+
node.body = attr_decls + node.body
|
|
2363
|
+
|
|
2364
|
+
# Expand method aliases: __rmul__ = __mul__ → def __rmul__(self, *a, **kw)
|
|
2365
|
+
# Cython cdef classes don't support class-attribute method aliases.
|
|
2366
|
+
self._expand_method_aliases(node)
|
|
2367
|
+
|
|
2368
|
+
# cfunc method generation — generate for ALL eligible methods.
|
|
2369
|
+
# Even when the receiver is untyped (Python object), the cfunc method
|
|
2370
|
+
# has typed self and typed params, making attribute access C-level.
|
|
2371
|
+
# BUT: skip cfunc for classes that have non-cdef subclasses —
|
|
2372
|
+
# polymorphic dispatch goes through Python anyway, and the wrapper
|
|
2373
|
+
# just adds overhead.
|
|
2374
|
+
has_noncdef_subclass = self._has_noncdef_subclass(plan.name)
|
|
2375
|
+
new_body = []
|
|
2376
|
+
for stmt in node.body:
|
|
2377
|
+
new_body.append(stmt)
|
|
2378
|
+
if (isinstance(stmt, ast.FunctionDef)
|
|
2379
|
+
and self._is_cfunc_eligible(stmt)
|
|
2380
|
+
and not has_noncdef_subclass):
|
|
2381
|
+
fast_name, fast_method = self._make_cfunc_method(stmt, plan)
|
|
2382
|
+
plan.cfunc_methods[stmt.name] = fast_name
|
|
2383
|
+
idx = new_body.index(stmt)
|
|
2384
|
+
new_body.insert(idx + 1, fast_method)
|
|
2385
|
+
|
|
2386
|
+
# Rewrite original methods → thin wrappers with typed params
|
|
2387
|
+
for i, stmt in enumerate(new_body):
|
|
2388
|
+
if (isinstance(stmt, ast.FunctionDef)
|
|
2389
|
+
and stmt.name in plan.cfunc_methods):
|
|
2390
|
+
fast_name = plan.cfunc_methods[stmt.name]
|
|
2391
|
+
new_body[i] = self._make_wrapper(stmt, fast_name, plan)
|
|
2392
|
+
|
|
2393
|
+
node.body = new_body
|
|
2394
|
+
if plan.cfunc_methods:
|
|
2395
|
+
self.applied_strategies.append("cfunc-method")
|
|
2396
|
+
|
|
2397
|
+
def _is_cfunc_eligible(self, method: ast.FunctionDef) -> bool:
|
|
2398
|
+
name = method.name
|
|
2399
|
+
if name.startswith("__") and name.endswith("__"):
|
|
2400
|
+
return False
|
|
2401
|
+
if method.decorator_list:
|
|
2402
|
+
return False
|
|
2403
|
+
for node in ast.walk(method):
|
|
2404
|
+
if isinstance(node, (ast.Yield, ast.YieldFrom)):
|
|
2405
|
+
return False
|
|
2406
|
+
args = method.args
|
|
2407
|
+
if args.vararg or args.kwarg or args.kwonlyargs or args.defaults:
|
|
2408
|
+
return False
|
|
2409
|
+
return True
|
|
2410
|
+
|
|
2411
|
+
def _make_cfunc_method(self, method: ast.FunctionDef,
|
|
2412
|
+
plan: ClassPlan) -> tuple[str, ast.FunctionDef]:
|
|
2413
|
+
fast_name = f"_{method.name}_fast"
|
|
2414
|
+
new_body = [copy.deepcopy(stmt) for stmt in method.body]
|
|
2415
|
+
new_args = copy.deepcopy(method.args)
|
|
2416
|
+
|
|
2417
|
+
# @cython.locals for typed params (inferred from __init__ assignments)
|
|
2418
|
+
locals_decls: dict[str, str] = {}
|
|
2419
|
+
init = next((s for s in plan.node.body
|
|
2420
|
+
if isinstance(s, ast.FunctionDef)
|
|
2421
|
+
and s.name == "__init__"), None)
|
|
2422
|
+
if init:
|
|
2423
|
+
for stmt in ast.walk(init):
|
|
2424
|
+
if (isinstance(stmt, ast.Assign)
|
|
2425
|
+
and len(stmt.targets) == 1
|
|
2426
|
+
and isinstance(stmt.targets[0], ast.Attribute)
|
|
2427
|
+
and isinstance(stmt.targets[0].value, ast.Name)
|
|
2428
|
+
and stmt.targets[0].value.id == "self"):
|
|
2429
|
+
attr_name = stmt.targets[0].attr
|
|
2430
|
+
attr_type = plan.all_attrs.get(attr_name, OBJECT)
|
|
2431
|
+
if attr_type != OBJECT and isinstance(stmt.value, ast.Name):
|
|
2432
|
+
locals_decls[stmt.value.id] = attr_type
|
|
2433
|
+
|
|
2434
|
+
# Infer param types from comparisons with typed attrs
|
|
2435
|
+
for node in ast.walk(method):
|
|
2436
|
+
if (isinstance(node, ast.Compare)
|
|
2437
|
+
and isinstance(node.left, ast.Attribute)
|
|
2438
|
+
and isinstance(node.left.value, ast.Name)
|
|
2439
|
+
and node.left.value.id == "self"):
|
|
2440
|
+
attr_type = plan.all_attrs.get(node.left.attr, OBJECT)
|
|
2441
|
+
if attr_type != OBJECT:
|
|
2442
|
+
for comparator in node.comparators:
|
|
2443
|
+
if isinstance(comparator, ast.Name):
|
|
2444
|
+
locals_decls.setdefault(comparator.id, attr_type)
|
|
2445
|
+
|
|
2446
|
+
# Annotate params that access cdef class attrs (e.g. other: 'Point')
|
|
2447
|
+
# This enables C-level attribute access inside the cfunc method.
|
|
2448
|
+
cls_name = plan.name
|
|
2449
|
+
for arg in new_args.args:
|
|
2450
|
+
if arg.arg == "self":
|
|
2451
|
+
continue
|
|
2452
|
+
for node in ast.walk(method):
|
|
2453
|
+
if (isinstance(node, ast.Attribute)
|
|
2454
|
+
and isinstance(node.value, ast.Name)
|
|
2455
|
+
and node.value.id == arg.arg
|
|
2456
|
+
and node.attr in plan.all_attrs):
|
|
2457
|
+
arg.annotation = ast.Constant(value=cls_name)
|
|
2458
|
+
break
|
|
2459
|
+
|
|
2460
|
+
fast_method = ast.FunctionDef(
|
|
2461
|
+
name=fast_name,
|
|
2462
|
+
args=new_args,
|
|
2463
|
+
body=new_body,
|
|
2464
|
+
decorator_list=[],
|
|
2465
|
+
returns=copy.deepcopy(method.returns),
|
|
2466
|
+
)
|
|
2467
|
+
fast_method.decorator_list.append(ast.Attribute(
|
|
2468
|
+
value=ast.Name(id="cython", ctx=ast.Load()),
|
|
2469
|
+
attr="cfunc", ctx=ast.Load()))
|
|
2470
|
+
|
|
2471
|
+
if locals_decls:
|
|
2472
|
+
fast_method.decorator_list.append(ast.Call(
|
|
2473
|
+
func=ast.Attribute(
|
|
2474
|
+
value=ast.Name(id="cython", ctx=ast.Load()),
|
|
2475
|
+
attr="locals", ctx=ast.Load()),
|
|
2476
|
+
args=[],
|
|
2477
|
+
keywords=[ast.keyword(arg=n, value=_ctype_to_ast(t))
|
|
2478
|
+
for n, t in sorted(locals_decls.items())]))
|
|
2479
|
+
|
|
2480
|
+
# Return type inference
|
|
2481
|
+
ret_type = self._infer_return_type(method, plan)
|
|
2482
|
+
if ret_type:
|
|
2483
|
+
fast_method.decorator_list.append(ast.Call(
|
|
2484
|
+
func=ast.Attribute(
|
|
2485
|
+
value=ast.Name(id="cython", ctx=ast.Load()),
|
|
2486
|
+
attr="returns", ctx=ast.Load()),
|
|
2487
|
+
args=[_ctype_to_ast(ret_type)],
|
|
2488
|
+
keywords=[]))
|
|
2489
|
+
|
|
2490
|
+
return fast_name, fast_method
|
|
2491
|
+
|
|
2492
|
+
def _infer_return_type(self, method: ast.FunctionDef,
|
|
2493
|
+
plan: ClassPlan) -> str | None:
|
|
2494
|
+
for node in ast.walk(method):
|
|
2495
|
+
if isinstance(node, ast.Return) and node.value is not None:
|
|
2496
|
+
val = node.value
|
|
2497
|
+
if (isinstance(val, ast.Attribute)
|
|
2498
|
+
and isinstance(val.value, ast.Name)
|
|
2499
|
+
and val.value.id == "self"):
|
|
2500
|
+
attr_type = plan.all_attrs.get(val.attr, OBJECT)
|
|
2501
|
+
if attr_type == DOUBLE:
|
|
2502
|
+
return DOUBLE
|
|
2503
|
+
if isinstance(val, ast.Compare):
|
|
2504
|
+
return BINT
|
|
2505
|
+
if isinstance(val, ast.BinOp):
|
|
2506
|
+
lt = self._expr_type(val.left, plan)
|
|
2507
|
+
rt = self._expr_type(val.right, plan)
|
|
2508
|
+
if DOUBLE in (lt, rt):
|
|
2509
|
+
return DOUBLE
|
|
2510
|
+
if lt == LONGLONG and rt == LONGLONG:
|
|
2511
|
+
return LONGLONG
|
|
2512
|
+
if isinstance(val, ast.Constant):
|
|
2513
|
+
if isinstance(val.value, bool):
|
|
2514
|
+
return BINT
|
|
2515
|
+
if isinstance(val.value, float):
|
|
2516
|
+
return DOUBLE
|
|
2517
|
+
return None
|
|
2518
|
+
|
|
2519
|
+
def _expr_type(self, node, plan: ClassPlan) -> str:
|
|
2520
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) \
|
|
2521
|
+
and node.value.id == "self":
|
|
2522
|
+
return plan.all_attrs.get(node.attr, OBJECT)
|
|
2523
|
+
if isinstance(node, ast.Constant):
|
|
2524
|
+
if isinstance(node.value, float):
|
|
2525
|
+
return DOUBLE
|
|
2526
|
+
if isinstance(node.value, int):
|
|
2527
|
+
return LONGLONG
|
|
2528
|
+
return OBJECT
|
|
2529
|
+
|
|
2530
|
+
def _make_wrapper(self, method: ast.FunctionDef, fast_name: str,
|
|
2531
|
+
plan: ClassPlan) -> ast.FunctionDef:
|
|
2532
|
+
args = copy.deepcopy(method.args)
|
|
2533
|
+
call_args = [ast.Name(id=a.arg, ctx=ast.Load())
|
|
2534
|
+
for a in args.args if a.arg != "self"]
|
|
2535
|
+
cls_name = plan.name
|
|
2536
|
+
|
|
2537
|
+
# Infer parameter types for the wrapper signature.
|
|
2538
|
+
# Typed params let Cython avoid Python-level boxing/unboxing at the
|
|
2539
|
+
# call boundary, which is the dominant overhead for thin wrappers.
|
|
2540
|
+
wrapper_param_types = self._infer_wrapper_param_types(method, plan)
|
|
2541
|
+
|
|
2542
|
+
for arg in args.args:
|
|
2543
|
+
if arg.arg == "self":
|
|
2544
|
+
continue
|
|
2545
|
+
inferred = wrapper_param_types.get(arg.arg)
|
|
2546
|
+
if inferred == cls_name:
|
|
2547
|
+
# Same-class param (e.g. other: 'P') — string annotation
|
|
2548
|
+
arg.annotation = ast.Constant(value=cls_name)
|
|
2549
|
+
elif inferred and inferred != OBJECT:
|
|
2550
|
+
# Scalar param (e.g. s: cython.double) — Cython type
|
|
2551
|
+
arg.annotation = _ctype_to_ast(inferred)
|
|
2552
|
+
|
|
2553
|
+
return ast.FunctionDef(
|
|
2554
|
+
name=method.name,
|
|
2555
|
+
args=args,
|
|
2556
|
+
body=[ast.Return(value=ast.Call(
|
|
2557
|
+
func=ast.Attribute(
|
|
2558
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
2559
|
+
attr=fast_name, ctx=ast.Load()),
|
|
2560
|
+
args=call_args,
|
|
2561
|
+
keywords=[]))],
|
|
2562
|
+
decorator_list=copy.deepcopy(method.decorator_list),
|
|
2563
|
+
returns=copy.deepcopy(method.returns),
|
|
2564
|
+
)
|
|
2565
|
+
|
|
2566
|
+
def _infer_wrapper_param_types(self, method: ast.FunctionDef,
|
|
2567
|
+
plan: ClassPlan) -> dict[str, str]:
|
|
2568
|
+
"""Infer Cython types for wrapper parameters.
|
|
2569
|
+
|
|
2570
|
+
A parameter gets a type if it interacts with a typed self.attr:
|
|
2571
|
+
- Direct attribute access: other.x -> other is the cdef class type
|
|
2572
|
+
- Arithmetic with typed attr: self.x *= s -> s is the attr's type
|
|
2573
|
+
- Comparison with typed attr: self.x == v -> v is the attr's type
|
|
2574
|
+
- Assignment from param: self.x = s -> s is the attr's type
|
|
2575
|
+
"""
|
|
2576
|
+
result: dict[str, str] = {}
|
|
2577
|
+
cls_name = plan.name
|
|
2578
|
+
|
|
2579
|
+
for arg in method.args.args:
|
|
2580
|
+
if arg.arg == "self":
|
|
2581
|
+
continue
|
|
2582
|
+
arg_name = arg.arg
|
|
2583
|
+
found = False
|
|
2584
|
+
for node in ast.walk(method):
|
|
2585
|
+
# Case 1: param.attr — param is cdef class type
|
|
2586
|
+
if (isinstance(node, ast.Attribute)
|
|
2587
|
+
and isinstance(node.value, ast.Name)
|
|
2588
|
+
and node.value.id == arg_name
|
|
2589
|
+
and node.attr in plan.all_attrs):
|
|
2590
|
+
result[arg_name] = cls_name
|
|
2591
|
+
found = True
|
|
2592
|
+
break
|
|
2593
|
+
|
|
2594
|
+
# Case 2: self.attr OP= param (augmented assign)
|
|
2595
|
+
if (isinstance(node, ast.AugAssign)
|
|
2596
|
+
and isinstance(node.target, ast.Attribute)
|
|
2597
|
+
and isinstance(node.target.value, ast.Name)
|
|
2598
|
+
and node.target.value.id == "self"
|
|
2599
|
+
and node.target.attr in plan.all_attrs
|
|
2600
|
+
and isinstance(node.value, ast.Name)
|
|
2601
|
+
and node.value.id == arg_name):
|
|
2602
|
+
attr_type = plan.all_attrs.get(node.target.attr, OBJECT)
|
|
2603
|
+
if attr_type != OBJECT:
|
|
2604
|
+
result[arg_name] = attr_type
|
|
2605
|
+
found = True
|
|
2606
|
+
break
|
|
2607
|
+
|
|
2608
|
+
# Case 3: param OP= self.attr (augmented assign, reversed)
|
|
2609
|
+
if (isinstance(node, ast.AugAssign)
|
|
2610
|
+
and isinstance(node.target, ast.Name)
|
|
2611
|
+
and node.target.id == arg_name
|
|
2612
|
+
and isinstance(node.value, ast.Attribute)
|
|
2613
|
+
and isinstance(node.value.value, ast.Name)
|
|
2614
|
+
and node.value.value.id == "self"
|
|
2615
|
+
and node.value.attr in plan.all_attrs):
|
|
2616
|
+
attr_type = plan.all_attrs.get(node.value.attr, OBJECT)
|
|
2617
|
+
if attr_type != OBJECT:
|
|
2618
|
+
result[arg_name] = attr_type
|
|
2619
|
+
found = True
|
|
2620
|
+
break
|
|
2621
|
+
|
|
2622
|
+
# Case 4: self.attr OP param or param OP self.attr
|
|
2623
|
+
if isinstance(node, ast.BinOp):
|
|
2624
|
+
for side, other_side in [
|
|
2625
|
+
(node.left, node.right),
|
|
2626
|
+
(node.right, node.left)]:
|
|
2627
|
+
if (isinstance(side, ast.Name)
|
|
2628
|
+
and side.id == arg_name
|
|
2629
|
+
and isinstance(other_side, ast.Attribute)
|
|
2630
|
+
and isinstance(other_side.value, ast.Name)
|
|
2631
|
+
and other_side.value.id == "self"
|
|
2632
|
+
and other_side.attr in plan.all_attrs):
|
|
2633
|
+
attr_type = plan.all_attrs.get(
|
|
2634
|
+
other_side.attr, OBJECT)
|
|
2635
|
+
if attr_type != OBJECT:
|
|
2636
|
+
result[arg_name] = attr_type
|
|
2637
|
+
found = True
|
|
2638
|
+
break
|
|
2639
|
+
|
|
2640
|
+
# Case 5: self.attr == param (comparison)
|
|
2641
|
+
if isinstance(node, ast.Compare):
|
|
2642
|
+
if (isinstance(node.left, ast.Attribute)
|
|
2643
|
+
and isinstance(node.left.value, ast.Name)
|
|
2644
|
+
and node.left.value.id == "self"
|
|
2645
|
+
and node.left.attr in plan.all_attrs):
|
|
2646
|
+
for comp in node.comparators:
|
|
2647
|
+
if (isinstance(comp, ast.Name)
|
|
2648
|
+
and comp.id == arg_name):
|
|
2649
|
+
attr_type = plan.all_attrs.get(
|
|
2650
|
+
node.left.attr, OBJECT)
|
|
2651
|
+
if attr_type != OBJECT:
|
|
2652
|
+
result[arg_name] = attr_type
|
|
2653
|
+
found = True
|
|
2654
|
+
break
|
|
2655
|
+
|
|
2656
|
+
# Case 6: self.attr = param (assignment)
|
|
2657
|
+
if (isinstance(node, ast.Assign)
|
|
2658
|
+
and len(node.targets) == 1
|
|
2659
|
+
and isinstance(node.targets[0], ast.Attribute)
|
|
2660
|
+
and isinstance(node.targets[0].value, ast.Name)
|
|
2661
|
+
and node.targets[0].value.id == "self"
|
|
2662
|
+
and node.targets[0].attr in plan.all_attrs
|
|
2663
|
+
and isinstance(node.value, ast.Name)
|
|
2664
|
+
and node.value.id == arg_name):
|
|
2665
|
+
attr_type = plan.all_attrs.get(
|
|
2666
|
+
node.targets[0].attr, OBJECT)
|
|
2667
|
+
if attr_type != OBJECT:
|
|
2668
|
+
result[arg_name] = attr_type
|
|
2669
|
+
found = True
|
|
2670
|
+
break
|
|
2671
|
+
|
|
2672
|
+
if found:
|
|
2673
|
+
break
|
|
2674
|
+
|
|
2675
|
+
return result
|
|
2676
|
+
|
|
2677
|
+
# -----------------------------------------------------------------
|
|
2678
|
+
# Function-level transforms
|
|
2679
|
+
# -----------------------------------------------------------------
|
|
2680
|
+
|
|
2681
|
+
def _transform_functions(self) -> None:
|
|
2682
|
+
all_cfunc_methods: dict[str, dict[str, str]] = {}
|
|
2683
|
+
for plan in self.class_plans.values():
|
|
2684
|
+
if plan.skip:
|
|
2685
|
+
continue
|
|
2686
|
+
all_cfunc_methods[plan.name] = plan.cfunc_methods
|
|
2687
|
+
|
|
2688
|
+
for node in self.tree.body:
|
|
2689
|
+
if not isinstance(node, ast.FunctionDef):
|
|
2690
|
+
continue
|
|
2691
|
+
self._transform_one_function(node, all_cfunc_methods)
|
|
2692
|
+
|
|
2693
|
+
def _transform_one_function(self, func: ast.FunctionDef,
|
|
2694
|
+
all_cfunc_methods: dict[str, dict[str, str]]) -> None:
|
|
2695
|
+
sc = _Scanner(func)
|
|
2696
|
+
if sc.dynamic or sc.generator or sc.bad_except or sc.global_nonlocal:
|
|
2697
|
+
return
|
|
2698
|
+
|
|
2699
|
+
solver = _Solver(sc)
|
|
2700
|
+
solver.solve()
|
|
2701
|
+
|
|
2702
|
+
# Directives
|
|
2703
|
+
directives = [
|
|
2704
|
+
("boundscheck", False),
|
|
2705
|
+
("wraparound", False),
|
|
2706
|
+
("cdivision", True),
|
|
2707
|
+
]
|
|
2708
|
+
for name, val in directives:
|
|
2709
|
+
dec = ast.Call(
|
|
2710
|
+
func=ast.Attribute(
|
|
2711
|
+
value=ast.Name(id="cython", ctx=ast.Load()),
|
|
2712
|
+
attr=name, ctx=ast.Load()),
|
|
2713
|
+
args=[ast.Constant(value=val)],
|
|
2714
|
+
keywords=[])
|
|
2715
|
+
dec.lineno = max(func.lineno - 1, 1)
|
|
2716
|
+
dec.col_offset = func.col_offset
|
|
2717
|
+
func.decorator_list.append(dec)
|
|
2718
|
+
|
|
2719
|
+
# Reuse loop var types from _infer_loop_var_types
|
|
2720
|
+
loop_var_types: dict[str, str] = {}
|
|
2721
|
+
for inner in ast.walk(func):
|
|
2722
|
+
if isinstance(inner, ast.For) and isinstance(inner.target, ast.Name):
|
|
2723
|
+
var_name = inner.target.id
|
|
2724
|
+
iter_node = inner.iter
|
|
2725
|
+
if (isinstance(iter_node, ast.ListComp)
|
|
2726
|
+
and isinstance(iter_node.elt, ast.Call)
|
|
2727
|
+
and isinstance(iter_node.elt.func, ast.Name)
|
|
2728
|
+
and iter_node.elt.func.id in self.cdef_class_names):
|
|
2729
|
+
loop_var_types[var_name] = iter_node.elt.func.id
|
|
2730
|
+
elif isinstance(iter_node, ast.Name):
|
|
2731
|
+
cls = self._find_list_var_type(func, iter_node.id)
|
|
2732
|
+
if cls:
|
|
2733
|
+
loop_var_types[var_name] = cls
|
|
2734
|
+
|
|
2735
|
+
# Merge: typed locals from solver + loop var object types
|
|
2736
|
+
typed_locals = dict(solver.typed_locals)
|
|
2737
|
+
for var in self.loopify_float_vars.get(func.name, ()):
|
|
2738
|
+
if var not in typed_locals:
|
|
2739
|
+
typed_locals[var] = DOUBLE
|
|
2740
|
+
for var, cls in loop_var_types.items():
|
|
2741
|
+
typed_locals[var] = cls
|
|
2742
|
+
|
|
2743
|
+
# Closure-capture guard: a variable referenced inside a nested
|
|
2744
|
+
# genexp/listcomp/setcomp/dictcomp *body* (not its own loop target)
|
|
2745
|
+
# is captured by a Python closure. Cython C-types locals live
|
|
2746
|
+
# outside the cell protocol, so a typed captured var silently
|
|
2747
|
+
# yields wrong values (e.g. binary_trees `d`). Never type them.
|
|
2748
|
+
captured = set()
|
|
2749
|
+
for comp in ast.walk(func):
|
|
2750
|
+
if isinstance(comp, (ast.GeneratorExp, ast.ListComp,
|
|
2751
|
+
ast.SetComp, ast.DictComp)):
|
|
2752
|
+
for name_node in ast.walk(comp):
|
|
2753
|
+
if isinstance(name_node, ast.Name):
|
|
2754
|
+
captured.add(name_node.id)
|
|
2755
|
+
# comprehension loop targets are local to the comprehension, not captures
|
|
2756
|
+
for comp in ast.walk(func):
|
|
2757
|
+
if isinstance(comp, (ast.GeneratorExp, ast.ListComp,
|
|
2758
|
+
ast.SetComp, ast.DictComp)):
|
|
2759
|
+
for gen in comp.generators:
|
|
2760
|
+
tgt = gen.target
|
|
2761
|
+
for name_node in ast.walk(tgt):
|
|
2762
|
+
if isinstance(name_node, ast.Name):
|
|
2763
|
+
captured.discard(name_node.id)
|
|
2764
|
+
for name in captured:
|
|
2765
|
+
typed_locals.pop(name, None)
|
|
2766
|
+
|
|
2767
|
+
# Infer cdef class types for locals that access cdef attributes
|
|
2768
|
+
# (e.g. a = points[i]; a.x → a is 'P' type for C-level field access)
|
|
2769
|
+
cdef_locals = self._infer_cdef_locals(func, loop_var_types)
|
|
2770
|
+
for var, cls in cdef_locals.items():
|
|
2771
|
+
if var not in typed_locals:
|
|
2772
|
+
typed_locals[var] = cls
|
|
2773
|
+
|
|
2774
|
+
# Merge cdef_locals into loop_var_types for cfunc rewriting
|
|
2775
|
+
all_obj_vars = dict(loop_var_types)
|
|
2776
|
+
all_obj_vars.update(cdef_locals)
|
|
2777
|
+
|
|
2778
|
+
# Rewrite cfunc calls for typed obj vars
|
|
2779
|
+
if all_obj_vars:
|
|
2780
|
+
self._rewrite_cfunc_calls(func, all_obj_vars, all_cfunc_methods)
|
|
2781
|
+
|
|
2782
|
+
# @cython.locals
|
|
2783
|
+
if typed_locals:
|
|
2784
|
+
dec = ast.Call(
|
|
2785
|
+
func=ast.Attribute(
|
|
2786
|
+
value=ast.Name(id="cython", ctx=ast.Load()),
|
|
2787
|
+
attr="locals", ctx=ast.Load()),
|
|
2788
|
+
args=[],
|
|
2789
|
+
keywords=[ast.keyword(arg=n, value=_ctype_to_ast(t))
|
|
2790
|
+
for n, t in sorted(typed_locals.items())])
|
|
2791
|
+
dec.lineno = max(func.lineno - 1, 1)
|
|
2792
|
+
dec.col_offset = func.col_offset
|
|
2793
|
+
func.decorator_list.append(dec)
|
|
2794
|
+
self.applied_strategies.append("type-infer")
|
|
2795
|
+
|
|
2796
|
+
def _find_list_var_type(self, func: ast.FunctionDef,
|
|
2797
|
+
var_name: str) -> str | None:
|
|
2798
|
+
for node in ast.walk(func):
|
|
2799
|
+
if isinstance(node, ast.Assign):
|
|
2800
|
+
for target in node.targets:
|
|
2801
|
+
if isinstance(target, ast.Name) and target.id == var_name:
|
|
2802
|
+
val = node.value
|
|
2803
|
+
if (isinstance(val, ast.ListComp)
|
|
2804
|
+
and isinstance(val.elt, ast.Call)
|
|
2805
|
+
and isinstance(val.elt.func, ast.Name)
|
|
2806
|
+
and val.elt.func.id in self.cdef_class_names):
|
|
2807
|
+
return val.elt.func.id
|
|
2808
|
+
if (isinstance(val, ast.List) and val.elts
|
|
2809
|
+
and isinstance(val.elts[0], ast.Call)
|
|
2810
|
+
and isinstance(val.elts[0].func, ast.Name)
|
|
2811
|
+
and val.elts[0].func.id in self.cdef_class_names):
|
|
2812
|
+
return val.elts[0].func.id
|
|
2813
|
+
return None
|
|
2814
|
+
|
|
2815
|
+
def _infer_cdef_locals(self, func: ast.FunctionDef,
|
|
2816
|
+
known_vars: dict[str, str]) -> dict[str, str]:
|
|
2817
|
+
"""Infer cdef class types for locals that access cdef attributes.
|
|
2818
|
+
|
|
2819
|
+
When a module-level function does:
|
|
2820
|
+
a = points[i]
|
|
2821
|
+
dx = a.x - b.x
|
|
2822
|
+
Cython doesn't know that `a` and `b` are cdef class instances,
|
|
2823
|
+
so `a.x` goes through Python attribute lookup. By adding
|
|
2824
|
+
@cython.locals(a='P', b='P'), Cython does C-level field access.
|
|
2825
|
+
|
|
2826
|
+
Strategy: find all Name nodes that are used as the receiver of
|
|
2827
|
+
an Attribute access where the attribute name matches a cdef class
|
|
2828
|
+
field. Then trace back to find how that Name was assigned — if it
|
|
2829
|
+
came from a subscription/iteration of a list known to hold cdef
|
|
2830
|
+
instances, we can type it.
|
|
2831
|
+
|
|
2832
|
+
Also handle for-loop targets: for p in points → p is 'P' if
|
|
2833
|
+
points is known to hold P instances (tracked via known_vars or
|
|
2834
|
+
list construction).
|
|
2835
|
+
"""
|
|
2836
|
+
result: dict[str, str] = {}
|
|
2837
|
+
|
|
2838
|
+
# Build a map: attr_name -> set of cdef class names that have it
|
|
2839
|
+
attr_to_classes: dict[str, set[str]] = {}
|
|
2840
|
+
for plan in self.class_plans.values():
|
|
2841
|
+
if plan.skip:
|
|
2842
|
+
continue
|
|
2843
|
+
for attr_name in plan.all_attrs:
|
|
2844
|
+
attr_to_classes.setdefault(attr_name, set()).add(plan.name)
|
|
2845
|
+
|
|
2846
|
+
# Build a map: var_name -> cdef class type from assignments
|
|
2847
|
+
# We look for patterns like:
|
|
2848
|
+
# var = list_var[index] (where list_var holds cdef instances)
|
|
2849
|
+
# var = Call(cdef_class, ...) (direct construction)
|
|
2850
|
+
# for var in list_var (iteration over cdef list)
|
|
2851
|
+
var_types: dict[str, str] = dict(known_vars)
|
|
2852
|
+
|
|
2853
|
+
# Track which list vars hold cdef instances
|
|
2854
|
+
# by looking at append calls: points.append(P(...))
|
|
2855
|
+
list_to_class: dict[str, str] = {}
|
|
2856
|
+
for node in ast.walk(func):
|
|
2857
|
+
if (isinstance(node, ast.Expr)
|
|
2858
|
+
and isinstance(node.value, ast.Call)
|
|
2859
|
+
and isinstance(node.value.func, ast.Attribute)
|
|
2860
|
+
and node.value.func.attr == "append"
|
|
2861
|
+
and isinstance(node.value.func.value, ast.Name)
|
|
2862
|
+
and node.value.args
|
|
2863
|
+
and isinstance(node.value.args[0], ast.Call)
|
|
2864
|
+
and isinstance(node.value.args[0].func, ast.Name)
|
|
2865
|
+
and node.value.args[0].func.id in self.cdef_class_names):
|
|
2866
|
+
list_to_class[node.value.func.value.id] = \
|
|
2867
|
+
node.value.args[0].func.id
|
|
2868
|
+
|
|
2869
|
+
# Also check: var = [P(...) for ...] or var = [P(...), ...]
|
|
2870
|
+
for node in ast.walk(func):
|
|
2871
|
+
if isinstance(node, ast.Assign):
|
|
2872
|
+
for target in node.targets:
|
|
2873
|
+
if isinstance(target, ast.Name):
|
|
2874
|
+
cls = self._find_list_var_type(func, target.id)
|
|
2875
|
+
if cls:
|
|
2876
|
+
list_to_class[target.id] = cls
|
|
2877
|
+
|
|
2878
|
+
# Infer from for-loop targets
|
|
2879
|
+
for node in ast.walk(func):
|
|
2880
|
+
if (isinstance(node, ast.For)
|
|
2881
|
+
and isinstance(node.target, ast.Name)
|
|
2882
|
+
and isinstance(node.iter, ast.Name)
|
|
2883
|
+
and node.iter.id in list_to_class):
|
|
2884
|
+
var_types[node.target.id] = list_to_class[node.iter.id]
|
|
2885
|
+
|
|
2886
|
+
# Infer from assignments: var = list_var[index]
|
|
2887
|
+
for node in ast.walk(func):
|
|
2888
|
+
if (isinstance(node, ast.Assign)
|
|
2889
|
+
and len(node.targets) == 1
|
|
2890
|
+
and isinstance(node.targets[0], ast.Name)
|
|
2891
|
+
and isinstance(node.value, ast.Subscript)
|
|
2892
|
+
and isinstance(node.value.value, ast.Name)
|
|
2893
|
+
and node.value.value.id in list_to_class):
|
|
2894
|
+
var_types[node.targets[0].id] = \
|
|
2895
|
+
list_to_class[node.value.value.id]
|
|
2896
|
+
|
|
2897
|
+
# Infer from direct construction: var = CdefClass(...)
|
|
2898
|
+
for node in ast.walk(func):
|
|
2899
|
+
if (isinstance(node, ast.Assign)
|
|
2900
|
+
and len(node.targets) == 1
|
|
2901
|
+
and isinstance(node.targets[0], ast.Name)
|
|
2902
|
+
and isinstance(node.value, ast.Call)
|
|
2903
|
+
and isinstance(node.value.func, ast.Name)
|
|
2904
|
+
and node.value.func.id in self.cdef_class_names):
|
|
2905
|
+
var_types[node.targets[0].id] = node.value.func.id
|
|
2906
|
+
|
|
2907
|
+
# Now find all Name.attr where attr is a cdef field,
|
|
2908
|
+
# and the Name is in var_types
|
|
2909
|
+
for node in ast.walk(func):
|
|
2910
|
+
if (isinstance(node, ast.Attribute)
|
|
2911
|
+
and isinstance(node.value, ast.Name)
|
|
2912
|
+
and node.value.id in var_types
|
|
2913
|
+
and node.attr in attr_to_classes):
|
|
2914
|
+
cls = var_types[node.value.id]
|
|
2915
|
+
if cls in attr_to_classes[node.attr]:
|
|
2916
|
+
result[node.value.id] = cls
|
|
2917
|
+
|
|
2918
|
+
# Also find: var.attr = value (AugAssign on cdef field)
|
|
2919
|
+
for node in ast.walk(func):
|
|
2920
|
+
if (isinstance(node, ast.AugAssign)
|
|
2921
|
+
and isinstance(node.target, ast.Attribute)
|
|
2922
|
+
and isinstance(node.target.value, ast.Name)
|
|
2923
|
+
and node.target.value.id in var_types
|
|
2924
|
+
and node.target.attr in attr_to_classes):
|
|
2925
|
+
cls = var_types[node.target.value.id]
|
|
2926
|
+
if cls in attr_to_classes[node.target.attr]:
|
|
2927
|
+
result[node.target.value.id] = cls
|
|
2928
|
+
|
|
2929
|
+
# Also include vars that call cdef class methods: p.scale(...)
|
|
2930
|
+
# Even though 'scale' is a method name (not in all_attrs), typing p
|
|
2931
|
+
# as the cdef class lets Cython resolve the method call faster.
|
|
2932
|
+
# Check both direct: p.method() and chained: points[i].method()
|
|
2933
|
+
cdef_class_names = self.cdef_class_names
|
|
2934
|
+
for node in ast.walk(func):
|
|
2935
|
+
if (isinstance(node, ast.Call)
|
|
2936
|
+
and isinstance(node.func, ast.Attribute)
|
|
2937
|
+
and isinstance(node.func.value, ast.Name)
|
|
2938
|
+
and node.func.value.id in var_types):
|
|
2939
|
+
var_name = node.func.value.id
|
|
2940
|
+
cls = var_types[var_name]
|
|
2941
|
+
if cls in cdef_class_names:
|
|
2942
|
+
result[var_name] = cls
|
|
2943
|
+
|
|
2944
|
+
# Also handle direct subscript calls: points[i].distance(...)
|
|
2945
|
+
# points[i] is not a Name, so it won't be in var_types directly.
|
|
2946
|
+
# But we can type the list var itself so Cython optimizes subscription.
|
|
2947
|
+
# This is handled implicitly: if points is not a Name.attr pattern,
|
|
2948
|
+
# we at least ensure any loop var or assigned var from points is typed.
|
|
2949
|
+
|
|
2950
|
+
return result
|
|
2951
|
+
|
|
2952
|
+
def _rewrite_cfunc_calls(self, func: ast.FunctionDef,
|
|
2953
|
+
loop_var_types: dict[str, str],
|
|
2954
|
+
all_cfunc_methods: dict[str, dict[str, str]]) -> None:
|
|
2955
|
+
class _Rewriter(ast.NodeTransformer):
|
|
2956
|
+
def __init__(self, var_types, cfunc_map):
|
|
2957
|
+
self.var_types = var_types
|
|
2958
|
+
self.cfunc_map = cfunc_map
|
|
2959
|
+
|
|
2960
|
+
def visit_Call(self, node):
|
|
2961
|
+
self.generic_visit(node)
|
|
2962
|
+
if (isinstance(node.func, ast.Attribute)
|
|
2963
|
+
and isinstance(node.func.value, ast.Name)
|
|
2964
|
+
and node.func.value.id in self.var_types):
|
|
2965
|
+
var_type = self.var_types[node.func.value.id]
|
|
2966
|
+
method_name = node.func.attr
|
|
2967
|
+
fast_map = self.cfunc_map.get(var_type, {})
|
|
2968
|
+
if method_name in fast_map and not node.keywords:
|
|
2969
|
+
node.func.attr = fast_map[method_name]
|
|
2970
|
+
return node
|
|
2971
|
+
_Rewriter(loop_var_types, all_cfunc_methods).visit(func)
|
|
2972
|
+
|
|
2973
|
+
# -----------------------------------------------------------------
|
|
2974
|
+
# Ensure `import cython`
|
|
2975
|
+
# -----------------------------------------------------------------
|
|
2976
|
+
|
|
2977
|
+
def _ensure_cython_import(self) -> None:
|
|
2978
|
+
for node in self.tree.body:
|
|
2979
|
+
if (isinstance(node, ast.Import)
|
|
2980
|
+
and any(a.name == "cython" for a in node.names)):
|
|
2981
|
+
return
|
|
2982
|
+
# Check if it's already there as ast.ImportFrom
|
|
2983
|
+
for node in self.tree.body:
|
|
2984
|
+
if (isinstance(node, ast.ImportFrom)
|
|
2985
|
+
and node.module == "cython"):
|
|
2986
|
+
return
|
|
2987
|
+
self.tree.body.insert(0, ast.Import(
|
|
2988
|
+
names=[ast.alias(name="cython", asname=None)]))
|
|
2989
|
+
|
|
2990
|
+
|
|
2991
|
+
# ---------------------------------------------------------------------------
|
|
2992
|
+
# Type classification helpers (for attribute inference)
|
|
2993
|
+
# ---------------------------------------------------------------------------
|
|
2994
|
+
|
|
2995
|
+
def _classify_literal(node: ast.AST) -> str:
|
|
2996
|
+
if isinstance(node, ast.Constant):
|
|
2997
|
+
if isinstance(node.value, bool):
|
|
2998
|
+
return LONGLONG
|
|
2999
|
+
if isinstance(node.value, float):
|
|
3000
|
+
return DOUBLE
|
|
3001
|
+
if isinstance(node.value, int):
|
|
3002
|
+
return LONGLONG
|
|
3003
|
+
return OBJECT
|
|
3004
|
+
|
|
3005
|
+
|
|
3006
|
+
def _classify_expr(node: ast.AST) -> str | None:
|
|
3007
|
+
if isinstance(node, ast.Constant):
|
|
3008
|
+
if isinstance(node.value, bool):
|
|
3009
|
+
return LONGLONG
|
|
3010
|
+
if isinstance(node.value, float):
|
|
3011
|
+
return DOUBLE
|
|
3012
|
+
if isinstance(node.value, int):
|
|
3013
|
+
return LONGLONG
|
|
3014
|
+
if isinstance(node, ast.BinOp):
|
|
3015
|
+
lt = _classify_expr(node.left)
|
|
3016
|
+
rt = _classify_expr(node.right)
|
|
3017
|
+
if lt and rt:
|
|
3018
|
+
if DOUBLE in (lt, rt):
|
|
3019
|
+
return DOUBLE
|
|
3020
|
+
return LONGLONG
|
|
3021
|
+
return None
|
|
3022
|
+
if isinstance(node, ast.UnaryOp):
|
|
3023
|
+
return _classify_expr(node.operand)
|
|
3024
|
+
if isinstance(node, ast.Call):
|
|
3025
|
+
func = node.func
|
|
3026
|
+
if isinstance(func, ast.Name):
|
|
3027
|
+
if func.id == "float":
|
|
3028
|
+
return DOUBLE
|
|
3029
|
+
if func.id in ("int", "len", "abs"):
|
|
3030
|
+
return LONGLONG
|
|
3031
|
+
if isinstance(node, ast.Compare):
|
|
3032
|
+
return LONGLONG
|
|
3033
|
+
return None
|