Skip to content

Close over the frame that defined you - #98

Merged
tamnd merged 1 commit into
mainfrom
closures
Aug 29, 2026
Merged

Close over the frame that defined you#98
tamnd merged 1 commit into
mainfrom
closures

Conversation

@tamnd

@tamnd tamnd commented Aug 29, 2026

Copy link
Copy Markdown
Owner

A function defined inside a function can now read and write the names around it, and nonlocal says which of them it means to write.

A name shared that way lives in a cell both frames hold rather than in a register in each of them, which is the whole difference between a closure and a copy. Two functions defined in the same frame see each other's writes, a counter carries on counting between calls, and every def in a loop closes over the one loop variable rather than its value at the time, so all three print 3. A name captured two functions deep is carried by the function in between whether that function mentions it or not, because a capture list only reaches the frame that wrote the def.

Which names need a cell is a fact about the slot rather than about any one read of it, and it is not known until the body is finished, because the def that captures a name can come after every use of it. So the HIR records it on the slot, the printer spells it out on every use (cell x, free x), and the bytecode compiler is the one pass that consults it. That keeps the lowering to a single pass and means nothing that is not a closure pays for any of this: a function capturing nothing compiles to exactly what it did before, and the call benchmark is unchanged at 0.11s.

A nonlocal with nothing to bind to is a SyntaxError rather than a report that the feature is missing, which meant giving the lowering a second kind of failure. Calling it unsupported would send somebody looking through the milestones for it. All four of CPython's messages are word for word, along with the NameError for a free variable with nothing in it, which is a different sentence from the one an ordinary unbound local gets. Every message was checked against a running 3.14.

Part of #7.

A function defined inside a function can read and write the names around
it, and `nonlocal` says which of them it means to write. A name shared that
way lives in a cell both frames hold rather than a register in each, which
is the difference between a closure and a copy.

Which names need a cell is a fact about the slot rather than about any one
read of it, and is not known until the body is finished, because the `def`
that captures a name can come after every use of it. So the HIR records it
on the slot and the bytecode compiler is the one pass that consults it.
Nothing that is not a closure pays anything: the call benchmark is
unchanged.

A `nonlocal` with nothing to bind to is now a `SyntaxError` rather than a
report of a missing feature, which meant giving the lowering a second kind
of failure. All four of CPython's messages are word for word, as is the
`NameError` for a free variable with nothing in it.
@tamnd
tamnd merged commit cc9638b into main Aug 29, 2026
9 checks passed
@tamnd
tamnd deleted the closures branch August 29, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant