opast 0.1.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.
Files changed (51) hide show
  1. opast/__init__.py +60 -0
  2. opast/__main__.py +136 -0
  3. opast/analysis.py +608 -0
  4. opast/bench/__init__.py +12 -0
  5. opast/bench/__main__.py +162 -0
  6. opast/bench/workloads/__init__.py +1 -0
  7. opast/bench/workloads/algebra.py +16 -0
  8. opast/bench/workloads/comptomap.py +14 -0
  9. opast/bench/workloads/control.py +14 -0
  10. opast/bench/workloads/daily.py +78 -0
  11. opast/bench/workloads/dedynamize.py +12 -0
  12. opast/bench/workloads/inline.py +23 -0
  13. opast/bench/workloads/inlinestmt.py +22 -0
  14. opast/bench/workloads/jitlazy.py +20 -0
  15. opast/bench/workloads/lencache.py +19 -0
  16. opast/bench/workloads/licm.py +22 -0
  17. opast/bench/workloads/localize.py +28 -0
  18. opast/bench/workloads/loopfold.py +25 -0
  19. opast/bench/workloads/looptocomp.py +17 -0
  20. opast/bench/workloads/mixed.py +25 -0
  21. opast/bench/workloads/rangeiter.py +17 -0
  22. opast/bench/workloads/strength.py +15 -0
  23. opast/importhook.py +198 -0
  24. opast/jitsupport.py +372 -0
  25. opast/magic.py +157 -0
  26. opast/passes/__init__.py +35 -0
  27. opast/passes/algebra.py +222 -0
  28. opast/passes/base.py +40 -0
  29. opast/passes/comprehension.py +281 -0
  30. opast/passes/cse.py +281 -0
  31. opast/passes/deadcode.py +167 -0
  32. opast/passes/dedynamize.py +266 -0
  33. opast/passes/folding.py +227 -0
  34. opast/passes/inline.py +454 -0
  35. opast/passes/jit.py +929 -0
  36. opast/passes/licm.py +253 -0
  37. opast/passes/localize.py +263 -0
  38. opast/passes/loopfold.py +358 -0
  39. opast/passes/looptocomp.py +274 -0
  40. opast/passes/propagate.py +491 -0
  41. opast/passes/rangeiter.py +269 -0
  42. opast/passes/unused.py +221 -0
  43. opast/pipeline.py +198 -0
  44. opast/runner.py +125 -0
  45. opast/safety.py +140 -0
  46. opast-0.1.0.dist-info/METADATA +167 -0
  47. opast-0.1.0.dist-info/RECORD +51 -0
  48. opast-0.1.0.dist-info/WHEEL +5 -0
  49. opast-0.1.0.dist-info/entry_points.txt +2 -0
  50. opast-0.1.0.dist-info/licenses/LICENSE +21 -0
  51. opast-0.1.0.dist-info/top_level.txt +1 -0
opast/__init__.py ADDED
@@ -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
+ ]
opast/__main__.py ADDED
@@ -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())