spec: unique binder names per fn — A2b leg of RawBuf drop-leak (refs #43)

The last open leg of the #43 owned-heap drop-leak cluster is the
UniquenessTable shadow-name collapse: the side-table keys by
(def_name, binder_name), so a fn that shadows a binder name collapses
every shadow onto one key. The outermost binding (records last, on pop)
overwrites the inner ones; codegen's scope-close drop gates then read
the collapsed consume_count for the innermost binding and suppress its
drop, leaking the owned slab.

Spec 0056 resolves this as a compiler-internal naming collision — NOT
the deeper, separate problem of persistent AST provenance back to the
authored form (certain to be needed eventually, deliberately deferred;
conflating the two is a category error). Fix: alpha-rename shadowing
binders during desugar (reusing the existing fresh-name machinery that
already mints $mp_N / $lr_N), making the (def, name) key injective
again. No consumer of the uniqueness table changes — they all key by
name, and the name becomes a per-fn injective identity. IR-neutral:
codegen names heap by fresh SSA, never by source binder name. The
pre-desugar hashed form is untouched, so module identity and hash-pin
tests are unaffected.

RED fixtures securing the binder-kind class (the user's gating
condition before proceeding to the uniform-across-kinds fix):
- Let-shadow: the three raw_buf_{int,float,bool}_shadow_rebind tests
  (already in tree, committed f7f4c3b) — assert live == 0.
- Flat-pattern-shadow: a differential test plus its two fixtures
  (flat_pat_shadow_leak.ail shadows the outer binder; _control.ail
  alpha-renames it to a distinct name). The shadow must reach the
  control's live count. Differential by design, to isolate the
  shadow-collapse drop from unrelated baseline drop gaps out of
  #43's scope.

Grounding-check PASS; ail-check parse-gate green on every fenced block.
This commit is contained in:
2026-05-30 12:21:27 +02:00
parent 0f6108e428
commit 0015f3dad1
4 changed files with 361 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
(module flat_pat_shadow_control
(data IntList
(ctor Nil)
(ctor Cons (con Int) (con IntList)))
(data Box (vars a)
(ctor MkBox a))
(fn blen
(doc "borrow-only length over a borrowed IntList")
(type (fn-type (params (borrow (con IntList))) (ret (con Int))))
(params xs)
(body (match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t) (app + 1 (app blen t))))))
(fn csum
(doc "consuming sum over an IntList")
(type (fn-type (params (con IntList)) (ret (con Int))))
(params xs)
(body (match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t) (app + h (app csum t))))))
(fn main
(doc "Alpha-renamed control for flat_pat_shadow_leak.ail (refs #43): structurally identical, but the inner flat-match pattern-binder is `y` (no shadow of the outer `x`). The inner payload's arm-close drop fires because (def,\"y\") does not collide with (def,\"x\"). This is the live-count baseline the shadow fixture must reach once binder names are unique per fn at desugar (C').")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(let x (term-ctor IntList Cons 1 (term-ctor IntList Nil))
(seq
(match (term-ctor Box MkBox (term-ctor IntList Cons 2 (term-ctor IntList Nil)))
(case (pat-ctor MkBox y)
(app print (app blen y))))
(app print (app csum x)))))))
+31
View File
@@ -0,0 +1,31 @@
(module flat_pat_shadow_leak
(data IntList
(ctor Nil)
(ctor Cons (con Int) (con IntList)))
(data Box (vars a)
(ctor MkBox a))
(fn blen
(doc "borrow-only length over a borrowed IntList")
(type (fn-type (params (borrow (con IntList))) (ret (con Int))))
(params xs)
(body (match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t) (app + 1 (app blen t))))))
(fn csum
(doc "consuming sum over an IntList")
(type (fn-type (params (con IntList)) (ret (con Int))))
(params xs)
(body (match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t) (app + h (app csum t))))))
(fn main
(doc "A2b completeness fixture (refs #43): a flat-match pattern-binder `x` shadows the outer let `x`. The inner `x` (Box payload, a heap IntList) is borrow-only (consume_count 0) and must be dropped at arm close; the outer `x` is consumed once by csum (count 1). Under the (def,name) uniqueness-table collision the arm-close drop gate (match_lower.rs) reads the collapsed outer count 1 and suppresses the inner payload's drop, leaking 2 cells. Pairs with flat_pat_shadow_control.ail (same program, inner binder alpha-renamed to `y`); the live-count delta is exactly the suppressed drop. GREEN once binder names are made unique per fn at desugar.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(let x (term-ctor IntList Cons 1 (term-ctor IntList Nil))
(seq
(match (term-ctor Box MkBox (term-ctor IntList Cons 2 (term-ctor IntList Nil)))
(case (pat-ctor MkBox x)
(app print (app blen x))))
(app print (app csum x)))))))