Skip to content

Commit a9d42dc

Browse files
authored
gh-124697: Represent inlined comprehensions as subscopes in the symbol table (#156819)
1 parent 2f5791d commit a9d42dc

20 files changed

Lines changed: 1187 additions & 375 deletions

‎Doc/library/symtable.rst‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ 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+
68+
.. versionadded:: next
69+
6070
The following members refer to different flavors of
6171
:ref:`annotation scopes <annotation-scopes>`.
6272

‎Doc/whatsnew/3.16.rst‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,16 @@ symtable
573573
like the builtin :func:`compile`.
574574
(Contributed by Serhiy Storchaka in :gh:`153844`.)
575575

576+
* Inlined list, set and dict comprehensions (:pep:`709`) are now represented
577+
as their own symbol table entries, of type
578+
:attr:`~symtable.SymbolTableType.INLINED_COMPREHENSION`. Each such entry is
579+
a lexical child of the enclosing scope and records the comprehension's own
580+
locals, cells, and free names. It does not correspond to a separate
581+
compilation unit. For names loaded only inside an inlined list, set, or
582+
dict comprehension, :meth:`symtable.Symbol.is_referenced` can now return
583+
``True`` on the enclosing function.
584+
(Contributed by Irit Katriel in :gh:`124697`.)
585+
576586

577587
tkinter
578588
-------

‎Include/internal/pycore_compile.h‎

Lines changed: 9 additions & 12 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 */
@@ -152,11 +151,10 @@ PyObject *_PyCompile_MaybeMangle(struct _PyCompiler *c, PyObject *name);
152151
int _PyCompile_MaybeAddStaticAttributeToClass(struct _PyCompiler *c, expr_ty e);
153152
int _PyCompile_GetRefType(struct _PyCompiler *c, PyObject *name);
154153
int _PyCompile_LookupCellvar(struct _PyCompiler *c, PyObject *name);
155-
int _PyCompile_ResolveNameop(struct _PyCompiler *c, PyObject *mangled, int scope,
154+
int _PyCompile_ResolveNameop(struct _PyCompiler *c, PyObject *mangled,
156155
_PyCompile_optype *optype, Py_ssize_t *arg);
157156

158157
int _PyCompile_IsInteractiveTopLevel(struct _PyCompiler *c);
159-
int _PyCompile_IsInInlinedComp(struct _PyCompiler *c);
160158
int _PyCompile_ScopeType(struct _PyCompiler *c);
161159
int _PyCompile_OptimizationLevel(struct _PyCompiler *c);
162160
int _PyCompile_LookupArg(struct _PyCompiler *c, PyCodeObject *co, PyObject *name);
@@ -180,16 +178,15 @@ enum {
180178

181179
typedef struct {
182180
PyObject *pushed_locals;
183-
PyObject *temp_symbols;
184-
PyObject *fast_hidden;
185181
_PyJumpTargetLabel cleanup;
182+
PySTEntryObject *saved_ste;
186183
} _PyCompile_InlinedComprehensionState;
187184

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

‎Include/internal/pycore_symtable.h‎

Lines changed: 6 additions & 2 deletions
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+
// List/set/dict comprehension inlined into the enclosing compilation unit
37+
// (PEP 709). Lexical child of that unit, not a separate code object.
38+
// See InternalDocs/inlined_comprehensions.md.
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

@@ -142,6 +146,7 @@ extern PyTypeObject PySTEntry_Type;
142146
extern long _PyST_GetSymbol(PySTEntryObject *, PyObject *);
143147
extern int _PyST_GetScope(PySTEntryObject *, PyObject *);
144148
extern int _PyST_IsFunctionLike(PySTEntryObject *);
149+
extern int _PyST_IsClassClosureName(PyObject *);
145150

146151
extern struct symtable* _PySymtable_Build(
147152
struct _mod *mod,
@@ -172,7 +177,6 @@ _Py_IsPrivateName(PyObject *);
172177
#define DEF_ANNOT (2<<7) /* this name is annotated */
173178
#define DEF_COMP_ITER (2<<8) /* this name is a comprehension iteration variable */
174179
#define DEF_TYPE_PARAM (2<<9) /* this name is a type parameter */
175-
#define DEF_COMP_CELL (2<<10) /* this name is a cell in an inlined comprehension */
176180

177181
#define DEF_BOUND (DEF_LOCAL | DEF_PARAM | DEF_IMPORT)
178182

‎InternalDocs/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Compiling Python Source Code
2323

2424
- [Compiler Design](compiler.md)
2525

26+
- [Inlined comprehensions](inlined_comprehensions.md)
27+
2628
- [Changing Python's Grammar](changing_grammar.md)
2729

2830
Runtime Objects

‎InternalDocs/compiler.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,10 @@ AST node type). Next, the AST tree is walked with the various code blocks that
354354
delineate the reach of a local variable as blocks are entered and exited using
355355
`symtable_enter_block()` and `symtable_exit_block()`, respectively.
356356

357+
See [Inlined comprehensions](inlined_comprehensions.md) for how list, set,
358+
and dict comprehensions are represented as lexical subscopes of the
359+
enclosing unit.
360+
357361
Once the symbol table is created, the `AST` is transformed by `compiler_codegen()`
358362
in [Python/compile.c](../Python/compile.c) into a sequence of pseudo instructions.
359363
These are similar to bytecode, but in some cases they are more abstract, and are
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
Inlined comprehensions
2+
======================
3+
4+
Since [PEP 709](https://peps.python.org/pep-0709/), list, set, and dict
5+
comprehensions are compiled into the enclosing compilation unit instead of
6+
creating a nested function and calling it. Generator expressions are not
7+
inlined; they still become their own code object.
8+
9+
The resulting bytecode lives in the enclosing unit, but the comprehension
10+
still has its own locals: iteration variables must not leak into, or
11+
overwrite, names in the enclosing scope. The symbol table models that as a
12+
nested lexical scope; codegen then emits the comprehension inlined into its
13+
containing compilation unit.
14+
15+
Which comprehensions are inlined
16+
--------------------------------
17+
18+
`symtable_handle_comprehension()` in
19+
[`Python/symtable.c`](../Python/symtable.c) inlines a comprehension when it
20+
is not a generator expression and the current block cannot see class scope
21+
(`!ste_can_see_class_scope`). Annotation scopes that can see a class keep
22+
the historical nested-function compilation so class-local names are not
23+
treated as comprehension locals.
24+
25+
The outermost iterator expression is always evaluated in the enclosing
26+
scope. The rest of the comprehension (targets, `if` clauses, inner
27+
generators, and the element/value expressions) is visited in the
28+
comprehension's own block.
29+
30+
Symbol table
31+
------------
32+
33+
An inlined comprehension gets an `InlinedComprehensionBlock` entry
34+
([`pycore_symtable.h`](../Include/internal/pycore_symtable.h)). That entry
35+
is a child of the enclosing block, with `ste_parent` pointing at the
36+
enclosing `PySTEntryObject`. It is a lexical subscope, not a compilation
37+
unit: there is no separate code object, `co_consts` entry, or compiler
38+
scope for it.
39+
40+
Uses and bindings inside the comprehension are recorded on that child
41+
table. Because those loads are in the enclosing compilation unit, a
42+
`FREE` use is also marked `USE` on the parent table. For
43+
`def inner(): return [x for y in ()]`, both `inner.lookup("x")` and the
44+
inlined child's lookup are `FREE|USE`.
45+
46+
### Analysis
47+
48+
`analyze_block()` records this block's declarations, analyzes children,
49+
then classifies uses. `finalize_inlined_comprehension()` copies
50+
`USE` from an inlined child onto the parent before that second pass, and
51+
drops inlined-only frees so `analyze_cells()` does not promote those
52+
names to cells.
53+
54+
A name that is `FREE` in the comprehension and bound in the parent is
55+
dropped from the parent's free set unless:
56+
57+
* a real nested unit (function, lambda, or genexp) still needs it as a
58+
cell, or
59+
* a sibling nested scope already marked it free.
60+
61+
That keeps iteration variables as fast locals when they are only used by
62+
nested inlined comprehensions. A nested lambda that captures the name
63+
still forces a cell on the binding comprehension.
64+
65+
Class-closure names (`__class__` and friends) loaded from an inlined
66+
comprehension do not require a class cell unless a nested function,
67+
lambda, or genexp captures them. Compile treats the inlined loads as
68+
implicit globals. `is_free_in_any_child()` walks through inlined children
69+
and only counts `FREE` on non-inlined descendants.
70+
71+
Compiler
72+
--------
73+
74+
Codegen stays in the enclosing compiler unit. Around the inlined region,
75+
`_PyCompile_EnterInlinedComprehensionScope()` /
76+
`_PyCompile_ExitInlinedComprehensionScope()` in
77+
[`Python/compile.c`](../Python/compile.c) swap `c->u->u_ste` so name
78+
lookup uses the comprehension's symbol table. The saved `u_ste` is
79+
restored on both success and error.
80+
81+
`_PyCompile_ResolveNameop()` calls `compiler_resolve_inlined_free()`,
82+
which walks `ste_parent` while the current table is inlined and the name
83+
is `FREE` or missing (scope `0`). Missing names include loads synthesized
84+
by codegen, such as the implicit receiver for zero-argument `super()`.
85+
The walk stops at a class: nested scopes do not see class locals.
86+
Class-closure names that would otherwise be free through a class become
87+
`GLOBAL_IMPLICIT`.
88+
89+
### Isolating iteration variables
90+
91+
`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+
* In module and class units the name is added to `u_fasthidden` so
99+
assemble can set `CO_FAST_HIDDEN`.
100+
101+
A `SETUP_FINALLY` / `COMPILE_FBLOCK_INLINED_COMPREHENSION` handler
102+
restores those slots if the comprehension raises, so an enclosing `except`
103+
or `finally` sees the original values.
104+
105+
Runtime
106+
-------
107+
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`.
112+
113+
Source
114+
------
115+
116+
* [`Python/symtable.c`](../Python/symtable.c):
117+
`symtable_handle_comprehension()`, `analyze_block()`,
118+
`finalize_inlined_comprehension()`, `is_free_in_any_child()`
119+
* [`Python/compile.c`](../Python/compile.c):
120+
`compiler_resolve_inlined_free()`,
121+
`_PyCompile_EnterInlinedComprehensionScope()`,
122+
`compiler_cellvars()`
123+
* [`Python/codegen.c`](../Python/codegen.c):
124+
`codegen_comprehension()`,
125+
`push_inlined_comprehension_state()`,
126+
`codegen_push_inlined_comprehension_locals()`
127+
* [`Include/internal/pycore_symtable.h`](../Include/internal/pycore_symtable.h):
128+
`InlinedComprehensionBlock`
129+
* [`Include/internal/pycore_compile.h`](../Include/internal/pycore_compile.h):
130+
`_PyCompile_InlinedComprehensionState`
131+
* [`Objects/frameobject.c`](../Objects/frameobject.c):
132+
`FrameLocalsProxy` duplicate-name handling

‎Lib/symtable.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
DEF_NONLOCAL, DEF_LOCAL,
88
DEF_PARAM, DEF_TYPE_PARAM, DEF_FREE_CLASS,
99
DEF_IMPORT, DEF_BOUND, DEF_ANNOT,
10-
DEF_COMP_ITER, DEF_COMP_CELL,
10+
DEF_COMP_ITER,
1111
SCOPE_OFF, SCOPE_MASK,
1212
FREE, LOCAL, GLOBAL_IMPLICIT, GLOBAL_EXPLICIT, CELL
1313
)
@@ -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):
@@ -151,8 +154,10 @@ def lookup(self, name):
151154
flags = self._table.symbols[name]
152155
namespaces = self.__check_children(name)
153156
module_scope = (self._table.name == "top")
157+
inlined = (self._table.type == _symtable.TYPE_INLINED_COMPREHENSION)
154158
sym = self._symbols[name] = Symbol(name, flags, namespaces,
155-
module_scope=module_scope)
159+
module_scope=module_scope,
160+
inlined_comprehension=inlined)
156161
return sym
157162

158163
def get_symbols(self):
@@ -246,12 +251,14 @@ class Class(SymbolTable):
246251

247252
class Symbol:
248253

249-
def __init__(self, name, flags, namespaces=None, *, module_scope=False):
254+
def __init__(self, name, flags, namespaces=None, *, module_scope=False,
255+
inlined_comprehension=False):
250256
self.__name = name
251257
self.__flags = flags
252258
self.__scope = _get_scope(flags)
253259
self.__namespaces = namespaces or ()
254260
self.__module_scope = module_scope
261+
self.__inlined_comprehension = inlined_comprehension
255262

256263
def __repr__(self):
257264
flags_str = '|'.join(self._flags_str())
@@ -345,7 +352,7 @@ def is_comp_iter(self):
345352
def is_comp_cell(self):
346353
"""Return *True* if the symbol is a cell in an inlined comprehension.
347354
"""
348-
return bool(self.__flags & DEF_COMP_CELL)
355+
return self.is_cell() and self.__inlined_comprehension
349356

350357
def is_namespace(self):
351358
"""Returns *True* if name binding introduces new namespace.

‎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)

0 commit comments

Comments
 (0)