Skip to content

Commit 3245fd7

Browse files
committed
gh-124697: Represent inlined comprehensions as subscopes in the symbol table
1 parent d557d64 commit 3245fd7

12 files changed

Lines changed: 359 additions & 267 deletions

File tree

Doc/library/symtable.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ Examining Symbol Tables
5757

5858
Used for the symbol table of a class.
5959

60+
.. attribute:: INLINED_COMPREHENSION
61+
:value: "inlined comprehension"
62+
63+
Used for the symbol table of a list, set or dict comprehension that
64+
is inlined into the enclosing code unit (see :pep:`709`). A symbol
65+
table of this type represents a sub-scope of the enclosing code unit's
66+
scope, and it does not correspond to a separate compilation unit.
67+
6068
The following members refer to different flavors of
6169
:ref:`annotation scopes <annotation-scopes>`.
6270

Include/internal/pycore_compile.h

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,8 @@ typedef struct {
6969
PyObject *u_varnames; /* local variables */
7070
PyObject *u_cellvars; /* cell variables */
7171
PyObject *u_freevars; /* free variables */
72-
PyObject *u_fasthidden; /* dict; keys are names that are fast-locals only
73-
temporarily within an inlined comprehension. When
74-
value is True, treat as fast-local. */
72+
PyObject *u_fasthidden; /* set of names that are fast-locals only
73+
temporarily within an inlined comprehension. */
7574

7675
Py_ssize_t u_argcount; /* number of arguments for block */
7776
Py_ssize_t u_posonlyargcount; /* number of positional only arguments for block */
@@ -155,7 +154,6 @@ int _PyCompile_ResolveNameop(struct _PyCompiler *c, PyObject *mangled, int scope
155154
_PyCompile_optype *optype, Py_ssize_t *arg);
156155

157156
int _PyCompile_IsInteractiveTopLevel(struct _PyCompiler *c);
158-
int _PyCompile_IsInInlinedComp(struct _PyCompiler *c);
159157
int _PyCompile_ScopeType(struct _PyCompiler *c);
160158
int _PyCompile_OptimizationLevel(struct _PyCompiler *c);
161159
int _PyCompile_LookupArg(struct _PyCompiler *c, PyCodeObject *co, PyObject *name);
@@ -179,16 +177,15 @@ enum {
179177

180178
typedef struct {
181179
PyObject *pushed_locals;
182-
PyObject *temp_symbols;
183-
PyObject *fast_hidden;
184180
_PyJumpTargetLabel cleanup;
181+
PySTEntryObject *saved_ste;
185182
} _PyCompile_InlinedComprehensionState;
186183

187-
int _PyCompile_TweakInlinedComprehensionScopes(struct _PyCompiler *c, _Py_SourceLocation loc,
188-
PySTEntryObject *entry,
189-
_PyCompile_InlinedComprehensionState *state);
190-
int _PyCompile_RevertInlinedComprehensionScopes(struct _PyCompiler *c, _Py_SourceLocation loc,
191-
_PyCompile_InlinedComprehensionState *state);
184+
int _PyCompile_EnterInlinedComprehensionScope(struct _PyCompiler *c, _Py_SourceLocation loc,
185+
PySTEntryObject *entry,
186+
_PyCompile_InlinedComprehensionState *state);
187+
int _PyCompile_ExitInlinedComprehensionScope(struct _PyCompiler *c, _Py_SourceLocation loc,
188+
_PyCompile_InlinedComprehensionState *state);
192189
int _PyCompile_AddDeferredAnnotation(struct _PyCompiler *c, stmt_ty s,
193190
PyObject **conditional_annotation_index);
194191
void _PyCompile_EnterConditionalBlock(struct _PyCompiler *c);

Include/internal/pycore_symtable.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ typedef enum _block_type {
3333
// i.e., a TypeVar, a TypeVarTuple or a ParamSpec object (the latter two
3434
// do not support a bound or a constraint tuple).
3535
TypeVariableBlock,
36+
// Comprehension which is inlined into the enclosing code unit (see PEP 709).
37+
// Represents a sub-scope of the enclosing code unit's scope rather than a
38+
// separate scope.
39+
InlinedComprehensionBlock,
3640
} _Py_block_ty;
3741

3842
typedef enum _comprehension_type {
@@ -119,7 +123,6 @@ typedef struct _symtable_entry {
119123
should be created */
120124
unsigned ste_needs_classdict : 1; /* for class scopes, true if a closure
121125
over the class dict should be created */
122-
unsigned ste_comp_inlined : 1; /* true if this comprehension is inlined */
123126
unsigned ste_comp_iter_target : 1; /* true if visiting comprehension target */
124127
unsigned ste_can_see_class_scope : 1; /* true if this block can see names bound in an
125128
enclosing class scope */
@@ -132,6 +135,7 @@ typedef struct _symtable_entry {
132135
int ste_comp_iter_expr; /* non-zero if visiting a comprehension range expression */
133136
_Py_SourceLocation ste_loc; /* source location of block */
134137
struct _symtable_entry *ste_annotation_block; /* symbol table entry for this entry's annotations */
138+
struct _symtable_entry *ste_parent; /* st entry for the enclosing block if this entry is a sub-scope, NULL otherwise */
135139
struct symtable *ste_table;
136140
} PySTEntryObject;
137141

Lib/symtable.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class SymbolTableType(StrEnum):
5656
TYPE_ALIAS = "type alias"
5757
TYPE_PARAMETERS = "type parameters"
5858
TYPE_VARIABLE = "type variable"
59+
INLINED_COMPREHENSION = "inlined comprehension"
5960

6061

6162
class SymbolTable:
@@ -98,6 +99,8 @@ def get_type(self):
9899
return SymbolTableType.TYPE_PARAMETERS
99100
if self._table.type == _symtable.TYPE_TYPE_VARIABLE:
100101
return SymbolTableType.TYPE_VARIABLE
102+
if self._table.type == _symtable.TYPE_INLINED_COMPREHENSION:
103+
return SymbolTableType.INLINED_COMPREHENSION
101104
assert False, f"unexpected type: {self._table.type}"
102105

103106
def get_id(self):

Lib/test/test_compiler_assemble.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ def complete_metadata(self, metadata, filename="myfile.py"):
1717
metadata.setdefault(key, key)
1818
for key in ['consts']:
1919
metadata.setdefault(key, [])
20-
for key in ['names', 'varnames', 'cellvars', 'freevars', 'fasthidden']:
20+
for key in ['names', 'varnames', 'cellvars', 'freevars']:
2121
metadata.setdefault(key, {})
22+
metadata.setdefault('fasthidden', None)
2223
for key in ['argcount', 'posonlyargcount', 'kwonlyargcount']:
2324
metadata.setdefault(key, 0)
2425
metadata.setdefault('firstlineno', 1)

Lib/test/test_symtable.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,11 @@ def test_symbol_repr(self):
426426
"<symbol 'T': LOCAL, DEF_LOCAL|DEF_TYPE_PARAM>")
427427

428428
st1 = symtable.symtable("[x for x in [1]]", "?", "exec")
429-
self.assertEqual(repr(st1.lookup("x")),
429+
self.assertEqual(repr(st1.get_children()[0].lookup("x")),
430430
"<symbol 'x': LOCAL, USE|DEF_LOCAL|DEF_COMP_ITER>")
431431

432432
st2 = symtable.symtable("[(lambda: x) for x in [1]]", "?", "exec")
433-
self.assertEqual(repr(st2.lookup("x")),
433+
self.assertEqual(repr(st2.get_children()[0].lookup("x")),
434434
"<symbol 'x': CELL, DEF_LOCAL|DEF_COMP_ITER|DEF_COMP_CELL>")
435435

436436
st3 = symtable.symtable("def f():\n"
@@ -502,6 +502,64 @@ def test_nested_genexpr(self):
502502
self.assertEqual(sorted(st.get_identifiers()), [".0", "y"])
503503
self.assertEqual(st.get_children(), [])
504504

505+
def test_inlined_comprehension_in_genexpr(self):
506+
st = symtable.symtable("([y for y in x] for x in a)", "?", "exec")
507+
self.assertEqual(len(st.get_children()), 1)
508+
st = st.get_children()[0]
509+
self.assertIs(st.get_type(), symtable.SymbolTableType.FUNCTION)
510+
self.assertEqual(st.get_name(), "<genexpr>")
511+
self.assertFalse(st.is_nested())
512+
self.assertEqual(sorted(st.get_identifiers()), [".0", "x"])
513+
children = st.get_children()
514+
self.assertEqual(len(children), 1)
515+
self.check_inlined_listcomp(children[0], ["y"], nested=True)
516+
517+
def check_inlined_listcomp(self, st, identifiers, *, nested, nchildren=0):
518+
self.assertIs(st.get_type(), symtable.SymbolTableType.INLINED_COMPREHENSION)
519+
self.assertEqual(st.get_name(), "<listcomp>")
520+
self.assertEqual(st.is_nested(), nested)
521+
self.assertEqual(sorted(st.get_identifiers()), identifiers)
522+
children = st.get_children()
523+
self.assertEqual(len(children), nchildren)
524+
return children
525+
526+
def check_nested_inlined_listcomp(self, outer, hoisted, outer_ids, inner_ids, *, nested):
527+
# Nested namespaces of inlined comprehensions are also hoisted into
528+
# the enclosing scope's children list.
529+
inner, = self.check_inlined_listcomp(
530+
outer, outer_ids, nested=nested, nchildren=1)
531+
self.check_inlined_listcomp(inner, inner_ids, nested=True)
532+
self.assertIs(hoisted, inner)
533+
534+
def test_inlined_comprehension(self):
535+
st = symtable.symtable("[x for x in [1]]", "?", "exec")
536+
self.assertEqual(sorted(st.get_identifiers()), [])
537+
children = st.get_children()
538+
self.assertEqual(len(children), 1)
539+
self.check_inlined_listcomp(children[0], ["x"], nested=False)
540+
541+
def test_inlined_nested_comprehension(self):
542+
st = symtable.symtable("[[y for y in x] for x in [1]]", "?", "exec")
543+
self.assertEqual(sorted(st.get_identifiers()), [])
544+
children = st.get_children()
545+
self.assertEqual(len(children), 2)
546+
self.check_nested_inlined_listcomp(
547+
children[0], children[1], ["x"], ["y"], nested=False)
548+
549+
def test_inlined_sibling_nested_comprehensions(self):
550+
st = symtable.symtable(
551+
"def f(): [[y for y in x] for x in [1]]; [[w for w in z] for z in [2]]",
552+
"?", "exec")
553+
f = find_block(st, "f")
554+
self.assertIs(f.get_type(), symtable.SymbolTableType.FUNCTION)
555+
self.assertEqual(sorted(f.get_identifiers()), [])
556+
children = f.get_children()
557+
self.assertEqual(len(children), 4)
558+
self.check_nested_inlined_listcomp(
559+
children[0], children[1], ["x"], ["y"], nested=True)
560+
self.check_nested_inlined_listcomp(
561+
children[2], children[3], ["z"], ["w"], nested=True)
562+
505563
def test__symtable_refleak(self):
506564
# Regression test for reference leak in PyUnicode_FSDecoder.
507565
# See https://github.com/python/cpython/issues/139748.

Modules/_testinternalcapi.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1357,13 +1357,16 @@ _testinternalcapi_assemble_code_object_impl(PyObject *module,
13571357
umd.u_cellvars = PyDict_GetItemString(metadata, "cellvars");
13581358
umd.u_freevars = PyDict_GetItemString(metadata, "freevars");
13591359
umd.u_fasthidden = PyDict_GetItemString(metadata, "fasthidden");
1360+
if (umd.u_fasthidden == Py_None) {
1361+
umd.u_fasthidden = NULL;
1362+
}
13601363

13611364
assert(PyDict_Check(umd.u_consts));
13621365
assert(PyDict_Check(umd.u_names));
13631366
assert(PyDict_Check(umd.u_varnames));
13641367
assert(PyDict_Check(umd.u_cellvars));
13651368
assert(PyDict_Check(umd.u_freevars));
1366-
assert(PyDict_Check(umd.u_fasthidden));
1369+
assert(umd.u_fasthidden == NULL || PySet_Check(umd.u_fasthidden));
13671370

13681371
umd.u_argcount = get_nonnegative_int_from_dict(metadata, "argcount");
13691372
umd.u_posonlyargcount = get_nonnegative_int_from_dict(metadata, "posonlyargcount");

Modules/symtablemodule.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ symtable_init_constants(PyObject *m)
144144
return -1;
145145
if (PyModule_AddIntConstant(m, "TYPE_TYPE_VARIABLE", TypeVariableBlock) < 0)
146146
return -1;
147+
if (PyModule_AddIntConstant(m, "TYPE_INLINED_COMPREHENSION", InlinedComprehensionBlock) < 0)
148+
return -1;
147149

148150
if (PyModule_AddIntMacro(m, LOCAL) < 0) return -1;
149151
if (PyModule_AddIntMacro(m, GLOBAL_EXPLICIT) < 0) return -1;

Python/assemble.c

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,15 @@ compute_localsplus_info(_PyCompile_CodeUnitMetadata *umd, int nlocalsplus,
520520

521521
_PyLocals_Kind kind = CO_FAST_LOCAL | argvarkinds[i].kind;
522522

523-
int has_key = PyDict_Contains(umd->u_fasthidden, k);
524-
RETURN_IF_ERROR(has_key);
525-
if (has_key) {
526-
kind |= CO_FAST_HIDDEN;
523+
if (umd->u_fasthidden != NULL) {
524+
int hidden = PySet_Contains(umd->u_fasthidden, k);
525+
RETURN_IF_ERROR(hidden);
526+
if (hidden) {
527+
kind |= CO_FAST_HIDDEN;
528+
}
527529
}
528530

529-
has_key = PyDict_Contains(umd->u_cellvars, k);
531+
int has_key = PyDict_Contains(umd->u_cellvars, k);
530532
RETURN_IF_ERROR(has_key);
531533
if (has_key) {
532534
kind |= CO_FAST_CELL;

Python/codegen.c

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3342,7 +3342,7 @@ codegen_nameop(compiler *c, location loc,
33423342
case COMPILE_OP_DEREF:
33433343
switch (ctx) {
33443344
case Load:
3345-
if (SYMTABLE_ENTRY(c)->ste_type == ClassBlock && !_PyCompile_IsInInlinedComp(c)) {
3345+
if (SYMTABLE_ENTRY(c)->ste_type == ClassBlock) {
33463346
op = LOAD_FROM_DICT_OR_DEREF;
33473347
// First load the locals
33483348
if (codegen_addop_noarg(INSTR_SEQUENCE(c), LOAD_LOCALS, loc) < 0) {
@@ -3395,8 +3395,9 @@ codegen_nameop(compiler *c, location loc,
33953395
case COMPILE_OP_NAME:
33963396
switch (ctx) {
33973397
case Load:
3398-
op = (SYMTABLE_ENTRY(c)->ste_type == ClassBlock
3399-
&& _PyCompile_IsInInlinedComp(c))
3398+
/* LOAD_NAME in a class reads the class dict; inlined comps must not. */
3399+
op = (SCOPE_TYPE(c) == COMPILE_SCOPE_CLASS
3400+
&& SYMTABLE_ENTRY(c)->ste_type == InlinedComprehensionBlock)
34003401
? LOAD_GLOBAL
34013402
: LOAD_NAME;
34023403
break;
@@ -4917,9 +4918,6 @@ codegen_push_inlined_comprehension_locals(compiler *c, location loc,
49174918
PySTEntryObject *comp,
49184919
_PyCompile_InlinedComprehensionState *state)
49194920
{
4920-
int in_class_block = (SYMTABLE_ENTRY(c)->ste_type == ClassBlock) &&
4921-
!_PyCompile_IsInInlinedComp(c);
4922-
PySTEntryObject *outer = SYMTABLE_ENTRY(c);
49234921
// iterate over names bound in the comprehension and ensure we isolate
49244922
// them from the outer scope as needed
49254923
PyObject *k, *v;
@@ -4930,11 +4928,7 @@ codegen_push_inlined_comprehension_locals(compiler *c, location loc,
49304928
RETURN_IF_ERROR(symbol);
49314929
long scope = SYMBOL_TO_SCOPE(symbol);
49324930

4933-
long outsymbol = _PyST_GetSymbol(outer, k);
4934-
RETURN_IF_ERROR(outsymbol);
4935-
long outsc = SYMBOL_TO_SCOPE(outsymbol);
4936-
4937-
if ((symbol & DEF_LOCAL && !(symbol & DEF_NONLOCAL)) || in_class_block) {
4931+
if ((symbol & DEF_LOCAL) && !(symbol & DEF_NONLOCAL)) {
49384932
// local names bound in comprehension must be isolated from
49394933
// outer scope; push existing value (which may be NULL if
49404934
// not defined) on stack
@@ -4949,15 +4943,17 @@ codegen_push_inlined_comprehension_locals(compiler *c, location loc,
49494943
// comprehension and restore the original one after
49504944
ADDOP_NAME(c, loc, LOAD_FAST_AND_CLEAR, k, varnames);
49514945
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-
}
4946+
ADDOP_NAME(c, loc, MAKE_CELL, k, cellvars);
49574947
}
49584948
if (PyList_Append(state->pushed_locals, k) < 0) {
49594949
return ERROR;
49604950
}
4951+
if (METADATA(c)->u_fasthidden != NULL) {
4952+
/* For Module/Class scopes, assemble needs to set CO_FAST_HIDDEN on these names */
4953+
if (PySet_Add(METADATA(c)->u_fasthidden, k) < 0) {
4954+
return ERROR;
4955+
}
4956+
}
49614957
}
49624958
}
49634959
if (state->pushed_locals) {
@@ -4986,7 +4982,7 @@ push_inlined_comprehension_state(compiler *c, location loc,
49864982
_PyCompile_InlinedComprehensionState *state)
49874983
{
49884984
RETURN_IF_ERROR(
4989-
_PyCompile_TweakInlinedComprehensionScopes(c, loc, comp, state));
4985+
_PyCompile_EnterInlinedComprehensionScope(c, loc, comp, state));
49904986
RETURN_IF_ERROR(
49914987
codegen_push_inlined_comprehension_locals(c, loc, comp, state));
49924988
return SUCCESS;
@@ -5044,7 +5040,7 @@ pop_inlined_comprehension_state(compiler *c, location loc,
50445040
_PyCompile_InlinedComprehensionState *state)
50455041
{
50465042
RETURN_IF_ERROR(codegen_pop_inlined_comprehension_locals(c, loc, state));
5047-
RETURN_IF_ERROR(_PyCompile_RevertInlinedComprehensionScopes(c, loc, state));
5043+
RETURN_IF_ERROR(_PyCompile_ExitInlinedComprehensionScope(c, loc, state));
50485044
return SUCCESS;
50495045
}
50505046

@@ -5054,13 +5050,13 @@ codegen_comprehension(compiler *c, expr_ty e, int type,
50545050
expr_ty val, bool avoid_creation)
50555051
{
50565052
PyCodeObject *co = NULL;
5057-
_PyCompile_InlinedComprehensionState inline_state = {NULL, NULL, NULL, NO_LABEL};
5053+
_PyCompile_InlinedComprehensionState inline_state = {NULL, NO_LABEL, NULL};
50585054
comprehension_ty outermost;
50595055
PySTEntryObject *entry = _PySymtable_Lookup(SYMTABLE(c), (void *)e);
50605056
if (entry == NULL) {
50615057
goto error;
50625058
}
5063-
int is_inlined = entry->ste_comp_inlined;
5059+
int is_inlined = (entry->ste_type == InlinedComprehensionBlock);
50645060
int is_async_comprehension = entry->ste_coroutine;
50655061

50665062
location loc = LOC(e);
@@ -5069,7 +5065,7 @@ codegen_comprehension(compiler *c, expr_ty e, int type,
50695065
IterStackPosition iter_state;
50705066
if (is_inlined) {
50715067
VISIT(c, expr, outermost->iter);
5072-
if (push_inlined_comprehension_state(c, loc, entry, &inline_state)) {
5068+
if (push_inlined_comprehension_state(c, loc, entry, &inline_state) < 0) {
50735069
goto error;
50745070
}
50755071
iter_state = ITERABLE_ON_STACK;
@@ -5140,8 +5136,8 @@ codegen_comprehension(compiler *c, expr_ty e, int type,
51405136
}
51415137

51425138
if (is_inlined) {
5143-
if (pop_inlined_comprehension_state(c, loc, &inline_state)) {
5144-
goto error;
5139+
if (pop_inlined_comprehension_state(c, loc, &inline_state) < 0) {
5140+
goto error_in_scope;
51455141
}
51465142
return SUCCESS;
51475143
}
@@ -5181,15 +5177,18 @@ codegen_comprehension(compiler *c, expr_ty e, int type,
51815177

51825178
return SUCCESS;
51835179
error_in_scope:
5184-
if (!is_inlined) {
5180+
if (is_inlined) {
5181+
if (inline_state.saved_ste != NULL) {
5182+
pop_inlined_comprehension_state(c, loc, &inline_state);
5183+
}
5184+
}
5185+
else {
51855186
_PyCompile_ExitScope(c);
51865187
}
51875188
error:
51885189
Py_XDECREF(co);
51895190
Py_XDECREF(entry);
51905191
Py_XDECREF(inline_state.pushed_locals);
5191-
Py_XDECREF(inline_state.temp_symbols);
5192-
Py_XDECREF(inline_state.fast_hidden);
51935192
return ERROR;
51945193
}
51955194

0 commit comments

Comments
 (0)