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.
@@ -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