Skip to content

Commit 2a0c9b0

Browse files
committed
gh-124697: avoid duplicate names in the same frame
1 parent 9d22a53 commit 2a0c9b0

10 files changed

Lines changed: 268 additions & 52 deletions

File tree

‎Doc/library/dis.rst‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,26 @@ but are replaced by real opcodes or removed before bytecode is generated.
20032003
.. versionchanged:: 3.13
20042004
This opcode is now a pseudo-instruction.
20052005

2006+
.. opcode:: LOAD_CLOSURE_AND_CLEAR (i)
2007+
2008+
Pushes a reference to the cell contained in slot ``i`` of the "fast locals"
2009+
storage and clears that slot. Used to isolate an inlined comprehension
2010+
local that reuses an enclosing free variable.
2011+
2012+
Note that ``LOAD_CLOSURE_AND_CLEAR`` is replaced with
2013+
``LOAD_FAST_AND_CLEAR`` in the assembler.
2014+
2015+
.. versionadded:: next
2016+
2017+
.. opcode:: STORE_CLOSURE (i)
2018+
2019+
Stores the TOS into the cell slot ``i`` of the "fast locals" storage.
2020+
Used to restore a cell saved by ``LOAD_CLOSURE_AND_CLEAR``.
2021+
2022+
Note that ``STORE_CLOSURE`` is replaced with ``STORE_FAST`` in the assembler.
2023+
2024+
.. versionadded:: next
2025+
20062026

20072027
.. _opcode_collections:
20082028

‎Include/internal/pycore_opcode_metadata.h‎

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

‎Include/opcode_ids.h‎

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

‎InternalDocs/inlined_comprehensions.md‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,35 @@ The walk stops at a class: nested scopes do not see class locals.
8686
Class-closure names that would otherwise be free through a class become
8787
`GLOBAL_IMPLICIT`.
8888

89+
If the inlined name is `LOCAL` or `CELL` but the nearest non-inlined
90+
enclosing table has it as `FREE`, resolve it as `FREE` so the
91+
comprehension reuses that localsplus slot. `compiler_cellvars()` also
92+
skips adding those child cells, which would otherwise create a second
93+
same-named entry.
94+
8995
### Isolating iteration variables
9096

9197
`codegen_push_inlined_comprehension_locals()` in
92-
[`Python/codegen.c`](../Python/codegen.c) isolates names bound in the
93-
comprehension:
94-
95-
* `LOAD_FAST_AND_CLEAR` saves the enclosing value (possibly `NULL`) and
96-
clears the slot.
97-
* `MAKE_CELL` runs if the name is a cell for this comprehension.
98+
[`Python/codegen.c`](../Python/codegen.c) isolates each name bound in
99+
the comprehension on one of two paths:
100+
101+
Reuse an enclosing free (`_PyCompile_GetRefType()` is `FREE`):
102+
103+
* `LOAD_CLOSURE_AND_CLEAR` saves the enclosing cell.
104+
* `MAKE_CELL` always runs, so the slot holds a fresh empty cell.
105+
The comprehension then uses `DEREF`; it must not store into the
106+
enclosing cell.
107+
* Restore uses `STORE_CLOSURE`.
108+
* Those pseudo instructions carry a cell/free index so
109+
`fix_cell_offsets` can remap them; they become `LOAD_FAST_AND_CLEAR`
110+
/ `STORE_FAST` in the assembler.
111+
112+
Own fast-local slot (everything else):
113+
114+
* `LOAD_FAST_AND_CLEAR` saves the enclosing value (possibly `NULL`)
115+
and clears the slot.
116+
* `MAKE_CELL` runs only if the name is a cell for this comprehension.
117+
* Restore uses `STORE_FAST_MAYBE_NULL`.
98118
* In module and class units the name is added to `u_fasthidden` so
99119
assemble can set `CO_FAST_HIDDEN`.
100120

@@ -105,10 +125,13 @@ or `finally` sees the original values.
105125
Runtime
106126
-------
107127

108-
An inlined comprehension cell can share a localsplus name with an
109-
enclosing free variable (for example `[lambda: x for x in x]` inside a
110-
nested function). `FrameLocalsProxy` keys, values, items, and `len`
111-
keep the first slot of each name so they agree with `getitem`.
128+
An inlined comprehension local that collides with an enclosing free
129+
(for example `[x for x in x]` or `[lambda: x for x in x]` inside a
130+
nested function) reuses the free slot. Isolation saves that cell and
131+
installs a temporary one so `STORE_DEREF` does not change the value
132+
seen by existing closures; lambdas that capture the iteration variable
133+
share the temporary cell. After the comprehension, the original cell
134+
is restored.
112135

113136
Source
114137
------
@@ -128,5 +151,3 @@ Source
128151
`InlinedComprehensionBlock`
129152
* [`Include/internal/pycore_compile.h`](../Include/internal/pycore_compile.h):
130153
`_PyCompile_InlinedComprehensionState`
131-
* [`Objects/frameobject.c`](../Objects/frameobject.c):
132-
`FrameLocalsProxy` duplicate-name handling

‎Lib/_opcode_metadata.py‎

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

‎Lib/test/test_listcomps.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,49 @@ def inner():
316316
outputs = {"z": [2, 2], "w": 99}
317317
self._check_in_scopes(code, outputs)
318318

319+
def test_inlined_comp_reuses_enclosing_free_slot(self):
320+
# An inlined local that collides with an enclosing free reuses that
321+
# free slot instead of adding a second same-named localsplus entry.
322+
def outer(x):
323+
def inner():
324+
return [x for x in x]
325+
return inner
326+
code = outer([1]).__code__
327+
self.assertEqual(code.co_varnames, ())
328+
self.assertEqual(code.co_cellvars, ())
329+
self.assertEqual(code.co_freevars, ('x',))
330+
331+
def test_inlined_comp_cell_reuses_enclosing_free_slot(self):
332+
def outer(x):
333+
def inner():
334+
return [lambda: x for x in x]
335+
return inner
336+
code = outer([1]).__code__
337+
self.assertEqual(code.co_varnames, ())
338+
self.assertEqual(code.co_cellvars, ())
339+
self.assertEqual(code.co_freevars, ('x',))
340+
341+
def test_nested_inlined_comp_reuses_enclosing_free_slot(self):
342+
def outer(x):
343+
def inner():
344+
return [[x for _ in (0,)] for x in x]
345+
return inner
346+
code = outer([1]).__code__
347+
self.assertNotIn('x', code.co_varnames)
348+
self.assertNotIn('x', code.co_cellvars)
349+
self.assertEqual(code.co_freevars, ('x',))
350+
351+
def test_inlined_comp_exception_restores_enclosing_free(self):
352+
def outer(x):
353+
def inner():
354+
try:
355+
[1 / 0 for x in x]
356+
except ZeroDivisionError:
357+
pass
358+
return x
359+
return inner()
360+
self.assertEqual(outer([1, 2]), [1, 2])
361+
319362
def test_free_inner_cell_outer(self):
320363
code = """
321364
g = 2
@@ -944,6 +987,36 @@ def inner():
944987
{"snaps": [1, 2], "vals": [2, 2], "consistent": [True, True]},
945988
ns={"sys": sys}, scopes=["module", "function"])
946989

990+
def test_frame_locals_comp_local_and_enclosing_free(self):
991+
# Same-name collision without a lambda: the inlined local reuses the
992+
# enclosing free slot. f_locals keys must still be unique.
993+
code = """
994+
def outer(x):
995+
def inner():
996+
return [(dict(**sys._getframe().f_locals),
997+
len(sys._getframe().f_locals),
998+
list(sys._getframe().f_locals.keys()),
999+
list(sys._getframe().f_locals.values()),
1000+
list(sys._getframe().f_locals.items()),
1001+
dict(sys._getframe().f_locals.items()))
1002+
for x in x]
1003+
return inner()
1004+
result = outer([1, 2])
1005+
snaps = [d['x'] for d, *_ in result]
1006+
consistent = []
1007+
for d, n, ks, vs, it, d_items in result:
1008+
consistent.append(
1009+
n == len(ks) == len(vs) == len(it)
1010+
and ks.count('x') == 1
1011+
and d == d_items == dict(zip(ks, vs))
1012+
)
1013+
"""
1014+
import sys
1015+
self._check_in_scopes(
1016+
code,
1017+
{"snaps": [1, 2], "consistent": [True, True]},
1018+
ns={"sys": sys}, scopes=["module", "function"])
1019+
9471020
def test_frame_locals_nested_comp_cell_and_enclosing_free(self):
9481021
# Stress a nested inlined shape where a comp cell and enclosing free
9491022
# share a name; all f_locals views must stay consistent.

‎Python/bytecodes.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,20 @@ dummy_func(
270270
LOAD_FAST,
271271
};
272272

273+
/* Like LOAD_FAST_AND_CLEAR, but the oparg is a cell/free index
274+
* remapped in fix_cell_offsets. Used to isolate an inlined
275+
* comprehension local that reuses an enclosing free slot. */
276+
pseudo(LOAD_CLOSURE_AND_CLEAR, (-- unused)) = {
277+
LOAD_FAST_AND_CLEAR,
278+
};
279+
280+
/* Like STORE_FAST, but the oparg is a cell/free index remapped
281+
* in fix_cell_offsets. Restores the cell saved by
282+
* LOAD_CLOSURE_AND_CLEAR. */
283+
pseudo(STORE_CLOSURE, (unused --)) = {
284+
STORE_FAST,
285+
};
286+
273287
inst(LOAD_FAST_CHECK, (-- value)) {
274288
_PyStackRef value_s = GETLOCAL(oparg);
275289
if (PyStackRef_IsNull(value_s)) {

0 commit comments

Comments
 (0)