You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
lowering: rename a root-level let that shadows an enclosing-scope name read earlier in the function
A root-level let in a nested function that reuses a name the function had
already read from an enclosing scope (a parameter of the outer function, a
module-level binding, a builtin) made that name local to the whole Python
def, so the earlier read raised UnboundLocalError. The capture census now
tells a reference to a binding outside the frame from one to the frame's
own root (a parameter or an earlier root-level let), and a root-level let
renames to _name exactly when some read of the name in the frame is outside.
Rebinding a parameter or an earlier let in sequence stays as Python does it,
and top-level lets are globals and never rename.
Copy file name to clipboardExpand all lines: INTERNALS.md
+13-4Lines changed: 13 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -234,7 +234,7 @@ the runtime alongside `Some`/`None_` unless it binds that name itself.
234
234
### Arm-scoped captures and nested-block `let`s — implements DESIGN §5
235
235
236
236
A match arm's capture, and a `let` in a nested block, both become function-wide Python locals, so one
237
-
that another binding of the same name is live across is renamed (issues #92, #96and #97). The
237
+
that another binding of the same name is live across is renamed (issues #92, #96, #97and #99). The
238
238
rule, in `src/lowering/captures.rs`, is liveness: binder `B` of name `n` (an arm capture or a
239
239
nested-block `let`) is freshened when some reference to `n` outside `B` resolves, under Pyfun
240
240
scoping, to a binding that is live across `B`: the frame root (a parameter, a root-level `let`, a
@@ -247,8 +247,10 @@ three arms under a root-level `why` read at the end all rename, and a lambda mad
247
247
reads `why` forces a later arm capturing `why` to rename.
248
248
249
249
The census is one walk of the frame's body (`Census`) that resolves every `Var` reference and `<-`
250
-
target to what it reads at that point, recorded as an `Occurrence { binder, in_closure }`: `binder`
251
-
is `None` for the frame root, or the identity of a registered arm/nested-block binder. Identity is
250
+
target to what it reads at that point, recorded as an `Occurrence { target, in_closure }`: the
251
+
`target` is `Outside` (a binding outside the frame: an enclosing function's, a module-level one, a
252
+
builtin), `Root` (a parameter or a root-level `let` of this frame), or `Binder` (a registered
253
+
arm/nested-block binder); `must_rename` counts the first two alike. Identity is
252
254
the address of the AST node (`binder_key`, the `MatchArm` or the `LetBinding`), which is what the
253
255
lowering holds when it decides that binder, so no spans are involved and a desugared or cloned tree
254
256
can never be mistaken for the original. Each registered binder also records its `ancestors`, the
@@ -298,7 +300,14 @@ are not a block, so any block inside a CE is nested; the module frame treats a b
298
300
binding as root, since a collision with a module-level binding is already isolated by
299
301
`lower_module`'s frame wrap). In a block that is not the root (`Frame::block_is_nested`, reported by
300
302
`enter_block`), `lower_block_let` decides each bound name by `Frame::must_rename(n, key(let))`. A
301
-
root-level `let` never renames: rebinding in sequence is what Python does too. Ordering: a value `let` lowers its value first (references there mean the outer binding), then
303
+
root-level `let` asks `Frame::must_rename_root(n)` instead (issue #99): rename iff any occurrence of
304
+
`n` in the frame is `Outside`. Python makes `n` local to the whole `def` as soon as anything in it
305
+
assigns `n`, so a read of an outer `n` before the `let` (or in its value, or in a closure made
306
+
before it) would raise `UnboundLocalError`; a read after the `let` resolves to `Root(let)` and a
307
+
read of a parameter or an earlier root-level `let` resolves to `Root(that)`, neither of which is
308
+
`Outside`, so rebinding in sequence stays as Python does it. Parameters are bound as `Root` when
309
+
the frame is built (`Frame::of_body` takes them), and the module frame never renames a root-level
310
+
`let` (`Frame::module`), since those are globals. Ordering: a value `let` lowers its value first (references there mean the outer binding), then
302
311
installs `n → _n` in `renames` for the rest of the block, where it dies with the block's
303
312
`restore_local_scope`; a parameterised `let` installs it before its body so a recursive call resolves
304
313
to the renamed def, and the def is emitted under the fresh name. The emitted target is passed down
0 commit comments