Skip to content

Commit 72afa83

Browse files
committed
pythongh-97933: inline sync list/dict/set comprehensions
1 parent e867c1b commit 72afa83

File tree

17 files changed

+391
-77
lines changed

17 files changed

+391
-77
lines changed

Doc/library/dis.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,16 @@ iterations of the loop.
12141214

12151215
.. versionadded:: 3.12
12161216

1217+
.. opcode:: LOAD_FAST_OR_NULL (var_num)
1218+
1219+
Pushes a reference to the local ``co_varnames[var_num]`` onto the stack, or
1220+
pushes ``NULL`` onto the stack if the local variable has not been
1221+
initialized. This opcode has the same runtime effect as ``LOAD_FAST``; it
1222+
exists to maintain the invariant that ``LOAD_FAST`` will never load ``NULL``
1223+
and may appear only where the variable is guaranteed to be initialized.
1224+
1225+
.. versionadded:: 3.12
1226+
12171227
.. opcode:: STORE_FAST (var_num)
12181228

12191229
Stores ``STACK.pop()`` into the local ``co_varnames[var_num]``.

Include/internal/pycore_opcode.h

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_symtable.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ typedef struct _symtable_entry {
6464
unsigned ste_needs_class_closure : 1; /* for class scopes, true if a
6565
closure over __class__
6666
should be created */
67+
unsigned ste_comp_inlined : 1; /* true if this comprehension is inlined */
6768
unsigned ste_comp_iter_target : 1; /* true if visiting comprehension target */
6869
int ste_comp_iter_expr; /* non-zero if visiting a comprehension range expression */
6970
int ste_lineno; /* first line of block */

Include/opcode.h

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ def _write_atomic(path, data, mode=0o666):
431431
# Python 3.12a5 3515 (Embed jump mask in COMPARE_OP oparg)
432432
# Python 3.12a5 3516 (Add COMPARE_AND_BRANCH instruction)
433433
# Python 3.12a5 3517 (Change YIELD_VALUE oparg to exception block depth)
434+
# Python 3.12a5 3518 (Inline sync list/dict/set comprehensions in the calling function)
434435

435436
# Python 3.13 will start with 3550
436437

@@ -443,7 +444,7 @@ def _write_atomic(path, data, mode=0o666):
443444
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
444445
# in PC/launcher.c must also be updated.
445446

446-
MAGIC_NUMBER = (3517).to_bytes(2, 'little') + b'\r\n'
447+
MAGIC_NUMBER = (3518).to_bytes(2, 'little') + b'\r\n'
447448

448449
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
449450

Lib/opcode.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ def pseudo_op(name, op, real_ops):
196196
hascompare.append(141)
197197

198198
def_op('CALL_FUNCTION_EX', 142) # Flags
199+
def_op('LOAD_FAST_OR_NULL', 143) # Local variable number, may load NULL if undefined
200+
haslocal.append(143)
199201

200202
def_op('EXTENDED_ARG', 144)
201203
EXTENDED_ARG = 144

Lib/test/test_dis.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,15 @@ def bug708901():
161161

162162

163163
def bug1333982(x=[]):
164-
assert 0, ([s for s in x] +
164+
assert 0, ((s for s in x) +
165165
1)
166166
pass
167167

168168
dis_bug1333982 = """\
169169
%3d RESUME 0
170170
171171
%3d LOAD_ASSERTION_ERROR
172-
LOAD_CONST 1 (<code object <listcomp> at 0x..., file "%s", line %d>)
172+
LOAD_CONST 1 (<code object <genexpr> at 0x..., file "%s", line %d>)
173173
MAKE_FUNCTION 0
174174
LOAD_FAST 0 (x)
175175
GET_ITER
@@ -642,7 +642,7 @@ async def _co(x):
642642
def _h(y):
643643
def foo(x):
644644
'''funcdoc'''
645-
return [x + z for z in y]
645+
return list(x + z for z in y)
646646
return foo
647647

648648
dis_nested_0 = """\
@@ -672,13 +672,15 @@ def foo(x):
672672
673673
%3d RESUME 0
674674
675-
%3d LOAD_CLOSURE 0 (x)
675+
%3d LOAD_GLOBAL 1 (NULL + list)
676+
LOAD_CLOSURE 0 (x)
676677
BUILD_TUPLE 1
677-
LOAD_CONST 1 (<code object <listcomp> at 0x..., file "%s", line %d>)
678+
LOAD_CONST 1 (<code object <genexpr> at 0x..., file "%s", line %d>)
678679
MAKE_FUNCTION 8 (closure)
679680
LOAD_DEREF 1 (y)
680681
GET_ITER
681682
CALL 0
683+
CALL 1
682684
RETURN_VALUE
683685
""" % (dis_nested_0,
684686
__file__,
@@ -690,21 +692,29 @@ def foo(x):
690692
)
691693

692694
dis_nested_2 = """%s
693-
Disassembly of <code object <listcomp> at 0x..., file "%s", line %d>:
695+
Disassembly of <code object <genexpr> at 0x..., file "%s", line %d>:
694696
COPY_FREE_VARS 1
695697
696-
%3d RESUME 0
697-
BUILD_LIST 0
698+
%3d RETURN_GENERATOR
699+
POP_TOP
700+
RESUME 0
698701
LOAD_FAST 0 (.0)
699-
>> FOR_ITER 7 (to 26)
702+
>> FOR_ITER 9 (to 32)
700703
STORE_FAST 1 (z)
701704
LOAD_DEREF 2 (x)
702705
LOAD_FAST 1 (z)
703706
BINARY_OP 0 (+)
704-
LIST_APPEND 2
705-
JUMP_BACKWARD 9 (to 8)
707+
YIELD_VALUE 1
708+
RESUME 1
709+
POP_TOP
710+
JUMP_BACKWARD 11 (to 10)
706711
>> END_FOR
712+
LOAD_CONST 0 (None)
707713
RETURN_VALUE
714+
>> CALL_INTRINSIC_1 3
715+
RERAISE 1
716+
ExceptionTable:
717+
1 row
708718
""" % (dis_nested_1,
709719
__file__,
710720
_h.__code__.co_firstlineno + 3,

Lib/test/test_inspect.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4106,14 +4106,14 @@ def test(*args, **kwargs):
41064106

41074107
@cpython_only
41084108
def test_signature_bind_implicit_arg(self):
4109-
# Issue #19611: getcallargs should work with set comprehensions
4109+
# Issue #19611: getcallargs should work with comprehensions
41104110
def make_set():
4111-
return {z * z for z in range(5)}
4112-
setcomp_code = make_set.__code__.co_consts[1]
4113-
setcomp_func = types.FunctionType(setcomp_code, {})
4111+
return set(z * z for z in range(5))
4112+
gencomp_code = make_set.__code__.co_consts[1]
4113+
gencomp_func = types.FunctionType(gencomp_code, {})
41144114

41154115
iterator = iter(range(5))
4116-
self.assertEqual(self.call(setcomp_func, iterator), {0, 1, 4, 9, 16})
4116+
self.assertEqual(set(self.call(gencomp_func, iterator)), {0, 1, 4, 9, 16})
41174117

41184118
def test_signature_bind_posonly_kwargs(self):
41194119
def foo(bar, /, **kwargs):

Lib/test/test_listcomps.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,48 @@
143143
>>> test_func()
144144
[2, 2, 2, 2, 2]
145145
146+
A comprehension's iteration var, if in a cell, doesn't stomp on a previous value:
147+
148+
>>> def test_func():
149+
... y = 10
150+
... items = [(lambda: y) for y in range(5)]
151+
... x = y
152+
... y = 20
153+
... return x, [z() for z in items]
154+
>>> test_func()
155+
(10, [4, 4, 4, 4, 4])
156+
157+
A comprehension's iteration var doesn't shadow implicit globals for sibling scopes:
158+
159+
>>> g = -1
160+
>>> def test_func():
161+
... def inner():
162+
... return g
163+
... [g for g in range(5)]
164+
... return inner
165+
>>> test_func()()
166+
-1
167+
168+
A modification to a closed-over variable is visible in the outer scope:
169+
170+
>>> def test_func():
171+
... x = -1
172+
... items = [(x:=y) for y in range(3)]
173+
... return x
174+
>>> test_func()
175+
2
176+
177+
Comprehensions' scopes don't interact with each other:
178+
179+
>>> def test_func():
180+
... lst = list(range(3))
181+
... ret = [lambda: x for x in lst]
182+
... inc = [x + 1 for x in lst]
183+
... [x for x in inc]
184+
... return ret
185+
>>> test_func()[0]()
186+
2
187+
146188
"""
147189

148190

Lib/test/test_trace.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,7 @@ def test_trace_list_comprehension(self):
187187
firstlineno_called = get_firstlineno(traced_doubler)
188188
expected = {
189189
(self.my_py_filename, firstlineno_calling + 1): 1,
190-
# List comprehensions work differently in 3.x, so the count
191-
# below changed compared to 2.x.
192-
(self.my_py_filename, firstlineno_calling + 2): 12,
190+
(self.my_py_filename, firstlineno_calling + 2): 11,
193191
(self.my_py_filename, firstlineno_calling + 3): 1,
194192
(self.my_py_filename, firstlineno_called + 1): 10,
195193
}

0 commit comments

Comments
 (0)