Files
AILang/examples/pat_extract_partial_drop.ail.json
T
Brummel c96940ea96 Iter 18d.3: move-aware pattern bindings via codegen-side bookkeeping
Closes 18d.2's per-field-dec scope cut without mutating the
source. Codegen Emitter gains a per-fn-body side table:

  moved_slots: BTreeMap<binder_name, BTreeSet<field_idx>>

When lower_match destructures a pattern and binds a non-wildcard
pointer-typed slot, codegen records (scrutinee_binder, slot_idx)
in moved_slots — but does NOT mutate the source's slot. The
source box stays untouched; the bookkeeping carries the
move-out information through codegen.

Two consumer sites:

(1) Term::Let scope close. If moved_slots[binder] is non-empty,
codegen inlines a per-field dec sequence: load each pointer-
typed field whose index is NOT in moved_slots, dispatch through
field_drop_call, then ailang_rc_dec on the outer cell. If
moved_slots[binder] is empty (the common case for every shipping
fixture today), the call to drop_<m>_<T>(ptr) is unchanged from
18c.4 — no IR shape regression.

(2) lower_reuse_as_rc reuse arm. Per-field dec is now safe — the
moved-out slots are skipped via moved_slots lookup; non-moved
slots are dec'd via field_drop_call's null-guarded drop fns.
Replaces the 18d.2 punt rationale block with the new invariant.

drop_<m>_<T> itself is unchanged: it operates on a fully-owned
pointer (the cascade has no notion of "partially moved"). The
partial-move complexity is confined to top-level emission sites
that have the static info.

Why static tracking, not source-mutation: the previous attempt
(null-out the source's slot at pattern-bind point) ran into two
structural issues: (i) borrowed scrutinees must not be mutated —
null-out violates 18a's borrow contract; (ii) Desugarer creates
chains that re-scrutinise the same scrutinee across arms, and
mutation in arm 1 makes arm 2's re-load see garbage. Static
tracking sidesteps both: the source box stays bit-identical
across the entire match.

Tests:
- new fixture examples/pat_extract_partial_drop — Pair(IntList,
  IntList) destructured (a, _) — exercises moved + wildcard
  pointer slots.
- new e2e alloc_rc_partial_drop_skips_moved_keeps_wildcarded
  asserts stdout 6 under both gc and rc, plus IR shape: main's
  let-close inlines drop on the wildcarded slot but NOT on the
  moved slot.
- updated reuse_as_demo_under_rc_uses_inplace_rewrite IR-shape
  assertion — the canonical fixture moves the only pointer
  slot, so the reuse arm should contain neither @drop_ nor
  @ailang_rc_dec for fields.

Test deltas: e2e 56 -> 57 (+1). All other buckets unchanged.
cargo test --workspace green.

Known regression — narrow, deliberate, queued for 18d.4:

The prior alloc_rc_borrow_only_recursive_list_drop fixture from
18c.4 had a pattern (Cons h t) where t was bound but never
consumed downstream. Under 18c.4, drop_<m>_<IntList>(xs) at
let-close cascaded through xs.tail and freed the entire 5-cell
chain. Under 18d.3 with strategy (b), xs.tail is in
moved_slots, so the cascade is interrupted there — but t is
never consumed, so its 4-cell tail now leaks. The fixture
stdout is unchanged (still "11"), but memory hygiene under
--alloc=rc is strictly worse.

The fix is symmetric to 18c.4's known fn-parameter-dec debt:
pattern-bound binders whose consume_count is 0 at arm close
should also get a drop call emitted. Same emission seam as
let-close-drop. Queued as 18d.4. Until 18d.4 ships, fixtures
with pattern-extracted unused pointer binders leak under
--alloc=rc; this is documented and bounded (shipping fixtures
beyond the test suite generally consume every binder they
bind).
2026-05-08 12:18:12 +02:00

2 lines
2.5 KiB
JSON

{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"Cons"}],"doc":"Recursive Int list — boxed.","kind":"type","name":"IntList"},{"ctors":[{"fields":[{"k":"con","name":"IntList"},{"k":"con","name":"IntList"}],"name":"Pair"}],"doc":"Two IntLists side by side. Both fields are pointer-typed; the test exercises move-tracking against EACH slot independently.","kind":"type","name":"Pair"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Consume xs, sum its elements.","kind":"fn","name":"sum_list","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Int"}}},{"body":{"body":{"arms":[{"body":{"args":[{"args":[{"name":"a","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"pat":{"ctor":"Pair","fields":[{"name":"a","p":"var"},{"p":"wild"}],"p":"ctor"}}],"scrutinee":{"name":"p","t":"var"},"t":"match"},"name":"p","t":"let","value":{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[{"lit":{"kind":"int","value":3},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"},{"args":[{"lit":{"kind":"int","value":4},"t":"lit"},{"args":[{"lit":{"kind":"int","value":5},"t":"lit"},{"args":[{"lit":{"kind":"int","value":6},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Pair","t":"ctor","type":"Pair"}},"doc":"Build Pair([1,2,3], [4,5,6]); pattern-bind first slot only; sum first list. The wildcarded second slot's IntList lives only inside the Pair box; closing the Pair's let scope must dec it (non-moved) while skipping the moved first slot.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"pat_extract_partial_drop","schema":"ailang/v0"}