gh-124697: Represent inlined comprehensions as subscopes in the symbol table - #156819
gh-124697: Represent inlined comprehensions as subscopes in the symbol table#156819iritkatriel wants to merge 21 commits into
Conversation
Co-authored-by: Cursor <[email protected]>
…l tables Co-authored-by: Cursor <[email protected]>
Documentation build overview
53 files changed ·
|
carljm
left a comment
There was a problem hiding this comment.
This looks great! Thank you for working on this ❤️
JelleZijlstra
left a comment
There was a problem hiding this comment.
Very nice! This also fixes #121377 and #156664, but doesn't add tests for their reproducers. Can you add those tests?
I have a PR for the latter almost ready at #156691, but I'm happy to retire it in favor of this PR. @carljm @iritkatriel what do you think about backporting either my change or others? This PR feels a bit much to backport, but we may want to backport fixes for some of the bugs. 156691 is also a somewhat invasive change though.
Codex found that this now fails:
import sys
def outer(x):
def inner():
return [(lambda: x, dict(**sys._getframe().f_locals))
for x in x]
return inner()
outer([1])With TypeError: dict() got multiple values for keyword argument 'x'.. #156691 has a larger change to framelocalsproxy to deal with this sort of thing; you may want to incorporate its approach.
|
Another relevant bug is #156091 but that one still crashes under this PR. |
|
I'd be cautious about backporting an invasive fix for #156664. It's clearly a rarely-encountered edge case, given that nobody other than PyPy's test suite discovered the bug since 3.12, and the risk of introducing new bugs in an invasive fix seems high (particularly without this refactor in place.) I think "better the bugs you know than the ones you don't" applies in this case. |
|
Thanks. I pushed a fix for the frame locals bug. I have a fix for #156691 too, but it can go in a different PR because it's an orthogonal problem. |
carljm
left a comment
There was a problem hiding this comment.
This version looks like a major improvement! Codex did find a few issues.
| if (found < 0) { | ||
| goto error; | ||
| } | ||
| if (found) { |
There was a problem hiding this comment.
I think if we do this, we should apply the same deduplication to items(), values(), and __len__(). With the separate comprehension-cell and enclosing-free slots, these now disagree with keys():
import sys
def outer(x):
def inner():
return [(lambda: x,
len(sys._getframe().f_locals),
sys._getframe().f_locals.keys(),
sys._getframe().f_locals.items())
for x in x]
return inner()[0][1:]
print(outer([1, 2]))Main returns (1, ['x'], [('x', 1)]); this revision returns (2, ['x'], [('x', 1), ('x', [1, 2])]). values() likewise includes both values, and dict(f_locals.items()) silently selects the enclosing list instead of the iteration value returned by dict(f_locals).
Let's extend the new frame-locals test to check that all of these operations agree.
| _PyUnicode_EqualToASCIIString(k, "__classdict__") || | ||
| _PyUnicode_EqualToASCIIString(k, "__conditional_annotations__"))) { | ||
| scope = GLOBAL_IMPLICIT; | ||
| _PyST_IsClassClosureName(k)) { |
There was a problem hiding this comment.
I think this check now counts an inlined child as needing a real class closure, even though its special-name references compile as global lookups.
For example:
class Meta(type):
def __new__(mcls, name, bases, ns):
ns.pop('__classcell__', None)
return type.__new__(mcls, name, bases, ns)
class C(metaclass=Meta):
result = [[super for _ in (0,)] for _ in (0,)]Main creates C without supplying a __classcell__. This PR adds the cell and raises RuntimeError: __class__ not set defining 'C'. A single comprehension still succeeds.
Can we traverse through inlined children to determine whether an actual nested compilation unit needs the cell?
| } | ||
| if (inline_comp) { | ||
| if (!inline_comprehension(ste, entry, scopes, child_free, inlined_cells)) { | ||
| if (entry->ste_type == InlinedComprehensionBlock && ste->ste_type != InlinedComprehensionBlock) { |
There was a problem hiding this comment.
Can we preserve the fast-local optimization when a variable is only referenced by other inlined comprehensions?
def f(n):
return [[x for _ in range(20)] for x in range(20)]Deferring finalization until the enclosing scope is no longer inlined lets analyze_cells() promote x before those references are handled. Main has no cells or dereference instructions here; this revision has co_cellvars == ('x',), two MAKE_CELL instructions, and STORE_DEREF/LOAD_DEREF.
Small timeit measurements on matching debug builds showed this case around 11–17% slower. I haven't measured release-build impact.
Maybe this is fine? But it seems like it should be possible to keep this refactor behavior-preserving.
|
|
||
| Used for the symbol table of a class. | ||
|
|
||
| .. attribute:: INLINED_COMPREHENSION |
There was a problem hiding this comment.
Nit: could we add .. versionadded:: 3.16 to this member? The enclosing enum is documented as introduced in 3.13, so the new member's availability is otherwise unclear.
| static int | ||
| compiler_resolve_inlined_free(PySTEntryObject **ste, int scope, PyObject *name) | ||
| { | ||
| while (scope == FREE && (*ste)->ste_type == InlinedComprehensionBlock) { |
There was a problem hiding this comment.
Could we account for the implicit receiver synthesized by load_args_for_super() here? Its scope is 0 in the comprehension's symbol table, so this scope == FREE condition skips the enclosing-scope lookup.
This now aborts compilation in a debug build:
class A:
def f(self):
return 42
class B(A):
def f(self):
return [super().f() for _ in (0,)]
print(B().f())Main returns [42]. The implicit receiver load passes self to codegen_nameop(), but self isn't explicitly referenced in the comprehension, so it has no symbol-table entry there. The unresolved scope reaches codegen_nameop()'s assertion. Renaming the receiver to _self gets past the assertion but raises SystemError: no locals found at runtime.
Resolves #124697
Inlined comprehensions are now represented in the symbol table as block of a new type
InlinedComprehensionBlock, which is a subscope of the enclosing scope (not a separate compilation unit).This moves the complexity of compiling inlined comprehensions from codegen to the symbol table
construction.
It removes the smell of the compiler modifying the symbol table in codegen.