Skip to content

Commit 10a91d7

Browse files
authored
gh-108113: Make it possible to create an optimized AST (#108154)
1 parent 47022a0 commit 10a91d7

File tree

10 files changed

+124
-14
lines changed

10 files changed

+124
-14
lines changed

Doc/library/ast.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -2122,10 +2122,12 @@ Async and await
21222122
Apart from the node classes, the :mod:`ast` module defines these utility functions
21232123
and classes for traversing abstract syntax trees:
21242124

2125-
.. function:: parse(source, filename='<unknown>', mode='exec', *, type_comments=False, feature_version=None)
2125+
.. function:: parse(source, filename='<unknown>', mode='exec', *, type_comments=False, feature_version=None, optimize=-1)
21262126

21272127
Parse the source into an AST node. Equivalent to ``compile(source,
2128-
filename, mode, ast.PyCF_ONLY_AST)``.
2128+
filename, mode, flags=FLAGS_VALUE, optimize=optimize)``,
2129+
where ``FLAGS_VALUE`` is ``ast.PyCF_ONLY_AST`` if ``optimize <= 0``
2130+
and ``ast.PyCF_OPTIMIZED_AST`` otherwise.
21292131

21302132
If ``type_comments=True`` is given, the parser is modified to check
21312133
and return type comments as specified by :pep:`484` and :pep:`526`.
@@ -2171,6 +2173,7 @@ and classes for traversing abstract syntax trees:
21712173

21722174
.. versionchanged:: 3.13
21732175
The minimum supported version for feature_version is now (3,7)
2176+
The ``optimize`` argument was added.
21742177

21752178

21762179
.. function:: unparse(ast_obj)

Doc/whatsnew/3.13.rst

+14
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ Other Language Changes
8585
This change will affect tools using docstrings, like :mod:`doctest`.
8686
(Contributed by Inada Naoki in :gh:`81283`.)
8787

88+
* The :func:`compile` built-in can now accept a new flag,
89+
``ast.PyCF_OPTIMIZED_AST``, which is similar to ``ast.PyCF_ONLY_AST``
90+
except that the returned ``AST`` is optimized according to the value
91+
of the ``optimize`` argument.
92+
(Contributed by Irit Katriel in :gh:`108113`).
93+
8894
New Modules
8995
===========
9096

@@ -94,6 +100,14 @@ New Modules
94100
Improved Modules
95101
================
96102

103+
ast
104+
---
105+
106+
* :func:`ast.parse` now accepts an optional argument ``optimize``
107+
which is passed on to the :func:`compile` built-in. This makes it
108+
possible to obtain an optimized ``AST``.
109+
(Contributed by Irit Katriel in :gh:`108113`).
110+
97111
array
98112
-----
99113

Include/cpython/compile.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
#define PyCF_TYPE_COMMENTS 0x1000
2020
#define PyCF_ALLOW_TOP_LEVEL_AWAIT 0x2000
2121
#define PyCF_ALLOW_INCOMPLETE_INPUT 0x4000
22+
#define PyCF_OPTIMIZED_AST (0x8000 | PyCF_ONLY_AST)
2223
#define PyCF_COMPILE_MASK (PyCF_ONLY_AST | PyCF_ALLOW_TOP_LEVEL_AWAIT | \
2324
PyCF_TYPE_COMMENTS | PyCF_DONT_IMPLY_DEDENT | \
24-
PyCF_ALLOW_INCOMPLETE_INPUT)
25+
PyCF_ALLOW_INCOMPLETE_INPUT | PyCF_OPTIMIZED_AST)
2526

2627
typedef struct {
2728
int cf_flags; /* bitmask of CO_xxx flags relevant to future */

Lib/ast.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232

3333

3434
def parse(source, filename='<unknown>', mode='exec', *,
35-
type_comments=False, feature_version=None):
35+
type_comments=False, feature_version=None, optimize=-1):
3636
"""
3737
Parse the source into an AST node.
3838
Equivalent to compile(source, filename, mode, PyCF_ONLY_AST).
3939
Pass type_comments=True to get back type comments where the syntax allows.
4040
"""
4141
flags = PyCF_ONLY_AST
42+
if optimize > 0:
43+
flags |= PyCF_OPTIMIZED_AST
4244
if type_comments:
4345
flags |= PyCF_TYPE_COMMENTS
4446
if feature_version is None:
@@ -50,7 +52,7 @@ def parse(source, filename='<unknown>', mode='exec', *,
5052
feature_version = minor
5153
# Else it should be an int giving the minor version for 3.x.
5254
return compile(source, filename, mode, flags,
53-
_feature_version=feature_version)
55+
_feature_version=feature_version, optimize=optimize)
5456

5557

5658
def literal_eval(node_or_string):

Lib/test/test_ast.py

+28
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,34 @@ def test_ast_validation(self):
357357
tree = ast.parse(snippet)
358358
compile(tree, '<string>', 'exec')
359359

360+
def test_optimization_levels__debug__(self):
361+
cases = [(-1, '__debug__'), (0, '__debug__'), (1, False), (2, False)]
362+
for (optval, expected) in cases:
363+
with self.subTest(optval=optval, expected=expected):
364+
res = ast.parse("__debug__", optimize=optval)
365+
self.assertIsInstance(res.body[0], ast.Expr)
366+
if isinstance(expected, bool):
367+
self.assertIsInstance(res.body[0].value, ast.Constant)
368+
self.assertEqual(res.body[0].value.value, expected)
369+
else:
370+
self.assertIsInstance(res.body[0].value, ast.Name)
371+
self.assertEqual(res.body[0].value.id, expected)
372+
373+
def test_optimization_levels_const_folding(self):
374+
folded = ('Expr', (1, 0, 1, 5), ('Constant', (1, 0, 1, 5), 3, None))
375+
not_folded = ('Expr', (1, 0, 1, 5),
376+
('BinOp', (1, 0, 1, 5),
377+
('Constant', (1, 0, 1, 1), 1, None),
378+
('Add',),
379+
('Constant', (1, 4, 1, 5), 2, None)))
380+
381+
cases = [(-1, not_folded), (0, not_folded), (1, folded), (2, folded)]
382+
for (optval, expected) in cases:
383+
with self.subTest(optval=optval):
384+
tree = ast.parse("1 + 2", optimize=optval)
385+
res = to_tuple(tree.body[0])
386+
self.assertEqual(res, expected)
387+
360388
def test_invalid_position_information(self):
361389
invalid_linenos = [
362390
(10, 1), (-10, -11), (10, -11), (-5, -2), (-5, 1)

Lib/test/test_builtin.py

+32-9
Original file line numberDiff line numberDiff line change
@@ -369,16 +369,17 @@ def f(): """doc"""
369369
(1, False, 'doc', False, False),
370370
(2, False, None, False, False)]
371371
for optval, *expected in values:
372+
with self.subTest(optval=optval):
372373
# test both direct compilation and compilation via AST
373-
codeobjs = []
374-
codeobjs.append(compile(codestr, "<test>", "exec", optimize=optval))
375-
tree = ast.parse(codestr)
376-
codeobjs.append(compile(tree, "<test>", "exec", optimize=optval))
377-
for code in codeobjs:
378-
ns = {}
379-
exec(code, ns)
380-
rv = ns['f']()
381-
self.assertEqual(rv, tuple(expected))
374+
codeobjs = []
375+
codeobjs.append(compile(codestr, "<test>", "exec", optimize=optval))
376+
tree = ast.parse(codestr)
377+
codeobjs.append(compile(tree, "<test>", "exec", optimize=optval))
378+
for code in codeobjs:
379+
ns = {}
380+
exec(code, ns)
381+
rv = ns['f']()
382+
self.assertEqual(rv, tuple(expected))
382383

383384
def test_compile_top_level_await_no_coro(self):
384385
"""Make sure top level non-await codes get the correct coroutine flags"""
@@ -517,6 +518,28 @@ def test_compile_async_generator(self):
517518
exec(co, glob)
518519
self.assertEqual(type(glob['ticker']()), AsyncGeneratorType)
519520

521+
def test_compile_ast(self):
522+
args = ("a*(1+2)", "f.py", "exec")
523+
raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0]
524+
opt = compile(*args, flags = ast.PyCF_OPTIMIZED_AST).body[0]
525+
526+
for tree in (raw, opt):
527+
self.assertIsInstance(tree.value, ast.BinOp)
528+
self.assertIsInstance(tree.value.op, ast.Mult)
529+
self.assertIsInstance(tree.value.left, ast.Name)
530+
self.assertEqual(tree.value.left.id, 'a')
531+
532+
raw_right = raw.value.right # expect BinOp(1, '+', 2)
533+
self.assertIsInstance(raw_right, ast.BinOp)
534+
self.assertIsInstance(raw_right.left, ast.Constant)
535+
self.assertEqual(raw_right.left.value, 1)
536+
self.assertIsInstance(raw_right.right, ast.Constant)
537+
self.assertEqual(raw_right.right.value, 2)
538+
539+
opt_right = opt.value.right # expect Constant(3)
540+
self.assertIsInstance(opt_right, ast.Constant)
541+
self.assertEqual(opt_right.value, 3)
542+
520543
def test_delattr(self):
521544
sys.spam = 1
522545
delattr(sys, 'spam')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The :func:`compile` built-in can now accept a new flag,
2+
``ast.PyCF_OPTIMIZED_AST``, which is similar to ``ast.PyCF_ONLY_AST``
3+
except that the returned ``AST`` is optimized according to the value
4+
of the ``optimize`` argument.
5+
6+
:func:`ast.parse` now accepts an optional argument ``optimize``
7+
which is passed on to the :func:`compile` built-in. This makes it
8+
possible to obtain an optimized ``AST``.

Parser/asdl_c.py

+3
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,9 @@ def visitModule(self, mod):
12081208
self.emit('if (PyModule_AddIntMacro(m, PyCF_TYPE_COMMENTS) < 0) {', 1)
12091209
self.emit("return -1;", 2)
12101210
self.emit('}', 1)
1211+
self.emit('if (PyModule_AddIntMacro(m, PyCF_OPTIMIZED_AST) < 0) {', 1)
1212+
self.emit("return -1;", 2)
1213+
self.emit('}', 1)
12111214
for dfn in mod.dfns:
12121215
self.visit(dfn)
12131216
self.emit("return 0;", 1)

Python/Python-ast.c

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/pythonrun.c

+25
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "pycore_pyerrors.h" // _PyErr_GetRaisedException, _Py_Offer_Suggestions
2222
#include "pycore_pylifecycle.h" // _Py_UnhandledKeyboardInterrupt
2323
#include "pycore_pystate.h" // _PyInterpreterState_GET()
24+
#include "pycore_symtable.h" // _PyFuture_FromAST()
2425
#include "pycore_sysmodule.h" // _PySys_Audit()
2526
#include "pycore_traceback.h" // _PyTraceBack_Print_Indented()
2627

@@ -1790,6 +1791,24 @@ run_pyc_file(FILE *fp, PyObject *globals, PyObject *locals,
17901791
return NULL;
17911792
}
17921793

1794+
static int
1795+
ast_optimize(mod_ty mod, PyObject *filename, PyCompilerFlags *cf,
1796+
int optimize, PyArena *arena)
1797+
{
1798+
PyFutureFeatures future;
1799+
if (!_PyFuture_FromAST(mod, filename, &future)) {
1800+
return -1;
1801+
}
1802+
int flags = future.ff_features | cf->cf_flags;
1803+
if (optimize == -1) {
1804+
optimize = _Py_GetConfig()->optimization_level;
1805+
}
1806+
if (!_PyAST_Optimize(mod, arena, optimize, flags)) {
1807+
return -1;
1808+
}
1809+
return 0;
1810+
}
1811+
17931812
PyObject *
17941813
Py_CompileStringObject(const char *str, PyObject *filename, int start,
17951814
PyCompilerFlags *flags, int optimize)
@@ -1806,6 +1825,12 @@ Py_CompileStringObject(const char *str, PyObject *filename, int start,
18061825
return NULL;
18071826
}
18081827
if (flags && (flags->cf_flags & PyCF_ONLY_AST)) {
1828+
if ((flags->cf_flags & PyCF_OPTIMIZED_AST) == PyCF_OPTIMIZED_AST) {
1829+
if (ast_optimize(mod, filename, flags, optimize, arena) < 0) {
1830+
_PyArena_Free(arena);
1831+
return NULL;
1832+
}
1833+
}
18091834
PyObject *result = PyAST_mod2obj(mod);
18101835
_PyArena_Free(arena);
18111836
return result;

0 commit comments

Comments
 (0)