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).
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,65 @@
|
||||
; Iter 18d.3 — move-aware pattern bindings: let-close emits an
|
||||
; inlined per-field dec sequence that skips moved slots and dec's
|
||||
; non-moved (wildcarded) pointer-typed slots.
|
||||
;
|
||||
; Fixture shape:
|
||||
; - Pair carries two `IntList` fields (both pointer-typed).
|
||||
; - main builds a Pair, pattern-matches it binding only the FIRST
|
||||
; field, wildcarding the SECOND.
|
||||
; - The matched binding `a` is consumed by `sum_list`. The
|
||||
; wildcarded slot is logically retained by `p` (no binder owns
|
||||
; it), so at p's let-close it must be dec'd.
|
||||
;
|
||||
; Expected IR shape under --alloc=rc at p's let-close:
|
||||
; - NO `call void @drop_pat_extract_partial_drop_Pair(ptr <p>)`
|
||||
; (the uniform drop fn is replaced by inline partial drop)
|
||||
; - YES a `getelementptr ... ptr <p>, i64 16` + `load ptr` +
|
||||
; `call void @drop_pat_extract_partial_drop_IntList(ptr <slot1_v>)`
|
||||
; (slot 1 was NOT moved → emit per-field dec)
|
||||
; - YES `call void @ailang_rc_dec(ptr <p>)` (outer cell)
|
||||
; - NO load+dec for slot 0 (moved → skipped)
|
||||
;
|
||||
; Expected stdout: 6 (1 + 2 + 3 = 6, sum of the first list).
|
||||
|
||||
(module pat_extract_partial_drop
|
||||
|
||||
(data IntList
|
||||
(doc "Recursive Int list — boxed.")
|
||||
(ctor Nil)
|
||||
(ctor Cons (con Int) (con IntList)))
|
||||
|
||||
(data Pair
|
||||
(doc "Two IntLists side by side. Both fields are pointer-typed; the test exercises move-tracking against EACH slot independently.")
|
||||
(ctor Pair (con IntList) (con IntList)))
|
||||
|
||||
(fn sum_list
|
||||
(doc "Consume xs, sum its elements.")
|
||||
(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 sum_list t))))))
|
||||
|
||||
(fn main
|
||||
(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.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let p
|
||||
(term-ctor Pair Pair
|
||||
(term-ctor IntList Cons 1
|
||||
(term-ctor IntList Cons 2
|
||||
(term-ctor IntList Cons 3
|
||||
(term-ctor IntList Nil))))
|
||||
(term-ctor IntList Cons 4
|
||||
(term-ctor IntList Cons 5
|
||||
(term-ctor IntList Cons 6
|
||||
(term-ctor IntList Nil)))))
|
||||
(match p
|
||||
(case (pat-ctor Pair a _)
|
||||
(do io/print_int (app sum_list a))))))))
|
||||
Reference in New Issue
Block a user