Skip to content

lowering: rename an arm capture or nested-block let the enclosing function uses elsewhere - #95

Merged
simontreanor merged 2 commits into
mainfrom
fix/arm-capture-freshening
Aug 30, 2026
Merged

lowering: rename an arm capture or nested-block let the enclosing function uses elsewhere#95
simontreanor merged 2 commits into
mainfrom
fix/arm-capture-freshening

Conversation

@simontreanor

@simontreanor simontreanor commented Aug 30, 2026

Copy link
Copy Markdown
Owner

A match-arm capture is arm-scoped in Pyfun, and a let in a nested block (an if branch, an arm body) is block-scoped. The Python either lowers to (case Some(pair):, pair = o._0 in an isinstance ladder, x = 10 in the branch) is an assignment to a function-wide local. One that reused a name the function also used for something else (a block-local def, a parameter, an outer let, a global read later) rebound that slot for the rest of the function, and pyfun check could not see it (#92, #96).

The rule

Both binders share one census of the enclosing Python frame (src/lowering/captures.rs): an occurrence is a Var reference 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 block let, computation expression) binds for itself.

  • A capture n of arm A is emitted as _n when n occurs in the frame outside A, not counting sibling arms of the same match that also capture n (disjoint alternatives reading their own capture), so case Ok x: … case Error x: … keeps its names.
  • A let n in a nested block B is emitted as _n when n occurs in the frame outside B. Occurrences inside B include the let's own value, so let x = x + 1 there reads the outer x and emits _x = x + 1. A let at the root of a function body never renames (sequential rebinding is what Python does too).

Fresh names bump to _n2, _n3 while 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 in lower_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_arm replace the pattern_bindings + extend + shadow_local_fns triple at every arm-binding site: both match positions, the active-pattern chain and fall-through sequence, the Option/Result ladder, the lookup peephole, and the fold pass's inlined match.
  • enter_block/exit_block bump the frame's block depth around the three block lowerings and hand a nested block its own census; lower_block_let decides each bound name. A value let lowers its value first, then installs the rename for the rest of the block (it dies with the block's restore_local_scope); a parameterised let installs 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's targets, through lower_binding_as and unpack_binding's rename hook for destructuring), never read from renames.
  • Lowerer::renames follows the block-scoped registries' discipline (LocalScope/Shadowed), so a lambda parameter, block let, 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 the global/nonlocal declarations lower_fn_body computes 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 { }, so let inside an if/match branch 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

…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.
@simontreanor simontreanor changed the title lowering: rename a match-arm capture the enclosing function uses elsewhere lowering: rename an arm capture or nested-block let the enclosing function uses elsewhere Aug 30, 2026
@simontreanor
simontreanor merged commit 63aa296 into main Aug 30, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant