Skip to content

Commit ba2d0ac

Browse files
Carl Meyerfacebook-github-bot
Carl Meyer
authored andcommitted
port comprehension inlining to Python/compile.c also
Summary: Original comprehension inlining diff from 3.8: D28940584 (03040db) In D39216342 we ported comprehension inlining only to the py-compiler. Since then we've discovered that py-compiler is in practice still used only for strict and static modules, so we should port valuable optimizations to `compile.c` also. This ports comprehension inlining to `compile.c` as well. Ensure that we run the comprehension-inliner tests against both py-compiler and compile.c. Also requires moving the implementation in py-compiler from `CinderCodeGenerator` up to the base class `CinderBaseCodeGenerator`, since side-by-side comparison testing betweeen py-compiler and compile.c uses `CinderBaseCodeGenerator`. In 3.8 there was a difference in behavior between py-compiler and compile.c on the two `nested_diff_scopes` inlining tests. This difference was not caught in 3.8 because the inlining tests only ran against `compile.c`, and the tested case did not occur in the corpus for side-by-side testing. In porting inlining to py-compiler in 3.10, we switched the inlining tests to run against py-compiler, and adjusted the two `nested_diff_scopes` tests accordingly; this resulted in adding `DELETE_DEREF` opcodes in those two tests that weren't present in those tests in 3.8. In this diff I remove those opcodes again and adjust the py-compiler implementation so that they aren't emitted. In actual fact neither the presence nor absence of those `DELETE_DEREF` opcodes is correct. It's not safe to inline a comprehension with cell vars at all, so in the two `nested_diff_scopes` tests we should not be inlining the comprehension with the lambda at all. I will make this change as a separate bugfix in 3.8 and port it separately to 3.10. Reviewed By: tekknolagi Differential Revision: D40656285 fbshipit-source-id: 405d3fe
1 parent 1d0bd34 commit ba2d0ac

File tree

14 files changed

+3111
-2788
lines changed

14 files changed

+3111
-2788
lines changed

Include/internal/pycore_symtable.h

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct symtable {
3939
the symbol table */
4040
int recursion_depth; /* current recursion depth */
4141
int recursion_limit; /* recursion limit */
42+
int st_inline_comprehensions; /* inline comprehensions? */
4243
};
4344

4445
typedef struct _symtable_entry {
@@ -72,6 +73,7 @@ typedef struct _symtable_entry {
7273
int ste_end_col_offset; /* end offset of first line of block */
7374
int ste_opt_lineno; /* lineno of last exec or import * */
7475
int ste_opt_col_offset; /* offset of last exec or import * */
76+
unsigned int ste_inlined_comprehension; /* comprehension is inlined; symbols merged in parent scope */
7577
struct symtable *ste_table;
7678
} PySTEntryObject;
7779

@@ -85,6 +87,11 @@ extern struct symtable* _PySymtable_Build(
8587
struct _mod *mod,
8688
PyObject *filename,
8789
PyFutureFeatures *future);
90+
extern struct symtable* _PySymtable_BuildEx(
91+
struct _mod *mod,
92+
PyObject *filename,
93+
PyFutureFeatures *future,
94+
int inline_comprehensions);
8895
PyAPI_FUNC(PySTEntryObject *) PySymtable_Lookup(struct symtable *, void *);
8996

9097
extern void _PySymtable_Free(struct symtable *);

Lib/compiler/pycodegen.py

+75-67
Original file line numberDiff line numberDiff line change
@@ -3018,13 +3018,86 @@ def __init__(self, kind, block, exit, unwinding_datum):
30183018
self.unwinding_datum = unwinding_datum
30193019

30203020

3021-
# This is identical to the base code generator, except for the fact that CO_SUPPRESS_JIT is emitted.
30223021
class CinderBaseCodeGenerator(CodeGenerator):
3022+
"""
3023+
Code generator equivalent to `Python/compile.c` in Cinder.
3024+
3025+
The base `CodeGenerator` is equivalent to upstream `Python/compile.c`.
3026+
"""
3027+
30233028
flow_graph = pyassem.PyFlowGraphCinder
3029+
_SymbolVisitor = symbols.CinderSymbolVisitor
3030+
3031+
# TODO(T132400505): Split into smaller methods.
3032+
def compile_comprehension(
3033+
self,
3034+
node: CompNode,
3035+
name: str,
3036+
elt: ast.expr,
3037+
val: ast.expr | None,
3038+
opcode: str,
3039+
oparg: object = 0,
3040+
) -> None:
3041+
# fetch the scope that correspond to comprehension
3042+
scope = self.scopes[node]
3043+
if scope.inlined:
3044+
# for inlined comprehension process with current generator
3045+
gen = self
3046+
else:
3047+
gen = self.make_func_codegen(
3048+
node, self.conjure_arguments([ast.arg(".0", None)]), name, node.lineno
3049+
)
3050+
gen.set_lineno(node)
3051+
3052+
if opcode:
3053+
gen.emit(opcode, oparg)
3054+
3055+
gen.compile_comprehension_generator(
3056+
node.generators, 0, 0, elt, val, type(node), not scope.inlined
3057+
)
3058+
3059+
if scope.inlined:
3060+
# collect list of defs that were introduced by comprehension
3061+
# note that we need to exclude:
3062+
# - .0 parameter since it is used
3063+
# - non-local names (typically named expressions), they are
3064+
# defined in enclosing scope and thus should not be deleted
3065+
to_delete = [
3066+
v
3067+
for v in scope.defs
3068+
if v != ".0"
3069+
and v not in scope.nonlocals
3070+
and v not in scope.parent.cells
3071+
]
3072+
# sort names to have deterministic deletion order
3073+
to_delete.sort()
3074+
for v in to_delete:
3075+
self.delName(v)
3076+
return
3077+
3078+
if not isinstance(node, ast.GeneratorExp):
3079+
gen.emit("RETURN_VALUE")
3080+
3081+
gen.finishFunction()
3082+
3083+
self._makeClosure(gen, 0)
3084+
3085+
# precomputation of outmost iterable
3086+
self.visit(node.generators[0].iter)
3087+
if node.generators[0].is_async:
3088+
self.emit("GET_AITER")
3089+
else:
3090+
self.emit("GET_ITER")
3091+
self.emit("CALL_FUNCTION", 1)
3092+
3093+
if gen.scope.coroutine and type(node) is not ast.GeneratorExp:
3094+
self.emit("GET_AWAITABLE")
3095+
self.emit("LOAD_CONST", None)
3096+
self.emit("YIELD_FROM")
30243097

30253098

30263099
class CinderCodeGenerator(CinderBaseCodeGenerator):
3027-
_SymbolVisitor = symbols.CinderSymbolVisitor
3100+
"""Contains some optimizations not (yet) present in Python/compile.c."""
30283101

30293102
def set_qual_name(self, qualname):
30303103
self._qual_name = qualname
@@ -3106,71 +3179,6 @@ def findFutures(self, node):
31063179
future_flags |= consts.CO_FUTURE_EAGER_IMPORTS
31073180
return future_flags
31083181

3109-
# TODO(T132400505): Split into smaller methods.
3110-
def compile_comprehension(
3111-
self,
3112-
node: CompNode,
3113-
name: str,
3114-
elt: ast.expr,
3115-
val: ast.expr | None,
3116-
opcode: str,
3117-
oparg: object = 0,
3118-
) -> None:
3119-
# fetch the scope that correspond to comprehension
3120-
scope = self.scopes[node]
3121-
if scope.inlined:
3122-
# for inlined comprehension process with current generator
3123-
gen = self
3124-
else:
3125-
gen = self.make_func_codegen(
3126-
node, self.conjure_arguments([ast.arg(".0", None)]), name, node.lineno
3127-
)
3128-
gen.set_lineno(node)
3129-
3130-
if opcode:
3131-
gen.emit(opcode, oparg)
3132-
3133-
gen.compile_comprehension_generator(
3134-
node.generators, 0, 0, elt, val, type(node), not scope.inlined
3135-
)
3136-
3137-
if scope.inlined:
3138-
# collect list of defs that were introduced by comprehension
3139-
# note that we need to exclude:
3140-
# - .0 parameter since it is used
3141-
# - non-local names (typically named expressions), they are
3142-
# defined in enclosing scope and thus should not be deleted
3143-
to_delete = [
3144-
v
3145-
for v in scope.defs
3146-
if v != ".0" and v not in scope.nonlocals and v not in scope.cells
3147-
]
3148-
# sort names to have deterministic deletion order
3149-
to_delete.sort()
3150-
for v in to_delete:
3151-
self.delName(v)
3152-
return
3153-
3154-
if not isinstance(node, ast.GeneratorExp):
3155-
gen.emit("RETURN_VALUE")
3156-
3157-
gen.finishFunction()
3158-
3159-
self._makeClosure(gen, 0)
3160-
3161-
# precomputation of outmost iterable
3162-
self.visit(node.generators[0].iter)
3163-
if node.generators[0].is_async:
3164-
self.emit("GET_AITER")
3165-
else:
3166-
self.emit("GET_ITER")
3167-
self.emit("CALL_FUNCTION", 1)
3168-
3169-
if gen.scope.coroutine and type(node) is not ast.GeneratorExp:
3170-
self.emit("GET_AWAITABLE")
3171-
self.emit("LOAD_CONST", None)
3172-
self.emit("YIELD_FROM")
3173-
31743182

31753183
def get_default_generator():
31763184
if "cinder" in sys.version:

Lib/compiler/symbols.py

+5
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,11 @@ def inline_nested_comprehensions(self):
777777
for u in comp.uses.keys():
778778
self.add_use(u)
779779

780+
# cell vars in comprehension become cells in current scope
781+
for c in comp.cells.keys():
782+
if c != ".0":
783+
self.cells[c] = 1
784+
780785
# splice children of comprehension into current scope
781786
# replacing existing entry for 'comp'
782787
i = self.children.index(comp)

Lib/importlib/_bootstrap_external.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,9 @@ def _write_atomic(path, data, mode=0o666):
357357
# Python 3.10b1 3442 (Port Cinder-specific PyCodeObject.co_qualname from 3.8)
358358
# Python 3.10b1 3443 (remove primitive enums, migrate PRIMITIVE_(UN)BOX back to opargs)
359359
# Python 3.10b1 3444 (optimizations of LOAD_METHOD_SUPER and LOAD_ATTR_SUPER)
360-
# Python 3.10b1 3445 (comprehension inliner)
360+
# Python 3.10b1 3445 (comprehension inliner in Lib/compiler)
361361
# Python 3.10b1 3446 (Set default PyCodeObject.co_qualname missed in 3442)
362+
# Python 3.10b1 3447 (comprehension inliner in Python/compile.c)
362363

363364

364365
#
@@ -369,7 +370,7 @@ def _write_atomic(path, data, mode=0o666):
369370
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
370371
# in PC/launcher.c must also be updated.
371372

372-
MAGIC_NUMBER = (3446).to_bytes(2, 'little') + b'\r\n'
373+
MAGIC_NUMBER = (3447).to_bytes(2, 'little') + b'\r\n'
373374
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
374375

375376
_PYCACHE = '__pycache__'

Lib/test/test_compiler/test_cinder.py

+38-31
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ def f(self):
291291

292292

293293
class ComprehensionInlinerTests(DisTests):
294+
compiler = staticmethod(py_compile)
295+
296+
def compile(self, code_str):
297+
return self.compiler(dedent(code_str), "<string>", "exec")
298+
294299
def test_sync_comp_top(self):
295300
# ensure module level comprehensions are not inlined
296301
src = """
@@ -307,7 +312,7 @@ def test_sync_comp_top(self):
307312
14 LOAD_CONST 2 (None)
308313
16 RETURN_VALUE
309314
"""
310-
co = py_compile(dedent(src), "<string>", "exec")
315+
co = self.compile(src)
311316
self.do_disassembly_test(co, expected)
312317

313318
def test_inline_sync_comp_nested_diff_scopes_1(self):
@@ -325,27 +330,26 @@ def f():
325330
10 LOAD_DEREF 0 (x)
326331
12 LIST_APPEND 2
327332
14 JUMP_ABSOLUTE 3 (to 6)
328-
>> 16 DELETE_DEREF 0 (x)
329-
18 POP_TOP
330-
331-
4 20 BUILD_LIST 0
332-
22 LOAD_GLOBAL 0 (lst)
333-
24 GET_ITER
334-
>> 26 FOR_ITER 8 (to 44)
335-
28 STORE_DEREF 0 (x)
336-
30 LOAD_CLOSURE 0 (x)
337-
32 BUILD_TUPLE 1
338-
34 LOAD_CONST 1 (<code object <lambda> at 0x..., file "<string>", line 4>)
339-
36 LOAD_CONST 2 ('f.<locals>.<lambda>')
340-
38 MAKE_FUNCTION 8 (closure)
341-
40 LIST_APPEND 2
342-
42 JUMP_ABSOLUTE 13 (to 26)
343-
>> 44 POP_TOP
344-
46 LOAD_CONST 0 (None)
345-
48 RETURN_VALUE
333+
>> 16 POP_TOP
334+
335+
4 18 BUILD_LIST 0
336+
20 LOAD_GLOBAL 0 (lst)
337+
22 GET_ITER
338+
>> 24 FOR_ITER 8 (to 42)
339+
26 STORE_DEREF 0 (x)
340+
28 LOAD_CLOSURE 0 (x)
341+
30 BUILD_TUPLE 1
342+
32 LOAD_CONST 1 (<code object <lambda> at 0x..., file "<string>", line 4>)
343+
34 LOAD_CONST 2 ('f.<locals>.<lambda>')
344+
36 MAKE_FUNCTION 8 (closure)
345+
38 LIST_APPEND 2
346+
40 JUMP_ABSOLUTE 12 (to 24)
347+
>> 42 POP_TOP
348+
44 LOAD_CONST 0 (None)
349+
46 RETURN_VALUE
346350
"""
347351
g = {}
348-
exec(py_compile(dedent(src), "<string>", "exec"), g)
352+
exec(self.compile(src), g)
349353
self.do_disassembly_test(g["f"], expected)
350354

351355
def test_inline_sync_comp_nested_diff_scopes_2(self):
@@ -377,13 +381,12 @@ def f():
377381
36 LOAD_DEREF 0 (x)
378382
38 LIST_APPEND 2
379383
40 JUMP_ABSOLUTE 16 (to 32)
380-
>> 42 DELETE_DEREF 0 (x)
381-
44 POP_TOP
382-
46 LOAD_CONST 0 (None)
383-
48 RETURN_VALUE
384+
>> 42 POP_TOP
385+
44 LOAD_CONST 0 (None)
386+
46 RETURN_VALUE
384387
"""
385388
g = {}
386-
exec(py_compile(dedent(src), "<string>", "exec"), g)
389+
exec(self.compile(src), g)
387390
self.do_disassembly_test(g["f"], expected)
388391

389392
def test_inline_sync_comp_nested_comprehensions(self):
@@ -414,7 +417,7 @@ def f():
414417
38 RETURN_VALUE
415418
"""
416419
g = {}
417-
exec(py_compile(dedent(src), "<string>", "exec"), g)
420+
exec(self.compile(src), g)
418421
self.do_disassembly_test(g["f"], expected)
419422

420423
def test_inline_sync_comp_named_expr_1(self):
@@ -441,7 +444,7 @@ def f():
441444
30 RETURN_VALUE
442445
"""
443446
g = {}
444-
exec(py_compile(dedent(src), "<string>", "exec"), g)
447+
exec(self.compile(src), g)
445448
self.do_disassembly_test(g["f"], expected)
446449

447450
def test_inline_async_comp_free_var1(self):
@@ -490,7 +493,7 @@ async def f(lst):
490493
68 RETURN_VALUE
491494
"""
492495
g = {}
493-
exec(py_compile(dedent(src), "<string>", "exec"), g)
496+
exec(self.compile(src), g)
494497
self.do_disassembly_test(g["f"], expected)
495498

496499
def test_comprehension_inlining_name_conflict_with_implicit_global(self):
@@ -520,7 +523,7 @@ def g():
520523
"""
521524

522525
g = {}
523-
exec(py_compile(dedent(src), "<string>", "exec"), g)
526+
exec(self.compile(src), g)
524527
self.do_disassembly_test(g["f"], expected)
525528

526529
def test_use_param_1(self):
@@ -554,7 +557,7 @@ def f(self, name, data, files=(), dirs=()):
554557
44 RETURN_VALUE
555558
"""
556559
g = {}
557-
exec(py_compile(dedent(src), "<string>", "exec"), g)
560+
exec(self.compile(src), g)
558561
self.do_disassembly_test(g["f"], expected)
559562

560563
def test_inline_comp_global1(self):
@@ -586,5 +589,9 @@ def f():
586589
34 RETURN_VALUE
587590
"""
588591
g = {}
589-
exec(py_compile(dedent(src), "<string>", "exec"), g)
592+
exec(self.compile(src), g)
590593
self.do_disassembly_test(g["f"], expected)
594+
595+
596+
class ComprehensionInlinerBuiltinCompilerTests(ComprehensionInlinerTests):
597+
compiler = staticmethod(compile)

Lib/test/test_compiler/test_py310.py

-17
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,3 @@ def f():
5858
return x
5959
"""
6060
self._check(codestr)
61-
62-
def test_no_nested_async_comprehension(self):
63-
codestr = """
64-
async def foo(a):
65-
return {k: [y for y in k if await bar(y)] for k in a}
66-
"""
67-
68-
# The base code generator matches 3.10 upstream and thus has this
69-
# restriction, but the restriction has been lifted in 3.11
70-
# (see https://github.com/python/cpython/pull/6766), so we also lift
71-
# it in CinderCodeGenerator.
72-
self._check_error(
73-
codestr,
74-
"asynchronous comprehension outside of an asynchronous function",
75-
generator=BaseCodeGenerator,
76-
)
77-
self.compile(codestr, generator=CinderCodeGenerator)

0 commit comments

Comments
 (0)