Files
AILang/crates/ailang-core
Brummel 55d76ae4a1 fix: alpha-rename shadowing binders at desugar — A2b leg (closes #43)
The last open leg of the RawBuf owned-heap drop-leak cluster #43: the
UniquenessTable shadow-name collapse. The side-table keys by
(def_name, binder_name); a fn that shadows a binder name — the shipped
`(let buf (new…) (let buf (set buf…) … (get buf)))` idiom — collapses
every shadow onto one key. The outermost binding records last (on pop)
and overwrites the inner ones, so codegen's scope-close drop gates read
the collapsed consume_count for the innermost binding, misjudge its
ownership, and suppress its drop. The owned slab leaked (live==1).

Fix: desugar now alpha-renames any binder whose authored name shadows
an enclosing binding to a fresh `<name>$<n>`, making the (def, name)
key injective per fn. The rename is on-shadow-only and uniform across
binder kinds (Let, flat-Match pattern-binders, Lam params, Loop
binders); non-shadowing binders keep their authored name, so every
existing fixture's desugared output is byte-identical (evidenced by the
full pre-existing desugar suite passing unchanged). IR-neutral: codegen
names heap by fresh SSA, never by the source binder name.

No consumer of the uniqueness table changes — they all key by name, and
the name is now a per-fn injective identity (acceptance criterion 5).
The fix is entirely internal to ailang-core::desugar; uniqueness.rs gets
only a doc-comment correction (its old text was wrong on two counts:
"last = innermost by pop-order" was backwards, and "every shipping
fixture has unique binder names" was false).

Mechanism (vs. the spec's sketch): the threaded lexical scope became a
`Scope` struct carrying `entries` keyed by each binder's EFFECTIVE
(post-rename) name and `rename` mapping authored→effective. Keying
entries by effective name is load-bearing, not cosmetic: the LetRec
capture-detection reads `free_vars_in_term` of the desugared body
(effective names) and filters by `scope.contains_key` — had entries
stayed keyed by authored name, renaming an enclosing let would have made
a captured shadowed binder invisible to capture detection (UnknownVar in
the lifted fn). `insert_fixed` records identity renames for never-renamed
binders (fn-params, LetRec name/params) so an inner renamable binder that
shadows them is still detected.

Alternatives rejected: (A) an AST id field and (B) a derived
binding-path — both solve the deeper, SEPARATE problem of persistent AST
provenance back to the authored form, which is certain to be needed
eventually but is not required here; conflating the internal naming
collision with provenance is a category error. C′ (this) reuses the
existing fresh-name machinery and touches one pass.

Verification: the three shipped Let-shadow tests
(raw_buf_{int,float,bool}_shadow_rebind_drop_balances_rc_stats) reach
live==0 (were RED at live==1); the flat-pattern differential
(flat_pat_shadow_binder_does_not_leak_more_than_alpha_renamed) reaches
its alpha-renamed control's live count (was 5 vs 3); a new desugar unit
test (shadowing_let_is_alpha_renamed) pins the rename + reference
resolution; full workspace suite green, no fixture output drift. Both
no-change consumers re-verified by hand: match_lower.rs keys by the
emitted (renamed) pattern binder, linearity builds table and lookups
from the same desugared tree.

Spec: docs/specs/0056-unique-binder-names.md.
Plan: docs/plans/0111-unique-binder-names.md.
2026-05-30 12:42:44 +02:00
..