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,522 @@
|
|
|
1
|
+
"""Pass 9 + Pass 10: recursive-function cdef lowering and pure-function
|
|
2
|
+
sum-LICM folding.
|
|
3
|
+
|
|
4
|
+
Pass 9 (cdef-rec): for module-level functions that (a) call themselves
|
|
5
|
+
recursively (directly or mutually), (b) are pure (no I/O, no
|
|
6
|
+
global/nonlocal writes, no dynamic calls), and (c) have solved argument
|
|
7
|
+
types from the type solver, emit a ``cdef`` C-function twin with typed
|
|
8
|
+
parameters + a thin ``def`` wrapper that keeps the original name and
|
|
9
|
+
signature visible to Python code. Recursive calls then go through the
|
|
10
|
+
C stack instead of the Python call protocol.
|
|
11
|
+
|
|
12
|
+
Pass 9 also lowers "pure leaf helpers": module functions that are not
|
|
13
|
+
themselves recursive but whose every call target is either a lowered
|
|
14
|
+
candidate or a default-parameter name bound (via ``def f(x, g=cand)``)
|
|
15
|
+
to a lowered candidate. For helpers, defaulted params are dropped from
|
|
16
|
+
the cdef twin and their call sites in the body are rewritten to the
|
|
17
|
+
candidate's ``__fast__`` twin — safe because default values were bound
|
|
18
|
+
at definition time and the ``def`` wrapper retains them for callers
|
|
19
|
+
that pass overrides.
|
|
20
|
+
|
|
21
|
+
Pass 10 (sum-licm): fold ``sum(F(args) for k in range(lo, hi))`` into
|
|
22
|
+
``(hi - lo) * __fast__F(args[k:=1])`` when:
|
|
23
|
+
|
|
24
|
+
- ``F`` is a lowered function (candidate or helper),
|
|
25
|
+
- ``F`` provably returns int (float folding is forbidden: IEEE 754
|
|
26
|
+
rounding of a*b differs from repeated addition; integer math is exact
|
|
27
|
+
under CPython's bignum arithmetic, so the fold is an identity), and
|
|
28
|
+
- ``F``'s result is provably independent of the comprehension loop
|
|
29
|
+
variable ``k`` — checked by name-expansion through F's parameter
|
|
30
|
+
bindings and local assignments, not by merely grepping the argument
|
|
31
|
+
list (binary_trees' ``make_check((k, d))`` unpacks ``k`` into a name
|
|
32
|
+
that is never used, so the fold is still valid).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import ast
|
|
38
|
+
import copy
|
|
39
|
+
import re
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_pure_stmt(stmt: ast.stmt, self_names: set[str]) -> bool:
|
|
43
|
+
"""Statement-level purity: assignments, if/return only."""
|
|
44
|
+
if isinstance(stmt, ast.Assign):
|
|
45
|
+
return True
|
|
46
|
+
if isinstance(stmt, ast.AugAssign):
|
|
47
|
+
t = stmt.target
|
|
48
|
+
return isinstance(t, ast.Name) and t.id in self_names
|
|
49
|
+
if isinstance(stmt, ast.Return):
|
|
50
|
+
return True
|
|
51
|
+
if isinstance(stmt, ast.If):
|
|
52
|
+
return (all(_is_pure_stmt(s, self_names) for s in stmt.body)
|
|
53
|
+
and all(_is_pure_stmt(s, self_names) for s in stmt.orelse))
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_pure_expr(node: ast.AST, allowed_calls: set[str]) -> bool:
|
|
58
|
+
"""Expression purity: names, constants, arithmetic, comparison,
|
|
59
|
+
boolean ops, tuple/list building, and calls ONLY to allowed
|
|
60
|
+
(known-pure) function names. Operator nodes (ast.Add etc.) are not
|
|
61
|
+
expressions and are never recursed into."""
|
|
62
|
+
if isinstance(node, ast.Name):
|
|
63
|
+
return True
|
|
64
|
+
if isinstance(node, ast.Constant):
|
|
65
|
+
return True
|
|
66
|
+
if isinstance(node, ast.Tuple):
|
|
67
|
+
return all(_is_pure_expr(e, allowed_calls) for e in node.elts)
|
|
68
|
+
if isinstance(node, ast.List):
|
|
69
|
+
return all(_is_pure_expr(e, allowed_calls) for e in node.elts)
|
|
70
|
+
if isinstance(node, ast.BinOp):
|
|
71
|
+
return (_is_pure_expr(node.left, allowed_calls)
|
|
72
|
+
and _is_pure_expr(node.right, allowed_calls))
|
|
73
|
+
if isinstance(node, ast.UnaryOp):
|
|
74
|
+
return _is_pure_expr(node.operand, allowed_calls)
|
|
75
|
+
if isinstance(node, ast.BoolOp):
|
|
76
|
+
return all(_is_pure_expr(v, allowed_calls) for v in node.values)
|
|
77
|
+
if isinstance(node, ast.Compare):
|
|
78
|
+
return (_is_pure_expr(node.left, allowed_calls)
|
|
79
|
+
and all(_is_pure_expr(c, allowed_calls)
|
|
80
|
+
for c in node.comparators))
|
|
81
|
+
if isinstance(node, ast.IfExp):
|
|
82
|
+
return (_is_pure_expr(node.test, allowed_calls)
|
|
83
|
+
and _is_pure_expr(node.body, allowed_calls)
|
|
84
|
+
and _is_pure_expr(node.orelse, allowed_calls))
|
|
85
|
+
if isinstance(node, ast.Call):
|
|
86
|
+
# name-directed call only (no method calls: x.f() unknown)
|
|
87
|
+
if isinstance(node.func, ast.Name):
|
|
88
|
+
if node.func.id in allowed_calls and not node.keywords:
|
|
89
|
+
return all(_is_pure_expr(a, allowed_calls)
|
|
90
|
+
for a in node.args)
|
|
91
|
+
return False
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _module_funcs(tree: ast.Module) -> dict[str, ast.FunctionDef]:
|
|
96
|
+
return {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef)}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _recursive_pure_candidates(tree: ast.Module) -> list[str]:
|
|
100
|
+
"""Names of module-level functions that call themselves (directly or
|
|
101
|
+
through the mutual-call graph) and whose bodies are pure statements
|
|
102
|
+
over allowed calls = {self} ∪ {other pure functions}."""
|
|
103
|
+
funcs = _module_funcs(tree)
|
|
104
|
+
calls: dict[str, set[str]] = {}
|
|
105
|
+
for name, fn in funcs.items():
|
|
106
|
+
called = set()
|
|
107
|
+
for sub in ast.walk(fn):
|
|
108
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name):
|
|
109
|
+
if sub.func.id in funcs:
|
|
110
|
+
called.add(sub.func.id)
|
|
111
|
+
calls[name] = called
|
|
112
|
+
|
|
113
|
+
pure: set[str] = set()
|
|
114
|
+
changed = True
|
|
115
|
+
while changed:
|
|
116
|
+
changed = False
|
|
117
|
+
for name, fn in funcs.items():
|
|
118
|
+
if name in pure:
|
|
119
|
+
continue
|
|
120
|
+
allowed = pure | {name}
|
|
121
|
+
if (all(_is_pure_stmt(s, {a.arg for a in fn.args.args})
|
|
122
|
+
for s in fn.body)
|
|
123
|
+
and all(_is_pure_expr(e, allowed)
|
|
124
|
+
for s in fn.body if isinstance(s, ast.Return)
|
|
125
|
+
for e in [s.value] if e is not None)
|
|
126
|
+
and calls[name] <= allowed):
|
|
127
|
+
pure.add(name)
|
|
128
|
+
changed = True
|
|
129
|
+
|
|
130
|
+
def _is_recursive(name: str) -> bool:
|
|
131
|
+
seen, stack = set(), [name]
|
|
132
|
+
while stack:
|
|
133
|
+
cur = stack.pop()
|
|
134
|
+
for nxt in calls.get(cur, ()):
|
|
135
|
+
if nxt == name:
|
|
136
|
+
return True
|
|
137
|
+
if nxt not in seen:
|
|
138
|
+
seen.add(nxt)
|
|
139
|
+
stack.append(nxt)
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
return [n for n in sorted(pure) if _is_recursive(n)]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _default_bound_params(fn: ast.FunctionDef) -> dict[str, str]:
|
|
146
|
+
"""{param_name: candidate_name} for trailing params whose default is
|
|
147
|
+
a bare Name referencing a module-level recursive-pure candidate."""
|
|
148
|
+
args = fn.args.args
|
|
149
|
+
defaults = fn.args.defaults
|
|
150
|
+
bound = {}
|
|
151
|
+
for param, dflt in zip(args[len(args) - len(defaults):], defaults):
|
|
152
|
+
if isinstance(dflt, ast.Name):
|
|
153
|
+
bound[param.arg] = dflt.id
|
|
154
|
+
return bound
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _pure_leaf_helpers(tree: ast.Module,
|
|
158
|
+
cands: list[str]) -> dict[str, dict[str, str]]:
|
|
159
|
+
"""Module-level non-recursive pure functions whose every in-body
|
|
160
|
+
call target is either a candidate or a default-param name bound to
|
|
161
|
+
a candidate. Returns {func_name: {param: candidate}}."""
|
|
162
|
+
funcs = _module_funcs(tree)
|
|
163
|
+
cand_set = set(cands)
|
|
164
|
+
helpers: dict[str, dict[str, str]] = {}
|
|
165
|
+
for name, fn in funcs.items():
|
|
166
|
+
if name in cand_set:
|
|
167
|
+
continue
|
|
168
|
+
bound = _default_bound_params(fn)
|
|
169
|
+
if not all(v in cand_set for v in bound.values()):
|
|
170
|
+
continue
|
|
171
|
+
allowed = cand_set | set(bound.keys()) | {name}
|
|
172
|
+
# every call in the BODY must target an allowed name
|
|
173
|
+
# (decorators like @cython.boundscheck(False) are ignored)
|
|
174
|
+
ok = True
|
|
175
|
+
for stmt in fn.body:
|
|
176
|
+
for sub in ast.walk(stmt):
|
|
177
|
+
if isinstance(sub, ast.Call):
|
|
178
|
+
if not (isinstance(sub.func, ast.Name)
|
|
179
|
+
and sub.func.id in allowed):
|
|
180
|
+
ok = False
|
|
181
|
+
break
|
|
182
|
+
if not ok:
|
|
183
|
+
break
|
|
184
|
+
if not ok:
|
|
185
|
+
continue
|
|
186
|
+
if not (all(_is_pure_stmt(s, {a.arg for a in fn.args.args})
|
|
187
|
+
for s in fn.body)
|
|
188
|
+
and all(_is_pure_expr(e, allowed)
|
|
189
|
+
for s in fn.body if isinstance(s, ast.Return)
|
|
190
|
+
for e in [s.value] if e is not None)):
|
|
191
|
+
continue
|
|
192
|
+
helpers[name] = bound
|
|
193
|
+
return helpers
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class _CdefRecLower(ast.NodeTransformer):
|
|
197
|
+
"""Rewrites eligible recursive functions and pure leaf helpers into
|
|
198
|
+
cdef twins + thin def wrappers."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, candidates: list[str], helpers: dict[str, dict],
|
|
201
|
+
arg_types: dict, int_returns: set[str]):
|
|
202
|
+
self.cands = set(candidates)
|
|
203
|
+
self.helpers = helpers # {name: {param: cand}}
|
|
204
|
+
self.arg_types = arg_types
|
|
205
|
+
self.int_returns = int_returns
|
|
206
|
+
self.added: list[str] = []
|
|
207
|
+
|
|
208
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
|
|
209
|
+
name = node.name
|
|
210
|
+
is_cand = name in self.cands
|
|
211
|
+
is_helper = name in self.helpers
|
|
212
|
+
if not (is_cand or is_helper):
|
|
213
|
+
return node
|
|
214
|
+
fast = f"__fast__{name}"
|
|
215
|
+
ats = self.arg_types.get(name, {})
|
|
216
|
+
|
|
217
|
+
if is_cand:
|
|
218
|
+
params = ", ".join(
|
|
219
|
+
f"{ats.get(a.arg, 'object')} {a.arg}"
|
|
220
|
+
for a in node.args.args)
|
|
221
|
+
else:
|
|
222
|
+
# helper: only non-defaulted params go into the cdef twin
|
|
223
|
+
bound = self.helpers[name]
|
|
224
|
+
ndef = len(node.args.defaults)
|
|
225
|
+
live = [a for a in node.args.args if a.arg not in bound]
|
|
226
|
+
if len(live) + ndef != len(node.args.args):
|
|
227
|
+
return node # unexpected shape — skip
|
|
228
|
+
params = ", ".join(
|
|
229
|
+
f"{ats.get(a.arg, 'object')} {a.arg}" for a in live)
|
|
230
|
+
|
|
231
|
+
ret = "long long" if name in self.int_returns else "object"
|
|
232
|
+
except_clause = " except? -1" if ret == "long long" else ""
|
|
233
|
+
|
|
234
|
+
# textual body with call-site rewriting
|
|
235
|
+
body_src = []
|
|
236
|
+
for stmt in node.body:
|
|
237
|
+
src = ast.unparse(stmt)
|
|
238
|
+
for cand in sorted(self.cands):
|
|
239
|
+
src = re.sub(rf"\b{cand}\(", f"__fast__{cand}(", src)
|
|
240
|
+
if is_helper:
|
|
241
|
+
for param, cand in self.helpers[name].items():
|
|
242
|
+
src = re.sub(rf"\b{param}\(", f"__fast__{cand}(", src)
|
|
243
|
+
body_src.append(" " + src.replace("\n", "\n "))
|
|
244
|
+
header = f"cdef {ret} {fast}({params}){except_clause}:"
|
|
245
|
+
self.added.append(header + "\n" + "\n".join(body_src))
|
|
246
|
+
|
|
247
|
+
# thin wrapper keeps the full original signature
|
|
248
|
+
args = [a.arg for a in node.args.args]
|
|
249
|
+
wrapper_src = f"def {name}({ast.unparse(node.args)}):\n"
|
|
250
|
+
if is_cand:
|
|
251
|
+
call_args = args
|
|
252
|
+
else:
|
|
253
|
+
bound = self.helpers[name]
|
|
254
|
+
call_args = [a for a in args if a not in bound]
|
|
255
|
+
wrapper_src += f" return {fast}({', '.join(call_args)})"
|
|
256
|
+
wrapper = ast.parse(wrapper_src).body[0]
|
|
257
|
+
ast.copy_location(wrapper, node)
|
|
258
|
+
return wrapper
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _result_depends_on_loopvar(fn: ast.FunctionDef, args: list[ast.expr],
|
|
262
|
+
loopvar: str) -> bool:
|
|
263
|
+
"""True if fn(…args…) may return a different value when loopvar
|
|
264
|
+
changes. Tracks parameter bindings and local assignments by name
|
|
265
|
+
expansion; conservative (returns True) on any complex shape."""
|
|
266
|
+
params = [a.arg for a in fn.args.args]
|
|
267
|
+
ndef = len(fn.args.defaults)
|
|
268
|
+
if len(args) != len(params) - ndef:
|
|
269
|
+
return True
|
|
270
|
+
env: dict[str, ast.expr] = dict(zip(params, args))
|
|
271
|
+
|
|
272
|
+
locs: dict[str, ast.expr] = {}
|
|
273
|
+
for stmt in fn.body:
|
|
274
|
+
if not isinstance(stmt, ast.Assign) or len(stmt.targets) != 1:
|
|
275
|
+
continue
|
|
276
|
+
target, value = stmt.targets[0], stmt.value
|
|
277
|
+
# tuple unpack from a bound parameter: i, d = itde
|
|
278
|
+
if (isinstance(target, ast.Tuple) and isinstance(value, ast.Name)
|
|
279
|
+
and value.id in env and isinstance(env[value.id], ast.Tuple)
|
|
280
|
+
and len(target.elts) == len(env[value.id].elts)):
|
|
281
|
+
for t, e in zip(target.elts, env[value.id].elts):
|
|
282
|
+
if not isinstance(t, ast.Name):
|
|
283
|
+
return True
|
|
284
|
+
locs[t.id] = e
|
|
285
|
+
continue
|
|
286
|
+
if isinstance(target, ast.Name):
|
|
287
|
+
locs[target.id] = value
|
|
288
|
+
elif isinstance(target, ast.Tuple):
|
|
289
|
+
if not isinstance(value, ast.Tuple) or len(target.elts) != len(value.elts):
|
|
290
|
+
return True
|
|
291
|
+
for t, e in zip(target.elts, value.elts):
|
|
292
|
+
if not isinstance(t, ast.Name):
|
|
293
|
+
return True
|
|
294
|
+
locs[t.id] = e
|
|
295
|
+
else:
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
def mentions_k(node: ast.expr, seen: frozenset) -> bool:
|
|
299
|
+
for sub in ast.walk(node):
|
|
300
|
+
if isinstance(sub, ast.Name):
|
|
301
|
+
nm = sub.id
|
|
302
|
+
if nm == loopvar:
|
|
303
|
+
return True
|
|
304
|
+
if nm in seen:
|
|
305
|
+
continue
|
|
306
|
+
if nm in locs and mentions_k(
|
|
307
|
+
locs[nm], seen | {nm}):
|
|
308
|
+
return True
|
|
309
|
+
if nm in env and any(
|
|
310
|
+
isinstance(x, ast.Name) and x.id == loopvar
|
|
311
|
+
for x in ast.walk(env[nm])):
|
|
312
|
+
return True
|
|
313
|
+
# env expansion deeper than one level: expand once more
|
|
314
|
+
if nm in env:
|
|
315
|
+
for x in ast.walk(env[nm]):
|
|
316
|
+
if isinstance(x, ast.Name) and x.id in locs \
|
|
317
|
+
and x.id not in seen \
|
|
318
|
+
and mentions_k(locs[x.id], seen | {x.id}):
|
|
319
|
+
return True
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
for stmt in fn.body:
|
|
323
|
+
if isinstance(stmt, ast.Return) and stmt.value is not None:
|
|
324
|
+
if mentions_k(stmt.value, frozenset()):
|
|
325
|
+
return True
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _replace_loopvar_with_const(node: ast.AST, loopvar: str) -> ast.AST:
|
|
330
|
+
"""Replace Name(loopvar) occurrences with Constant(1) — the value is
|
|
331
|
+
provably unused, any constant works."""
|
|
332
|
+
class _R(ast.NodeTransformer):
|
|
333
|
+
def visit_Name(self, n: ast.Name) -> ast.AST:
|
|
334
|
+
if n.id == loopvar:
|
|
335
|
+
return ast.copy_location(ast.Constant(value=1), n)
|
|
336
|
+
return n
|
|
337
|
+
return _R().visit(node)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _candidate_int_returns(funcs: dict[str, ast.FunctionDef],
|
|
341
|
+
cands: list[str]) -> set[str]:
|
|
342
|
+
"""Fixed-point int-return proof over the recursive-pure candidates:
|
|
343
|
+
assume self/mutual candidate calls return int, prove arithmetic
|
|
344
|
+
int-ness of every return expr. check_tree's ``1 + check_tree(l) +
|
|
345
|
+
check_tree(r)`` becomes provable once the assumption is in place."""
|
|
346
|
+
cand_set = set(cands)
|
|
347
|
+
proven: set[str] = set()
|
|
348
|
+
changed = True
|
|
349
|
+
while changed:
|
|
350
|
+
changed = False
|
|
351
|
+
for name in cands:
|
|
352
|
+
if name in proven:
|
|
353
|
+
continue
|
|
354
|
+
fn = funcs[name]
|
|
355
|
+
|
|
356
|
+
def inty(e: ast.expr) -> bool:
|
|
357
|
+
if isinstance(e, ast.Constant) and isinstance(e.value, int):
|
|
358
|
+
return True
|
|
359
|
+
if isinstance(e, ast.Name):
|
|
360
|
+
return False
|
|
361
|
+
if isinstance(e, ast.BinOp):
|
|
362
|
+
return inty(e.left) and inty(e.right)
|
|
363
|
+
if isinstance(e, ast.UnaryOp):
|
|
364
|
+
return (isinstance(e.op, (ast.UAdd, ast.USub, ast.Invert))
|
|
365
|
+
and inty(e.operand))
|
|
366
|
+
if isinstance(e, ast.Call):
|
|
367
|
+
return (isinstance(e.func, ast.Name)
|
|
368
|
+
and (e.func.id in proven
|
|
369
|
+
or e.func.id == name))
|
|
370
|
+
if isinstance(e, ast.IfExp):
|
|
371
|
+
return inty(e.body) and inty(e.orelse)
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
rets = [s.value for s in fn.body
|
|
375
|
+
if isinstance(s, ast.Return) and s.value is not None]
|
|
376
|
+
if not rets:
|
|
377
|
+
continue
|
|
378
|
+
if all(inty(r) for r in rets):
|
|
379
|
+
proven.add(name)
|
|
380
|
+
changed = True
|
|
381
|
+
return proven
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _helper_int_return(fn: ast.FunctionDef, bound: dict[str, str],
|
|
385
|
+
int_returns: set[str]) -> bool:
|
|
386
|
+
"""A helper provably returns int when every return expr is a call
|
|
387
|
+
to an int-returning candidate (possibly via cand-bound param names)
|
|
388
|
+
or int arithmetic over such calls."""
|
|
389
|
+
names_ok = set(int_returns) | set(bound.keys())
|
|
390
|
+
|
|
391
|
+
def inty(e: ast.expr) -> bool:
|
|
392
|
+
if isinstance(e, ast.Constant) and isinstance(e.value, int):
|
|
393
|
+
return True
|
|
394
|
+
if isinstance(e, ast.Call) and isinstance(e.func, ast.Name):
|
|
395
|
+
return e.func.id in names_ok
|
|
396
|
+
if isinstance(e, ast.BinOp):
|
|
397
|
+
return inty(e.left) and inty(e.right)
|
|
398
|
+
if isinstance(e, ast.UnaryOp):
|
|
399
|
+
return inty(e.operand)
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
rets = [s.value for s in fn.body
|
|
403
|
+
if isinstance(s, ast.Return) and s.value is not None]
|
|
404
|
+
return bool(rets) and all(inty(r) for r in rets)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _sum_licm_fold(tree: ast.Module, lowerable: dict[str, ast.FunctionDef],
|
|
408
|
+
int_returns: set[str]) -> list[str]:
|
|
409
|
+
"""Fold sum(F(...) for k in range(lo, hi)) → (hi-lo) * __fast__F(...)
|
|
410
|
+
when F is lowered, int-returning, and loop-invariant in result."""
|
|
411
|
+
folded = []
|
|
412
|
+
|
|
413
|
+
class _Fold(ast.NodeTransformer):
|
|
414
|
+
def visit_Assign(self, node: ast.Assign):
|
|
415
|
+
self.generic_visit(node)
|
|
416
|
+
if not (len(node.targets) == 1
|
|
417
|
+
and isinstance(node.targets[0], ast.Name)):
|
|
418
|
+
return node
|
|
419
|
+
val = node.value
|
|
420
|
+
if not (isinstance(val, ast.Call)
|
|
421
|
+
and isinstance(val.func, ast.Name)
|
|
422
|
+
and val.func.id == "sum"
|
|
423
|
+
and len(val.args) == 1
|
|
424
|
+
and isinstance(val.args[0], ast.GeneratorExp)):
|
|
425
|
+
return node
|
|
426
|
+
gen = val.args[0]
|
|
427
|
+
if len(gen.generators) != 1:
|
|
428
|
+
return node
|
|
429
|
+
g = gen.generators[0]
|
|
430
|
+
if g.ifs or g.is_async:
|
|
431
|
+
return node
|
|
432
|
+
tgt = g.target
|
|
433
|
+
if not isinstance(tgt, ast.Name):
|
|
434
|
+
return node
|
|
435
|
+
rng = g.iter
|
|
436
|
+
if not (isinstance(rng, ast.Call)
|
|
437
|
+
and isinstance(rng.func, ast.Name)
|
|
438
|
+
and rng.func.id == "range" and len(rng.args) == 2
|
|
439
|
+
and not rng.keywords):
|
|
440
|
+
return node
|
|
441
|
+
lo, hi = rng.args
|
|
442
|
+
elt = gen.elt
|
|
443
|
+
if not (isinstance(elt, ast.Call)
|
|
444
|
+
and isinstance(elt.func, ast.Name)
|
|
445
|
+
and elt.func.id in lowerable
|
|
446
|
+
and elt.func.id in int_returns
|
|
447
|
+
and not elt.keywords):
|
|
448
|
+
return node
|
|
449
|
+
fname = elt.func.id
|
|
450
|
+
fn = lowerable[fname]
|
|
451
|
+
if _result_depends_on_loopvar(fn, elt.args, tgt.id):
|
|
452
|
+
return node
|
|
453
|
+
folded.append(fname)
|
|
454
|
+
count = ast.BinOp(left=copy.deepcopy(hi), op=ast.Sub(),
|
|
455
|
+
right=copy.deepcopy(lo))
|
|
456
|
+
once = copy.deepcopy(elt)
|
|
457
|
+
once.func = ast.copy_location(
|
|
458
|
+
ast.Name(id=f"__fast__{fname}", ctx=ast.Load()),
|
|
459
|
+
elt.func)
|
|
460
|
+
once.args = [_replace_loopvar_with_const(a, tgt.id)
|
|
461
|
+
for a in once.args]
|
|
462
|
+
ast.fix_missing_locations(once)
|
|
463
|
+
node.value = ast.BinOp(left=count, op=ast.Mult(), right=once)
|
|
464
|
+
return node
|
|
465
|
+
|
|
466
|
+
_Fold().visit(tree)
|
|
467
|
+
return folded
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# cdef blocks are not valid Python syntax, so they cannot be inserted as
|
|
471
|
+
# AST nodes. Pass 9 plants ``__pf_cdef_N = 0`` marker assignments in the
|
|
472
|
+
# tree and registers the real cdef source here; the engine swaps them
|
|
473
|
+
# textually after ast.unparse (same mechanism as Pass 8's markers).
|
|
474
|
+
PENDING_CDEFS: dict[str, str] = {}
|
|
475
|
+
_marker_ctr = [0]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def apply_recursive_passes(tree: ast.Module, arg_types: dict,
|
|
479
|
+
int_returns: set[str]) -> list[str]:
|
|
480
|
+
"""Entry: run Pass 9 then Pass 10 on the module. Returns the list
|
|
481
|
+
of applied strategy tags."""
|
|
482
|
+
PENDING_CDEFS.clear()
|
|
483
|
+
applied = []
|
|
484
|
+
cands = _recursive_pure_candidates(tree)
|
|
485
|
+
helpers = _pure_leaf_helpers(tree, cands) if cands else {}
|
|
486
|
+
# snapshot ORIGINAL function defs (pre-lowering) for Pass 10's
|
|
487
|
+
# dependency analysis — wrappers are useless for that.
|
|
488
|
+
orig_funcs = _module_funcs(tree)
|
|
489
|
+
# recursion-aware int-return proof (solver cannot see through
|
|
490
|
+
# self-calls): fixed point over the candidates, then helpers.
|
|
491
|
+
int_returns = (set(int_returns)
|
|
492
|
+
| _candidate_int_returns(orig_funcs, cands))
|
|
493
|
+
# helpers inherit int-return proof transitively from candidates
|
|
494
|
+
for hname, bound in helpers.items():
|
|
495
|
+
if _helper_int_return(orig_funcs[hname], bound,
|
|
496
|
+
int_returns):
|
|
497
|
+
int_returns = int_returns | {hname}
|
|
498
|
+
if cands or helpers:
|
|
499
|
+
lower = _CdefRecLower(cands, helpers, arg_types, int_returns)
|
|
500
|
+
tree.body = [lower.visit(n) for n in tree.body]
|
|
501
|
+
insert_at = next((i for i, n in enumerate(tree.body)
|
|
502
|
+
if isinstance(n, ast.FunctionDef)), 0)
|
|
503
|
+
for cdef_src in lower.added:
|
|
504
|
+
marker = f"__pf_cdef_{_marker_ctr[0]}"
|
|
505
|
+
_marker_ctr[0] += 1
|
|
506
|
+
PENDING_CDEFS[marker] = cdef_src
|
|
507
|
+
placeholder = ast.Assign(
|
|
508
|
+
targets=[ast.Name(id=marker, ctx=ast.Store())],
|
|
509
|
+
value=ast.Constant(value=0))
|
|
510
|
+
tree.body.insert(insert_at, placeholder)
|
|
511
|
+
insert_at += 1
|
|
512
|
+
ast.fix_missing_locations(tree)
|
|
513
|
+
applied.append("cdef-rec")
|
|
514
|
+
|
|
515
|
+
lowerable = {n: f for n, f in orig_funcs.items()
|
|
516
|
+
if n in set(cands) | set(helpers)}
|
|
517
|
+
if lowerable:
|
|
518
|
+
folded = _sum_licm_fold(tree, lowerable, int_returns)
|
|
519
|
+
if folded:
|
|
520
|
+
ast.fix_missing_locations(tree)
|
|
521
|
+
applied.append("sum-licm")
|
|
522
|
+
return applied
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import pythonfaster._bootstrap
|