Skip to content

Commit 65116d5

Browse files
committed
gh-156664: Keep comprehension cells separate from free variables
1 parent 829258e commit 65116d5

4 files changed

Lines changed: 46 additions & 18 deletions

File tree

Lib/test/test_listcomps.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,41 @@ def inner():
775775

776776
self.assertEqual(g(), 3)
777777

778+
def captured_then_sibling():
779+
funcs = [lambda: x for x in [1]]
780+
return funcs[0](), [x for _ in [1]]
781+
782+
self.assertEqual(captured_then_sibling(), (1, [3]))
783+
784+
def captured_then_nested_function():
785+
funcs = [lambda: x for x in [1]]
786+
787+
def inner():
788+
return x
789+
790+
return funcs[0](), inner()
791+
792+
self.assertEqual(captured_then_nested_function(), (1, 3))
793+
794+
def captured_then_generator_expression():
795+
funcs = [lambda: x for x in [1]]
796+
return funcs[0](), list(x for _ in [1])
797+
798+
self.assertEqual(captured_then_generator_expression(), (1, [3]))
799+
800+
def raises_after_one():
801+
yield 1
802+
raise RuntimeError
803+
804+
def captured_then_exception():
805+
funcs = []
806+
try:
807+
[funcs.append(lambda: x) for x in raises_after_one()]
808+
except RuntimeError:
809+
return funcs[0](), [x for _ in [1]]
810+
811+
self.assertEqual(captured_then_exception(), (1, [3]))
812+
778813
def test_exception_locations(self):
779814
# The location of an exception raised from __init__ or
780815
# __next__ should be the iterator expression

Python/codegen.c

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4919,7 +4919,6 @@ codegen_push_inlined_comprehension_locals(compiler *c, location loc,
49194919
{
49204920
int in_class_block = (SYMTABLE_ENTRY(c)->ste_type == ClassBlock) &&
49214921
!_PyCompile_IsInInlinedComp(c);
4922-
PySTEntryObject *outer = SYMTABLE_ENTRY(c);
49234922
// iterate over names bound in the comprehension and ensure we isolate
49244923
// them from the outer scope as needed
49254924
PyObject *k, *v;
@@ -4930,10 +4929,6 @@ codegen_push_inlined_comprehension_locals(compiler *c, location loc,
49304929
RETURN_IF_ERROR(symbol);
49314930
long scope = SYMBOL_TO_SCOPE(symbol);
49324931

4933-
long outsymbol = _PyST_GetSymbol(outer, k);
4934-
RETURN_IF_ERROR(outsymbol);
4935-
long outsc = SYMBOL_TO_SCOPE(outsymbol);
4936-
49374932
if ((symbol & DEF_LOCAL && !(symbol & DEF_NONLOCAL)) || in_class_block) {
49384933
// local names bound in comprehension must be isolated from
49394934
// outer scope; push existing value (which may be NULL if
@@ -4949,11 +4944,7 @@ codegen_push_inlined_comprehension_locals(compiler *c, location loc,
49494944
// comprehension and restore the original one after
49504945
ADDOP_NAME(c, loc, LOAD_FAST_AND_CLEAR, k, varnames);
49514946
if (scope == CELL) {
4952-
if (outsc == FREE) {
4953-
ADDOP_NAME(c, loc, MAKE_CELL, k, freevars);
4954-
} else {
4955-
ADDOP_NAME(c, loc, MAKE_CELL, k, cellvars);
4956-
}
4947+
ADDOP_NAME(c, loc, MAKE_CELL, k, cellvars);
49574948
}
49584949
if (PyList_Append(state->pushed_locals, k) < 0) {
49594950
return ERROR;

Python/compile.c

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,12 +1080,9 @@ _PyCompile_TweakInlinedComprehensionScopes(compiler *c, location loc,
10801080
// we need to temporarily handle it with the right scope while
10811081
// compiling the comprehension. If it's free in the comprehension
10821082
// scope, no special handling; it should be handled the same as the
1083-
// enclosing scope. (If it's free in outer scope and cell in inner
1084-
// scope, we can't treat it as both cell and free in the same function,
1085-
// but treating it as free throughout is fine; it's *_DEREF
1086-
// either way.)
1087-
if ((scope != outsc && scope != FREE && !(scope == CELL && outsc == FREE))
1088-
|| in_class_block) {
1083+
// enclosing scope. A name that is a cell in the comprehension and free
1084+
// outside it uses separate cell and free-variable slots.
1085+
if ((scope != outsc && scope != FREE) || in_class_block) {
10891086
if (state->temp_symbols == NULL) {
10901087
state->temp_symbols = PyDict_New();
10911088
if (state->temp_symbols == NULL) {

Python/symtable.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ analyze_cells(PyObject *scopes, PyObject *free, PyObject *inlined_cells,
945945
if (scope == -1 && PyErr_Occurred()) {
946946
goto error;
947947
}
948-
if (scope != LOCAL)
948+
if (scope != LOCAL && scope != CELL)
949949
continue;
950950
int contains = PySet_Contains(free, name);
951951
if (contains < 0) {
@@ -966,7 +966,7 @@ analyze_cells(PyObject *scopes, PyObject *free, PyObject *inlined_cells,
966966
continue;
967967
}
968968
}
969-
else {
969+
else if (scope == LOCAL) {
970970
contains = PySet_Contains(inlined_cells, name);
971971
if (contains < 0) {
972972
goto error;
@@ -975,6 +975,11 @@ analyze_cells(PyObject *scopes, PyObject *free, PyObject *inlined_cells,
975975
continue;
976976
}
977977
}
978+
if (scope == CELL) {
979+
// Retain a cell copied from an inlined comprehension if no child
980+
// needs the same name as a free variable.
981+
continue;
982+
}
978983
/* Replace LOCAL with CELL for this name, and remove
979984
from free. It is safe to replace the value of name
980985
in the dict, because it will not cause a resize.

0 commit comments

Comments
 (0)