lowering: rename an arm capture or nested-block let the enclosing function uses elsewhere - #95
Merged
Merged
Conversation
…where A Pyfun capture is arm-scoped; the Python capture is a function-wide local. A capture whose name occurs elsewhere in the frame (outside its arm, not counting sibling arms capturing the same name) is emitted as _name, and every reference in the arm follows. Frame censuses in captures.rs; one enter_arm/exit_arm pair at every arm-binding site. Also records the third dogfooding session in ROADMAP.md.
…here Same census as the arm-capture rename: a let in a block that is not the frame's root block is emitted as _name when the name occurs in the frame outside that block. A value let lowers its value first and installs the rename after; a parameterised let installs it before its body and is defined under the fresh name; let mut, <- targets, and the nonlocal/global declarations of closures follow it. Root-level lets never rename.
This was referenced Aug 30, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A match-arm capture is arm-scoped in Pyfun, and a
letin a nested block (anifbranch, an arm body) is block-scoped. The Python either lowers to (case Some(pair):,pair = o._0in anisinstanceladder,x = 10in the branch) is an assignment to a function-wide local. One that reused a name the function also used for something else (a block-localdef, a parameter, an outerlet, a global read later) rebound that slot for the rest of the function, andpyfun checkcould not see it (#92, #96).The rule
Both binders share one census of the enclosing Python frame (
src/lowering/captures.rs): an occurrence is aVarreference or an<-target, never a binder; the walk covers the frame's whole body including nested closures, hiding the names a nested Python scope (lambda, parameterised blocklet, computation expression) binds for itself.nof armAis emitted as_nwhennoccurs in the frame outsideA, not counting sibling arms of the same match that also capturen(disjoint alternatives reading their own capture), socase Ok x: … case Error x: …keeps its names.let nin a nested blockBis emitted as_nwhennoccurs in the frame outsideB. Occurrences insideBinclude thelet's own value, solet x = x + 1there reads the outerxand emits_x = x + 1. Aletat the root of a function body never renames (sequential rebinding is what Python does too).Fresh names bump to
_n2,_n3while the frame uses the candidate, it was handed out already, it is a binder anywhere in the module, or it would start with_pf_. Every reference follows the rename.Mechanism
Lowerer::frames(Frame { occurrences, fresh, depth, root_is_body }) is pushed inlower_fn_body,lower_ce(a user builder gets the enclosing census plus the CE body's occurrences), and once for the module; the fold-loop pass pushes the enclosing frame merged with the inlined folder body. A def-emission site without its own push inherits the enclosing census; merges over-count; both only ever freshen more.enter_arm/exit_armreplace thepattern_bindings+extend+shadow_local_fnstriple at every arm-binding site: bothmatchpositions, the active-pattern chain and fall-through sequence, theOption/Resultladder, the lookup peephole, and the fold pass's inlined match.enter_block/exit_blockbump the frame's block depth around the three block lowerings and hand a nested block its own census;lower_block_letdecides each bound name. A valueletlowers its value first, then installs the rename for the rest of the block (it dies with the block'srestore_local_scope); a parameterisedletinstalls it before its body so recursion resolves to the renamed def, emitted under the fresh name. The target spelling is passed down explicitly (lower_let'stargets, throughlower_binding_asandunpack_binding's rename hook for destructuring), never read fromrenames.Lowerer::renamesfollows the block-scoped registries' discipline (LocalScope/Shadowed), so a lambda parameter, blocklet, inner capture or CE binder of the same name displaces the rename for its extent.lower_var,lower_pattern,bind_irrefutable/unpack_into_as, the peephole's payload binding, the ladder's whole-value binding, the active-pattern binder assignments,<-targets, and theglobal/nonlocaldeclarationslower_fn_bodycomputes all spell names through it.ROADMAP
Adds the "Dogfooding findings (2026-08-30, third session: performance)" section with the downstream verification numbers for #90, #91, #93 and #94, and the three design decisions made this round.
Not covered
A nested block inside a computation expression body cannot be written today (layout is suspended inside
{ }, soletinside anif/matchbranch there is a parse error); the machinery treats any block inside a CE as nested, so it is covered the day the parser admits it.Closes #92
Closes #96