lowering: rename a root-level let that shadows an enclosing-scope name read earlier in the function - #100
Merged
Merged
Conversation
…e 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.
…ts own def A let/let! target in an option/result/seq/async body is assigned in the CE's own Python def, so one that reuses a name the body had already read from outside the CE made that name local to the whole def and the earlier read raised UnboundLocalError. bind_ce_target now splits into registering the binder (evict, decide the rename under the root-level rule, install it for the rest of the body) and emitting the assignment, so the let! of result/option can register before its continuation lowers; the failure branch still returns the subject untouched.
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 root-level
letin a function that reuses a name the function had already read from an enclosing scope made that name local to the whole Pythondef, so the earlier read raisedUnboundLocalError.pyfun checkpassed; the program died at runtime.now emits
and prints
18.The rule
A root-level
let nis emitted as_niff some occurrence ofnin the same frame reads a binding outside the frame: an enclosing function's binding, a module-level binding, a builtin. Such a read can only sit before thelet, in thelet's own value, or in a closure made before it, and Python's function-wide locals rule turns the later assignment into a declaration that covers it. A read after theletresolves to theletitself; a read of a parameter or an earlier root-levelletresolves to the frame's own root; neither counts, so rebinding in sequence stays exactly as Python does it. Top-levellets are globals and never rename.Mechanism
src/lowering/captures.rs: an occurrence now records what it reads asTarget::Outside,Target::Root(key)(a parameter or root-levelletof this frame) orTarget::Binder(key)(an arm or nested-block binder).Frame::of_bodytakes the parameters and binds them as roots; a root-levellet, a CE binder and a top-levelletbind as roots too.Frame::must_renamecountsOutsideandRootalike (unchanged behaviour for arm captures and nested-blocklets); the newFrame::must_rename_rootasks only for anOutsideoccurrence, and returns false in the module frame.lower_block_letnow decides root-level names too, with the same ordering,targetsplumbing,let mut/nonlocalmapping and destructuring hook as for nested ones.Tests
Nine new tests in
tests/compile.rs(e2e where the output proves it): the issue program, a module binding read then shadowed,let n = n + 1over a module binding, a nested def made before theletthat reads the enclosing binding, a root-levellet mutwith<-from a closure (nonlocal _x), a destructuringlet (a, b)renaming onlya; and the negatives: rebinding a parameter, aletreferenced only after it, a top-levellet. No existing test changed. Full suite,clippy -D warnings,fmt --checkanddocs/verify_lessons.pyare clean.Computation-expression binders (second commit)
The same shape one level down: a
let/let!in anoption/result/seq/asyncbody is assigned in the CE's own def, soraised
UnboundLocalErroronm = n + 1. A CE binder is a root-level binder of the CE's frame and now takes exactly the root-level rule.bind_ce_targetsplits intoenter_ce_binder(evict the local-function registries, decide the rename withmust_rename_root, install it for the rest of the body) andbind_ce_target_as(emit the assignment under the chosen spelling, throughunpack_binding'stargetsfor a destructuring target). Thelet!ofresult/optionlowers its continuation before building its ladder, solower_short_circuit_itemsregisters the binder first and hands the spelling toshort_circuit_bind; the failure branch still returns the subject untouched.seqandasyncbind in order, sobind_ce_targetdoes both steps in one. Emitted:_n = _pf_t0._0, printsSome(8).Seven more tests: the repro, the
resultversion with both branches (Ok(8)/Error('no')), theasyncversion run throughasyncio.run(8), aseqlet([8]), a destructuringlet! (n, k)renaming onlyn; and the negatives: alet!read only after it, and alet!rebinding an earlier binder of the same CE.Docs:
DESIGN.md§5 paragraph and theINTERNALS.mdsection state the root-level rule for function bodies and CE bodies;ROADMAP.mditem 10 notes #99.Closes #99