opast 0.1.0__tar.gz

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.
Files changed (56) hide show
  1. opast-0.1.0/LICENSE +21 -0
  2. opast-0.1.0/PKG-INFO +167 -0
  3. opast-0.1.0/README.md +140 -0
  4. opast-0.1.0/pyproject.toml +42 -0
  5. opast-0.1.0/setup.cfg +4 -0
  6. opast-0.1.0/src/opast/__init__.py +60 -0
  7. opast-0.1.0/src/opast/__main__.py +136 -0
  8. opast-0.1.0/src/opast/analysis.py +608 -0
  9. opast-0.1.0/src/opast/bench/__init__.py +12 -0
  10. opast-0.1.0/src/opast/bench/__main__.py +162 -0
  11. opast-0.1.0/src/opast/bench/workloads/__init__.py +1 -0
  12. opast-0.1.0/src/opast/bench/workloads/algebra.py +16 -0
  13. opast-0.1.0/src/opast/bench/workloads/comptomap.py +14 -0
  14. opast-0.1.0/src/opast/bench/workloads/control.py +14 -0
  15. opast-0.1.0/src/opast/bench/workloads/daily.py +78 -0
  16. opast-0.1.0/src/opast/bench/workloads/dedynamize.py +12 -0
  17. opast-0.1.0/src/opast/bench/workloads/inline.py +23 -0
  18. opast-0.1.0/src/opast/bench/workloads/inlinestmt.py +22 -0
  19. opast-0.1.0/src/opast/bench/workloads/jitlazy.py +20 -0
  20. opast-0.1.0/src/opast/bench/workloads/lencache.py +19 -0
  21. opast-0.1.0/src/opast/bench/workloads/licm.py +22 -0
  22. opast-0.1.0/src/opast/bench/workloads/localize.py +28 -0
  23. opast-0.1.0/src/opast/bench/workloads/loopfold.py +25 -0
  24. opast-0.1.0/src/opast/bench/workloads/looptocomp.py +17 -0
  25. opast-0.1.0/src/opast/bench/workloads/mixed.py +25 -0
  26. opast-0.1.0/src/opast/bench/workloads/rangeiter.py +17 -0
  27. opast-0.1.0/src/opast/bench/workloads/strength.py +15 -0
  28. opast-0.1.0/src/opast/importhook.py +198 -0
  29. opast-0.1.0/src/opast/jitsupport.py +372 -0
  30. opast-0.1.0/src/opast/magic.py +157 -0
  31. opast-0.1.0/src/opast/passes/__init__.py +35 -0
  32. opast-0.1.0/src/opast/passes/algebra.py +222 -0
  33. opast-0.1.0/src/opast/passes/base.py +40 -0
  34. opast-0.1.0/src/opast/passes/comprehension.py +281 -0
  35. opast-0.1.0/src/opast/passes/cse.py +281 -0
  36. opast-0.1.0/src/opast/passes/deadcode.py +167 -0
  37. opast-0.1.0/src/opast/passes/dedynamize.py +266 -0
  38. opast-0.1.0/src/opast/passes/folding.py +227 -0
  39. opast-0.1.0/src/opast/passes/inline.py +454 -0
  40. opast-0.1.0/src/opast/passes/jit.py +929 -0
  41. opast-0.1.0/src/opast/passes/licm.py +253 -0
  42. opast-0.1.0/src/opast/passes/localize.py +263 -0
  43. opast-0.1.0/src/opast/passes/loopfold.py +358 -0
  44. opast-0.1.0/src/opast/passes/looptocomp.py +274 -0
  45. opast-0.1.0/src/opast/passes/propagate.py +491 -0
  46. opast-0.1.0/src/opast/passes/rangeiter.py +269 -0
  47. opast-0.1.0/src/opast/passes/unused.py +221 -0
  48. opast-0.1.0/src/opast/pipeline.py +198 -0
  49. opast-0.1.0/src/opast/runner.py +125 -0
  50. opast-0.1.0/src/opast/safety.py +140 -0
  51. opast-0.1.0/src/opast.egg-info/PKG-INFO +167 -0
  52. opast-0.1.0/src/opast.egg-info/SOURCES.txt +54 -0
  53. opast-0.1.0/src/opast.egg-info/dependency_links.txt +1 -0
  54. opast-0.1.0/src/opast.egg-info/entry_points.txt +2 -0
  55. opast-0.1.0/src/opast.egg-info/requires.txt +3 -0
  56. opast-0.1.0/src/opast.egg-info/top_level.txt +1 -0
opast-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YZJYBT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
opast-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: opast
3
+ Version: 0.1.0
4
+ Summary: Conservative AST-level Python source optimizer: provably-semantics-preserving passes (folding, propagation, inlining, loop rewrites) with dynamic-code fallback, plus an opt-in numba JIT path.
5
+ Author-email: YZJYBT <yzj0xfff2@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/YZJYBT/pyOpAst
8
+ Project-URL: Issues, https://github.com/YZJYBT/pyOpAst/issues
9
+ Project-URL: Changelog, https://github.com/YZJYBT/pyOpAst/blob/main/CHANGELOG.md
10
+ Keywords: optimizer,ast,performance,source-to-source,numba,jit
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Compilers
20
+ Classifier: Topic :: Software Development :: Code Generators
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: jit
25
+ Requires-Dist: numba>=0.61; extra == "jit"
26
+ Dynamic: license-file
27
+
28
+ # opast
29
+
30
+ > **English** | [中文](README-ZH.md) (the Chinese document carries the exhaustive per-pass safety conditions; this one is the canonical overview)
31
+
32
+ ![CI](https://github.com/YZJYBT/pyOpAst/actions/workflows/ci.yml/badge.svg)
33
+
34
+ **opast** ("OPtimizing AST") is a conservative AST-level source optimizer for Python. It rewrites your script into an equivalent but faster one and runs it on the very interpreter that invoked opast (plain CPython by default). Every transformation is backed by a static proof of semantic preservation; anything the optimizer cannot prove, it leaves alone.
35
+
36
+ - PyPI / import package / CLI command: `opast` — GitHub repository: **pyOpAst**
37
+ - Requires Python ≥ 3.10. Zero runtime dependencies; `numba` only for the opt-in `--jit` extra.
38
+
39
+ ```powershell
40
+ pip install opast # or from source: pip install -e .
41
+
42
+ opast script.py a b c # optimize, then run (same as python -m opast)
43
+ opast --show --no-run script.py # print the optimized source only
44
+ opast --report script.py # run + per-pass statistics on stderr
45
+ opast -c "print(sum(i for i in range(10)))" # inline code string, like python -c
46
+ opast --disable inline,licm script.py # skip passes by name
47
+ ```
48
+
49
+ ## Design
50
+
51
+ Three principles drive every pass:
52
+
53
+ 1. **Prove, then rewrite.** Passes only fire on facts established by static analysis: per-function *proven-int* type inference (a greatest-fixpoint over all bindings), interval (value-range) analysis with widening, escape analysis for *fresh containers* (built locally, never leaked, never mutated), straight-line dominance scans for definite binding, and module-wide stability checks for names (bound exactly once, never `global`-declared, no dynamic constructs anywhere).
54
+ 2. **Dynamic code disables optimization, scope by scope.** Any appearance of `eval` / `exec` / `globals` / `locals` / `vars` / `compile` / `__import__` / frame-introspection attributes / `from m import *` taints the enclosing scope; tainted scopes (and everything nested inside) are skipped entirely. A tainted module top level disables the whole file. The check is name-based and deliberately over-conservative.
55
+ 3. **Zero runtime overhead.** The static passes emit ordinary Python source — no guards, no helper runtime. The only runtime machinery lives behind the opt-in `--jit` flag, and it degrades to plain Python on any failure.
56
+
57
+ ## Optimization passes
58
+
59
+ The pipeline iterates to a fixpoint (default ≤ 8 rounds): de-dynamize → folding → constant propagation → algebraic → loop-fold → dead code → range-to-iter → LICM → CSE → unused → inline → loop-to-comp → comp-to-map → localize. Each pass feeds the next; collapsed constants cascade outward across iterations. See [README-ZH.md](README-ZH.md) for the full safety-condition spec of each pass.
60
+
61
+ | Pass | What it does |
62
+ | --- | --- |
63
+ | `de-dynamize` | Expands *pointless* dynamic code into static equivalents: top-level `eval("<const expr>")` → the expression, `exec("<const stmts>")` → the statements, `globals()['x'] = v` → `x = v`, `getattr(o, 'a')` → `o.a`, statement-form `setattr`/`delattr` → attribute syntax. All-or-nothing: commits only if the whole module ends up free of dynamic constructs, otherwise rolls back — un-tainting the module unlocks every other pass. |
64
+ | `constant-folding` | Folds constant arithmetic / strings / comparisons / boolean short-circuits / subscripts. Expressions that would raise at runtime are preserved; guardrails cap optimization-time work and literal sizes, with partial sub-expression folding allowed. |
65
+ | `const-prop` | Substitutes constants for variables along four routes: single-binding names (whole scope), **cross-scope** module constants into later-defined functions, **span propagation** for multiply-bound names (from an assignment up to the first statement that could rebind), and **copy propagation** `y = x` between plain locals (function scopes only). |
66
+ | `algebraic` | Identity cleanup (`x+0`, `x*1`, `-(-x)`, …) plus strength reduction — `E % 2**k → E & mask`, `E // 2**k → E >> k`, `E ** 2 → E * E` — and interval-analysis-backed `abs(E) → E` for provably non-negative `E`. Only on provably plain-`int` expressions; never duplicates or drops effects. |
67
+ | `loop-fold` | **Closed-form loop evaluation**: a `for i in range(<const>)` loop whose body is pure int arithmetic (no calls) is simulated exactly at optimization time and replaced by its final constant assignments. Step and magnitude budgets; any simulated exception keeps the loop. A successful fold proves the loop cannot raise, so it is legal even inside `try`/`with`. |
68
+ | `dead-code` | Unreachable statements after `return`/`raise`/`break`/`continue`, constant-condition `if`/`while` (with `else` semantics), `assert True`, useless constant expression statements, redundant `pass`. |
69
+ | `range-to-iter` | `for i in range(len(x))` over a provably fresh *sequence* becomes `for v in x` (index dead) or `for i, v in enumerate(x)` (index live); per-iteration `BINARY_SUBSCR` lookups disappear. Only exact `x[i]` loads are replaced; the enumerate form keeps `x` and `i` bound, so leftovers stay correct. |
70
+ | `licm` | Hoists provably pure-and-total loop-invariant int expressions (and `len()` of fresh containers) out of loop bodies and `while` tests into pre-loop temporaries, with dominance-checked definite binding. |
71
+ | `cse` | Merges repeated provably pure int expressions (same criteria as LICM) within a statement block into a temporary; speculative evaluation is sound because the expressions cannot raise. |
72
+ | `unused` | Removes unused imports, unused module-level functions (post-inlining helpers), and dead local stores (effectful right-hand sides are downgraded to bare expressions). Module-level variable assignments are deliberately kept — module globals are observable API. |
73
+ | `inline` | Two shapes: expression-body functions (`def f(...): return <expr>`) inline at any call site with constant/name arguments; straight-line statement bodies (≤ 6 assignments + `return`) inline at statement positions with arbitrary positional arguments via ordered temporaries. Requires module-wide name stability; the leftover `def` is cleaned by `unused`. |
74
+ | `loop-to-comp` | `x = []` + an adjacent append-accumulation loop (nested `for`/guard-`if` chains allowed) becomes a list comprehension (`set()`/`.add` → set comprehension): dedicated `LIST_APPEND`/`SET_ADD` bytecode instead of a per-iteration attribute lookup + method call. Rejected inside `try`/`with` (a mid-loop exception would expose the partially-built list). |
75
+ | `comp-to-map` | `(f(x) for x in it)` → `map(f, it)`, with `filter` for guards, restricted to positions where the generator/`map` identity difference is unobservable and `f` is provably stable. |
76
+ | `localize` | Per-iteration reads of stable globals/builtins inside loops become pre-loop locals (`LOAD_GLOBAL` → `LOAD_FAST`). Runs last so other passes claim names first. |
77
+
78
+ LICM/CSE additionally support **`len()` caching for fresh containers** — sound only because escape analysis guarantees no reference ever leaves the function, so no call can mutate the container.
79
+
80
+ ## Importing modules through the optimizer (`--opt-imports`)
81
+
82
+ ```powershell
83
+ opast --opt-imports script.py # also optimize modules imported from the script's directory
84
+ opast --opt-imports-under src script.py # add extra roots (repeatable)
85
+ ```
86
+
87
+ A `sys.meta_path` hook runs imported modules through the full pipeline before compilation. Optimized bytecode never touches `__pycache__` (a later plain `python` run must not pick it up); results go to a content-addressed private cache instead (`OPAST_NO_IMPORT_CACHE=1` bypasses). Semantics boundary: modules whose globals get monkeypatched from outside (e.g. `unittest.mock.patch`) silently lose the stability assumptions several passes rely on — do not enable this for such modules, which is why it is opt-in. `unused` is force-disabled for imported modules (their "unused" definitions are the export surface). Python API: `from opast.importhook import install, uninstall`.
88
+
89
+ ## Benchmarks
90
+
91
+ ```powershell
92
+ python -m opast.bench # all built-in workloads, best-of-3, results verified identical
93
+ python -m opast.bench --jit daily
94
+ python -m opast.bench --list
95
+ ```
96
+
97
+ 16 built-in workloads, each executed twice per measurement (original vs optimized) in the same interpreter with GC disabled and a `RESULT` equality check. Note that CPython's own compiler already does trivial constant folding — opast's wins come from what CPython does *not* do: inlining, type-proven algebraic rewrites, loop rewrites, de-dynamization.
98
+
99
+ ## IPython / Jupyter
100
+
101
+ ```
102
+ %load_ext opast
103
+
104
+ %%opast --report --disable licm
105
+ total = 0
106
+ for i in range(50_000):
107
+ total += i * 2
108
+ total
109
+ ```
110
+
111
+ Options mirror the CLI; the cell executes in the user namespace, so assignments persist. Analyses are cell-scoped — see README-ZH for the notebook caveats.
112
+
113
+ ## Experimental: `--jit` (numba, off by default)
114
+
115
+ ```powershell
116
+ pip install opast[jit]
117
+ opast --jit hot_script.py
118
+ ```
119
+
120
+ After the static fixpoint, a one-shot pass decorates hot numeric functions with a guarded `numba.njit` wrapper. Measured on CPython 3.14 (8M-iteration numeric kernel): 1.22 s pure Python vs 0.011 s steady-state (~110×), first call 0.79 s including compilation.
121
+
122
+ - **Static hotness** (constant loop bounds ≥ 10 000 or nested loops) compiles at decoration time; a strict whitelist predicts numba compatibility (int/float arithmetic, `range` loops, `math.*`, no containers/strings/attributes).
123
+ - **Loop outlining** extracts hot whitelisted loops out of mixed functions and module top level into fresh compiled functions, with proven input/output sets.
124
+ - **njit inter-calls**: candidate functions may call each other (fixpoint selection, call cycles dropped, compiled copies call raw dispatchers).
125
+ - **Runtime lazy compilation** covers *variable* loop bounds (`for i in range(n)`): the wrapper observes plain-Python calls and compiles when a trigger fires — bound argument ≥ `OPAST_JIT_LAZY_BOUND` (default 10 000), a single call ≥ 0.1 s, or ≥ 10 calls totalling ≥ 0.3 s. The triggering call's Python result doubles as the verification expectation, and `numba` itself is imported only on the first compilation attempt — a script whose lazy candidates never get hot pays nothing.
126
+ - **First-call verification**: whitelisted functions are pure, so the first call runs both versions and compares results; a divergence (in practice: int64 wraparound, which no static filter can rule out) triggers a permanent fallback to Python instead of silently wrong answers. `OPAST_JIT_NO_VERIFY=1` opts out.
127
+ - **Layered degradation**: no numba / incompatible interpreter / `OPAST_DISABLE_JIT` → original function; any numba error at compile or call time → permanent Python fallback. `OPAST_JIT_DEBUG=1` explains every fallback and lazy trigger on stderr.
128
+
129
+ ⚠️ Opt-in semantic caveat: numba integers are fixed 64-bit — intermediate values beyond ±9.2e18 wrap silently. This is why `--jit` is not on by default and not part of the semantic-preservation contract; the verification above is a safety net, not a proof.
130
+
131
+ ## Python API
132
+
133
+ ```python
134
+ from opast import optimize_source, optimize_file, run_path, run_source, PASS_NAMES
135
+
136
+ result = optimize_file("script.py")
137
+ print(result.source) # optimized source (ast.unparse)
138
+ print(result.report.summary()) # per-pass statistics
139
+ run_path("script.py", argv=("--flag",))
140
+
141
+ optimize_file("script.py", disable="inline,licm") # every entry point accepts `disable`
142
+ ```
143
+
144
+ ## Tests
145
+
146
+ ```powershell
147
+ python tests/verify_opast.py # full acceptance suite: per-pass behavior assertions
148
+ # + original-vs-optimized output comparison
149
+ ```
150
+
151
+ Case sources are generated into `tests/cases/` on demand (not tracked); the bench harness doubles as a correctness check.
152
+
153
+ ## Prior art
154
+
155
+ - [pyastop](https://github.com/xiaonanln/pyastop) (2017–2018): an early AST-optimizer prototype built around whole-project analysis and comment hints. opast instead proves each rewrite safe per pass, with no user annotations.
156
+ - [fatoptimizer](https://github.com/vstinner/fatoptimizer) (FAT Python, PEPs 509/510/511): runtime guards + function specialization, abandoned over guard overhead. opast's static passes have zero runtime overhead; runtime machinery exists only behind the opt-in `--jit`.
157
+ - CPython's built-in AST/peephole optimizer folds constants only; opast's gains come from everything beyond that.
158
+
159
+ ## Known limitations
160
+
161
+ - Optimized code is executed via `compile(optimized_ast, original_filename)`; traceback line numbers reuse original positions where possible but may drift slightly on rewritten lines. Inlining removes call frames from tracebacks (documented per pass).
162
+ - All transformations require static proof; anything unprovable is left untouched. Documented edge observables (e.g. the partially-built-list window excluded via `try`/`with` checks) are listed per pass in [README-ZH.md](README-ZH.md).
163
+ - Python ≥ 3.10.
164
+
165
+ ## License
166
+
167
+ MIT
opast-0.1.0/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # opast
2
+
3
+ > **English** | [中文](README-ZH.md) (the Chinese document carries the exhaustive per-pass safety conditions; this one is the canonical overview)
4
+
5
+ ![CI](https://github.com/YZJYBT/pyOpAst/actions/workflows/ci.yml/badge.svg)
6
+
7
+ **opast** ("OPtimizing AST") is a conservative AST-level source optimizer for Python. It rewrites your script into an equivalent but faster one and runs it on the very interpreter that invoked opast (plain CPython by default). Every transformation is backed by a static proof of semantic preservation; anything the optimizer cannot prove, it leaves alone.
8
+
9
+ - PyPI / import package / CLI command: `opast` — GitHub repository: **pyOpAst**
10
+ - Requires Python ≥ 3.10. Zero runtime dependencies; `numba` only for the opt-in `--jit` extra.
11
+
12
+ ```powershell
13
+ pip install opast # or from source: pip install -e .
14
+
15
+ opast script.py a b c # optimize, then run (same as python -m opast)
16
+ opast --show --no-run script.py # print the optimized source only
17
+ opast --report script.py # run + per-pass statistics on stderr
18
+ opast -c "print(sum(i for i in range(10)))" # inline code string, like python -c
19
+ opast --disable inline,licm script.py # skip passes by name
20
+ ```
21
+
22
+ ## Design
23
+
24
+ Three principles drive every pass:
25
+
26
+ 1. **Prove, then rewrite.** Passes only fire on facts established by static analysis: per-function *proven-int* type inference (a greatest-fixpoint over all bindings), interval (value-range) analysis with widening, escape analysis for *fresh containers* (built locally, never leaked, never mutated), straight-line dominance scans for definite binding, and module-wide stability checks for names (bound exactly once, never `global`-declared, no dynamic constructs anywhere).
27
+ 2. **Dynamic code disables optimization, scope by scope.** Any appearance of `eval` / `exec` / `globals` / `locals` / `vars` / `compile` / `__import__` / frame-introspection attributes / `from m import *` taints the enclosing scope; tainted scopes (and everything nested inside) are skipped entirely. A tainted module top level disables the whole file. The check is name-based and deliberately over-conservative.
28
+ 3. **Zero runtime overhead.** The static passes emit ordinary Python source — no guards, no helper runtime. The only runtime machinery lives behind the opt-in `--jit` flag, and it degrades to plain Python on any failure.
29
+
30
+ ## Optimization passes
31
+
32
+ The pipeline iterates to a fixpoint (default ≤ 8 rounds): de-dynamize → folding → constant propagation → algebraic → loop-fold → dead code → range-to-iter → LICM → CSE → unused → inline → loop-to-comp → comp-to-map → localize. Each pass feeds the next; collapsed constants cascade outward across iterations. See [README-ZH.md](README-ZH.md) for the full safety-condition spec of each pass.
33
+
34
+ | Pass | What it does |
35
+ | --- | --- |
36
+ | `de-dynamize` | Expands *pointless* dynamic code into static equivalents: top-level `eval("<const expr>")` → the expression, `exec("<const stmts>")` → the statements, `globals()['x'] = v` → `x = v`, `getattr(o, 'a')` → `o.a`, statement-form `setattr`/`delattr` → attribute syntax. All-or-nothing: commits only if the whole module ends up free of dynamic constructs, otherwise rolls back — un-tainting the module unlocks every other pass. |
37
+ | `constant-folding` | Folds constant arithmetic / strings / comparisons / boolean short-circuits / subscripts. Expressions that would raise at runtime are preserved; guardrails cap optimization-time work and literal sizes, with partial sub-expression folding allowed. |
38
+ | `const-prop` | Substitutes constants for variables along four routes: single-binding names (whole scope), **cross-scope** module constants into later-defined functions, **span propagation** for multiply-bound names (from an assignment up to the first statement that could rebind), and **copy propagation** `y = x` between plain locals (function scopes only). |
39
+ | `algebraic` | Identity cleanup (`x+0`, `x*1`, `-(-x)`, …) plus strength reduction — `E % 2**k → E & mask`, `E // 2**k → E >> k`, `E ** 2 → E * E` — and interval-analysis-backed `abs(E) → E` for provably non-negative `E`. Only on provably plain-`int` expressions; never duplicates or drops effects. |
40
+ | `loop-fold` | **Closed-form loop evaluation**: a `for i in range(<const>)` loop whose body is pure int arithmetic (no calls) is simulated exactly at optimization time and replaced by its final constant assignments. Step and magnitude budgets; any simulated exception keeps the loop. A successful fold proves the loop cannot raise, so it is legal even inside `try`/`with`. |
41
+ | `dead-code` | Unreachable statements after `return`/`raise`/`break`/`continue`, constant-condition `if`/`while` (with `else` semantics), `assert True`, useless constant expression statements, redundant `pass`. |
42
+ | `range-to-iter` | `for i in range(len(x))` over a provably fresh *sequence* becomes `for v in x` (index dead) or `for i, v in enumerate(x)` (index live); per-iteration `BINARY_SUBSCR` lookups disappear. Only exact `x[i]` loads are replaced; the enumerate form keeps `x` and `i` bound, so leftovers stay correct. |
43
+ | `licm` | Hoists provably pure-and-total loop-invariant int expressions (and `len()` of fresh containers) out of loop bodies and `while` tests into pre-loop temporaries, with dominance-checked definite binding. |
44
+ | `cse` | Merges repeated provably pure int expressions (same criteria as LICM) within a statement block into a temporary; speculative evaluation is sound because the expressions cannot raise. |
45
+ | `unused` | Removes unused imports, unused module-level functions (post-inlining helpers), and dead local stores (effectful right-hand sides are downgraded to bare expressions). Module-level variable assignments are deliberately kept — module globals are observable API. |
46
+ | `inline` | Two shapes: expression-body functions (`def f(...): return <expr>`) inline at any call site with constant/name arguments; straight-line statement bodies (≤ 6 assignments + `return`) inline at statement positions with arbitrary positional arguments via ordered temporaries. Requires module-wide name stability; the leftover `def` is cleaned by `unused`. |
47
+ | `loop-to-comp` | `x = []` + an adjacent append-accumulation loop (nested `for`/guard-`if` chains allowed) becomes a list comprehension (`set()`/`.add` → set comprehension): dedicated `LIST_APPEND`/`SET_ADD` bytecode instead of a per-iteration attribute lookup + method call. Rejected inside `try`/`with` (a mid-loop exception would expose the partially-built list). |
48
+ | `comp-to-map` | `(f(x) for x in it)` → `map(f, it)`, with `filter` for guards, restricted to positions where the generator/`map` identity difference is unobservable and `f` is provably stable. |
49
+ | `localize` | Per-iteration reads of stable globals/builtins inside loops become pre-loop locals (`LOAD_GLOBAL` → `LOAD_FAST`). Runs last so other passes claim names first. |
50
+
51
+ LICM/CSE additionally support **`len()` caching for fresh containers** — sound only because escape analysis guarantees no reference ever leaves the function, so no call can mutate the container.
52
+
53
+ ## Importing modules through the optimizer (`--opt-imports`)
54
+
55
+ ```powershell
56
+ opast --opt-imports script.py # also optimize modules imported from the script's directory
57
+ opast --opt-imports-under src script.py # add extra roots (repeatable)
58
+ ```
59
+
60
+ A `sys.meta_path` hook runs imported modules through the full pipeline before compilation. Optimized bytecode never touches `__pycache__` (a later plain `python` run must not pick it up); results go to a content-addressed private cache instead (`OPAST_NO_IMPORT_CACHE=1` bypasses). Semantics boundary: modules whose globals get monkeypatched from outside (e.g. `unittest.mock.patch`) silently lose the stability assumptions several passes rely on — do not enable this for such modules, which is why it is opt-in. `unused` is force-disabled for imported modules (their "unused" definitions are the export surface). Python API: `from opast.importhook import install, uninstall`.
61
+
62
+ ## Benchmarks
63
+
64
+ ```powershell
65
+ python -m opast.bench # all built-in workloads, best-of-3, results verified identical
66
+ python -m opast.bench --jit daily
67
+ python -m opast.bench --list
68
+ ```
69
+
70
+ 16 built-in workloads, each executed twice per measurement (original vs optimized) in the same interpreter with GC disabled and a `RESULT` equality check. Note that CPython's own compiler already does trivial constant folding — opast's wins come from what CPython does *not* do: inlining, type-proven algebraic rewrites, loop rewrites, de-dynamization.
71
+
72
+ ## IPython / Jupyter
73
+
74
+ ```
75
+ %load_ext opast
76
+
77
+ %%opast --report --disable licm
78
+ total = 0
79
+ for i in range(50_000):
80
+ total += i * 2
81
+ total
82
+ ```
83
+
84
+ Options mirror the CLI; the cell executes in the user namespace, so assignments persist. Analyses are cell-scoped — see README-ZH for the notebook caveats.
85
+
86
+ ## Experimental: `--jit` (numba, off by default)
87
+
88
+ ```powershell
89
+ pip install opast[jit]
90
+ opast --jit hot_script.py
91
+ ```
92
+
93
+ After the static fixpoint, a one-shot pass decorates hot numeric functions with a guarded `numba.njit` wrapper. Measured on CPython 3.14 (8M-iteration numeric kernel): 1.22 s pure Python vs 0.011 s steady-state (~110×), first call 0.79 s including compilation.
94
+
95
+ - **Static hotness** (constant loop bounds ≥ 10 000 or nested loops) compiles at decoration time; a strict whitelist predicts numba compatibility (int/float arithmetic, `range` loops, `math.*`, no containers/strings/attributes).
96
+ - **Loop outlining** extracts hot whitelisted loops out of mixed functions and module top level into fresh compiled functions, with proven input/output sets.
97
+ - **njit inter-calls**: candidate functions may call each other (fixpoint selection, call cycles dropped, compiled copies call raw dispatchers).
98
+ - **Runtime lazy compilation** covers *variable* loop bounds (`for i in range(n)`): the wrapper observes plain-Python calls and compiles when a trigger fires — bound argument ≥ `OPAST_JIT_LAZY_BOUND` (default 10 000), a single call ≥ 0.1 s, or ≥ 10 calls totalling ≥ 0.3 s. The triggering call's Python result doubles as the verification expectation, and `numba` itself is imported only on the first compilation attempt — a script whose lazy candidates never get hot pays nothing.
99
+ - **First-call verification**: whitelisted functions are pure, so the first call runs both versions and compares results; a divergence (in practice: int64 wraparound, which no static filter can rule out) triggers a permanent fallback to Python instead of silently wrong answers. `OPAST_JIT_NO_VERIFY=1` opts out.
100
+ - **Layered degradation**: no numba / incompatible interpreter / `OPAST_DISABLE_JIT` → original function; any numba error at compile or call time → permanent Python fallback. `OPAST_JIT_DEBUG=1` explains every fallback and lazy trigger on stderr.
101
+
102
+ ⚠️ Opt-in semantic caveat: numba integers are fixed 64-bit — intermediate values beyond ±9.2e18 wrap silently. This is why `--jit` is not on by default and not part of the semantic-preservation contract; the verification above is a safety net, not a proof.
103
+
104
+ ## Python API
105
+
106
+ ```python
107
+ from opast import optimize_source, optimize_file, run_path, run_source, PASS_NAMES
108
+
109
+ result = optimize_file("script.py")
110
+ print(result.source) # optimized source (ast.unparse)
111
+ print(result.report.summary()) # per-pass statistics
112
+ run_path("script.py", argv=("--flag",))
113
+
114
+ optimize_file("script.py", disable="inline,licm") # every entry point accepts `disable`
115
+ ```
116
+
117
+ ## Tests
118
+
119
+ ```powershell
120
+ python tests/verify_opast.py # full acceptance suite: per-pass behavior assertions
121
+ # + original-vs-optimized output comparison
122
+ ```
123
+
124
+ Case sources are generated into `tests/cases/` on demand (not tracked); the bench harness doubles as a correctness check.
125
+
126
+ ## Prior art
127
+
128
+ - [pyastop](https://github.com/xiaonanln/pyastop) (2017–2018): an early AST-optimizer prototype built around whole-project analysis and comment hints. opast instead proves each rewrite safe per pass, with no user annotations.
129
+ - [fatoptimizer](https://github.com/vstinner/fatoptimizer) (FAT Python, PEPs 509/510/511): runtime guards + function specialization, abandoned over guard overhead. opast's static passes have zero runtime overhead; runtime machinery exists only behind the opt-in `--jit`.
130
+ - CPython's built-in AST/peephole optimizer folds constants only; opast's gains come from everything beyond that.
131
+
132
+ ## Known limitations
133
+
134
+ - Optimized code is executed via `compile(optimized_ast, original_filename)`; traceback line numbers reuse original positions where possible but may drift slightly on rewritten lines. Inlining removes call frames from tracebacks (documented per pass).
135
+ - All transformations require static proof; anything unprovable is left untouched. Documented edge observables (e.g. the partially-built-list window excluded via `try`/`with` checks) are listed per pass in [README-ZH.md](README-ZH.md).
136
+ - Python ≥ 3.10.
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"] # PEP 639 SPDX license expression
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # Distribution/import/CLI name: opast ("OPtimizing AST"); GitHub repo: pyOpAst.
7
+ name = "opast"
8
+ version = "0.1.0"
9
+ description = "Conservative AST-level Python source optimizer: provably-semantics-preserving passes (folding, propagation, inlining, loop rewrites) with dynamic-code fallback, plus an opt-in numba JIT path."
10
+ requires-python = ">=3.10"
11
+ readme = "README.md"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ authors = [{ name = "YZJYBT", email = "yzj0xfff2@gmail.com" }]
15
+ keywords = ["optimizer", "ast", "performance", "source-to-source", "numba", "jit"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Software Development :: Compilers",
26
+ "Topic :: Software Development :: Code Generators",
27
+ ]
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/YZJYBT/pyOpAst"
31
+ Issues = "https://github.com/YZJYBT/pyOpAst/issues"
32
+ Changelog = "https://github.com/YZJYBT/pyOpAst/blob/main/CHANGELOG.md"
33
+
34
+ [project.optional-dependencies]
35
+ # numba >=0.61 covers Python 3.10-3.13; Python 3.14 needs numba >=0.65.
36
+ jit = ["numba>=0.61"]
37
+
38
+ [project.scripts]
39
+ opast = "opast.__main__:main"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
opast-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,60 @@
1
+ """opast -- AST-based Python source optimizer.
2
+
3
+ Optimizations: constant folding, algebraic simplification, dead code
4
+ elimination and simple function inlining. Scopes containing dynamic
5
+ constructs (eval/exec/globals/locals/vars/compile/__import__/``import *`` ...)
6
+ are conservatively skipped so optimization never changes their behaviour.
7
+
8
+ Typical use::
9
+
10
+ from opast import optimize_file, run_path
11
+
12
+ result = optimize_file("script.py")
13
+ print(result.source) # optimized source
14
+ print(result.report.summary()) # what happened
15
+
16
+ run_path("script.py", argv=("--flag",)) # optimize then execute
17
+
18
+ Every entry point accepts ``disable`` -- an iterable or comma-separated
19
+ string of pass names (see :data:`PASS_NAMES`, plus ``"jit"``) to skip::
20
+
21
+ optimize_file("script.py", disable="inline,licm")
22
+ run_source("print(1+1)", disable=("constant-folding",))
23
+ """
24
+
25
+ from .pipeline import (
26
+ DEFAULT_MAX_ITERATIONS,
27
+ PASS_NAMES,
28
+ OptimizationReport,
29
+ OptimizationResult,
30
+ PassStats,
31
+ optimize_ast,
32
+ optimize_file,
33
+ optimize_source,
34
+ )
35
+ from .runner import execute, run_path, run_source
36
+
37
+
38
+ def load_ipython_extension(ipython) -> None:
39
+ """Enable ``%%opast`` in IPython/Jupyter via ``%load_ext opast``."""
40
+ from .magic import load_ipython_extension as _load
41
+
42
+ _load(ipython)
43
+
44
+
45
+ __version__ = "0.1.0"
46
+
47
+ __all__ = [
48
+ "DEFAULT_MAX_ITERATIONS",
49
+ "PASS_NAMES",
50
+ "OptimizationReport",
51
+ "OptimizationResult",
52
+ "PassStats",
53
+ "optimize_ast",
54
+ "optimize_file",
55
+ "optimize_source",
56
+ "execute",
57
+ "run_path",
58
+ "run_source",
59
+ "__version__",
60
+ ]
@@ -0,0 +1,136 @@
1
+ """Command line interface: ``python -m opast [options] script.py [args...]``
2
+ or ``python -m opast [options] -c "code" [args...]``."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from .pipeline import (
12
+ DEFAULT_MAX_ITERATIONS,
13
+ PASS_NAMES,
14
+ optimize_file,
15
+ optimize_source,
16
+ )
17
+ from .runner import execute
18
+
19
+
20
+ def main(argv: list[str] | None = None) -> int:
21
+ parser = argparse.ArgumentParser(
22
+ prog="opast",
23
+ description=(
24
+ "Optimize a Python script with AST passes (constant folding, "
25
+ "algebraic simplification, dead code elimination, simple function "
26
+ "inlining) and run it with the current interpreter. Scopes that "
27
+ "use dynamic constructs (eval/exec/globals/locals/vars/compile/"
28
+ "__import__/import *) are left untouched."
29
+ ),
30
+ )
31
+ parser.add_argument("--show", action="store_true",
32
+ help="print the optimized source to stdout")
33
+ parser.add_argument("-o", "--output", metavar="FILE",
34
+ help="write the optimized source to FILE")
35
+ parser.add_argument("--no-run", action="store_true",
36
+ help="only optimize, do not execute the script")
37
+ parser.add_argument("--report", action="store_true",
38
+ help="print per-pass statistics to stderr")
39
+ parser.add_argument("--max-iterations", type=int,
40
+ default=DEFAULT_MAX_ITERATIONS, metavar="N",
41
+ help="max pipeline iterations (default: %(default)s)")
42
+ parser.add_argument("--jit", action="store_true",
43
+ help="decorate hot numeric functions with numba.njit "
44
+ "(optional dependency 'opast[jit]'; any numba "
45
+ "failure falls back to plain Python at runtime; "
46
+ "see README for the int64 caveat)")
47
+ parser.add_argument("--disable", metavar="PASSES", default="",
48
+ help="comma-separated pass names to skip: "
49
+ + ", ".join((*PASS_NAMES, "jit")))
50
+ parser.add_argument("--opt-imports", action="store_true",
51
+ help="also optimize imported pure-Python modules "
52
+ "under the script's directory (opt-in: modules "
53
+ "whose globals are monkeypatched from outside "
54
+ "must not be optimized; see README)")
55
+ parser.add_argument("--opt-imports-under", metavar="DIR", action="append",
56
+ default=[],
57
+ help="optimize imports under DIR (repeatable; "
58
+ "implies the hook without adding the script's "
59
+ "directory unless --opt-imports is also given)")
60
+ parser.add_argument("-c", "--code", metavar="CODE",
61
+ help="optimize and run CODE directly (like python -c; "
62
+ "remaining arguments go to sys.argv)")
63
+ parser.add_argument("script", nargs="?",
64
+ help="path to the Python script (or, with -c, the "
65
+ "first script argument)")
66
+ parser.add_argument("args", nargs=argparse.REMAINDER,
67
+ help="arguments passed to the script")
68
+ ns = parser.parse_args(argv)
69
+
70
+ if ns.code is None and ns.script is None:
71
+ parser.error("a script path is required (or use -c CODE)")
72
+
73
+ try:
74
+ if ns.code is not None:
75
+ result = optimize_source(
76
+ ns.code, filename="<string>",
77
+ max_iterations=ns.max_iterations, jit=ns.jit,
78
+ disable=ns.disable,
79
+ )
80
+ else:
81
+ result = optimize_file(
82
+ ns.script, max_iterations=ns.max_iterations, jit=ns.jit,
83
+ disable=ns.disable,
84
+ )
85
+ except ValueError as exc: # unknown --disable pass name
86
+ parser.error(str(exc))
87
+
88
+ if ns.jit:
89
+ from . import jitsupport
90
+
91
+ if not jitsupport.numba_available():
92
+ print(
93
+ "opast: numba is not available on this interpreter; "
94
+ "--jit decorations will run as plain Python.",
95
+ file=sys.stderr,
96
+ )
97
+ if ns.show:
98
+ print(result.source)
99
+ if ns.output:
100
+ Path(ns.output).write_text(result.source + "\n", encoding="utf-8")
101
+ if ns.report:
102
+ print(result.report.summary(), file=sys.stderr)
103
+ if not ns.no_run:
104
+ finder = None
105
+ if ns.opt_imports or ns.opt_imports_under:
106
+ from . import importhook
107
+
108
+ roots = list(ns.opt_imports_under)
109
+ if ns.opt_imports:
110
+ if ns.code is not None:
111
+ roots.append(os.getcwd())
112
+ else:
113
+ roots.append(str(Path(ns.script).resolve().parent))
114
+ finder = importhook.install(
115
+ roots,
116
+ max_iterations=ns.max_iterations,
117
+ jit=ns.jit,
118
+ disable=ns.disable,
119
+ report=ns.report,
120
+ )
121
+ try:
122
+ if ns.code is not None:
123
+ # With -c the positional slot, if used, is really the first
124
+ # script argument -- mimic ``python -c`` (sys.argv[0] == "-c").
125
+ args = ([ns.script] if ns.script is not None else []) + list(ns.args)
126
+ execute(result, tuple(args), write_source=ns.jit, argv0="-c")
127
+ else:
128
+ execute(result, tuple(ns.args), write_source=ns.jit)
129
+ finally:
130
+ if finder is not None:
131
+ importhook.uninstall(finder)
132
+ return 0
133
+
134
+
135
+ if __name__ == "__main__":
136
+ sys.exit(main())