c96940ea96
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).
66 lines
2.5 KiB
Plaintext
66 lines
2.5 KiB
Plaintext
; 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))))))))
|