ba63a16366
Closes the memory-hygiene regression 18d.3 introduced and the
symmetric Own-param-leaks debt 18c.3 / 18c.4 left open. Both
share an emission shape — a binder going out of scope without
being consumed (consume_count == 0) gets a drop call emitted.
Two emission seams added to crates/ailang-codegen/src/lib.rs:
(A) Pattern-binder dec at arm body close (lower_match): after
each arm's body lowers, for each pattern-bound binder of this
arm with consume_count == 0 and ptr-typed value, emit drop.
Routes through field_drop_call when moved_slots[binder] is
empty (the common case — pattern-bound binders rarely have
their own moves recorded), falls back to shallow ailang_rc_dec
when moved_slots is non-empty (dynamic-tag partial-drop debt).
Wildcard slots are NOT dec'd here — they're the source's
responsibility, which 18d.3 already handles via emit_inlined_
partial_drop.
(B) Own-param dec at fn return (emit_fn): widened the fn-type
destructure to pull param_modes. After the body's tail value
lowers but before the ret instruction, for each fn parameter
with param_modes[i] == Own and consume_count == 0 and ptr-typed
value AND not the body's tail SSA, emit drop. Same routing as
(A). Borrow/Implicit params are explicitly excluded — Borrow
because the caller still owns, Implicit because there's no
static "caller handed off" signal under back-compat.
drop_<m>_<T> itself remains uniform-on-fully-owned. The partial-
drop complexity is at the call sites with the static info,
matching the design call from 18c.4 and 18d.3.
Tests:
- examples/rc_own_param_drop.{ailx,ail.json} — canonical Own-
param fixture.
- alloc_rc_own_param_dec_at_fn_return E2E asserts (B)'s drop
fires before ret in the fn body's IR.
- alloc_rc_borrow_only_recursive_list_drop expanded with IR-
shape assertion that t's drop fires at arm close (closes the
18d.3 regression).
Side effect (correctness improvement): sum_list in reuse_as_
demo declares (own (con List)) and has consume_count(xs) == 0;
(B) now fires shallow ailang_rc_dec(xs) at each recursive
return, so the chain frees one cell per stack frame on unwind.
Stdout unchanged (9); existing reuse-arm IR-shape assertions on
map_inc continue to hold (they don't see sum_list's body).
Test deltas: e2e 57 -> 58. All other buckets unchanged. cargo
test --workspace green.
Known debt (deliberate, deferred):
- Dynamic-tag partial-drop. emit_inlined_partial_drop requires
a static Term::Ctor for layout; binders with non-empty
moved_slots whose runtime tag is dynamic fall back to shallow
ailang_rc_dec of the outer cell. Sound for shipping fixtures
(the moved-out child has been dec'd elsewhere by the time
iter B fires; remaining ctors have no further pointer fields
to dec). General fix needs tag-switch in the dec emission —
separate iter.
- Closure-typed Own params still go through field_drop_call's
Type::Fn arm (shallow dec), same path 18c.4 carved out.
- (drop-iterative) worklist allocator — Iter 18e.
63 lines
2.5 KiB
Plaintext
63 lines
2.5 KiB
Plaintext
; Iter 18d.4 — Own-param dec at fn return.
|
|
;
|
|
; Companion fixture to `alloc_rc_borrow_only_recursive_list_drop`.
|
|
; Here the static "caller handed off ownership" signal is the
|
|
; explicit `(own (con IntList))` parameter mode on `head_or_zero`.
|
|
; Under --alloc=rc, codegen now emits a drop call against the
|
|
; param SSA (`%arg_xs`) before the fn's `ret`, closing the
|
|
; symmetric debt 18c.3/18c.4 carried.
|
|
;
|
|
; Fixture shape:
|
|
; - `head_or_zero` takes `(own (con IntList))` and returns Int.
|
|
; Body destructures via match, returns the head in the Cons
|
|
; arm (ignoring the tail) and 0 in the Nil arm.
|
|
; - `xs` (the param) has consume_count == 0 (only borrow at
|
|
; match scrutinee). Iter B fires: param drop at fn return.
|
|
; - `t` (Cons-arm pattern binder) has consume_count == 0 (the
|
|
; arm body just returns `h` — `t` is unused). Iter A fires:
|
|
; drop_<m>_IntList(t) at arm close, freeing the tail chain
|
|
; before the fn returns.
|
|
; - At fn return, moved_slots[xs] = {1} (Cons.t was moved into
|
|
; the arm-bound `t` and dec'd by iter A). Iter B falls back
|
|
; to shallow `ailang_rc_dec(%arg_xs)` — the dynamic-tag
|
|
; partial-drop case is debt; for the canonical Cons-or-Nil
|
|
; dispatch here, dec'ing the outer cell only is correct
|
|
; because the active ctor's only ptr field (Cons.tail) is
|
|
; already freed by iter A, and Nil has no ptr fields.
|
|
;
|
|
; Expected stdout under --alloc=rc / --alloc=gc: 11 (head of the
|
|
; 5-element list).
|
|
|
|
(module rc_own_param_drop
|
|
|
|
(data IntList
|
|
(doc "Recursive Int list — boxed.")
|
|
(ctor Nil)
|
|
(ctor Cons (con Int) (con IntList)))
|
|
|
|
(fn head_or_zero
|
|
(doc "Take ownership of an IntList; return its head if Cons, else 0. The tail is loaded as a pattern binder but never consumed — iter A dec's it at arm close. The outer cell is dec'd at fn return via iter B's Own-param emission.")
|
|
(type
|
|
(fn-type
|
|
(params (own (con IntList)))
|
|
(ret (con Int))))
|
|
(params xs)
|
|
(body
|
|
(match xs
|
|
(case (pat-ctor Nil) 0)
|
|
(case (pat-ctor Cons h t) h))))
|
|
|
|
(fn main
|
|
(doc "Build a 5-element IntList; pass to head_or_zero (transferring ownership); print the result.")
|
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
|
(params)
|
|
(body
|
|
(let xs
|
|
(term-ctor IntList Cons 11
|
|
(term-ctor IntList Cons 22
|
|
(term-ctor IntList Cons 33
|
|
(term-ctor IntList Cons 44
|
|
(term-ctor IntList Cons 55
|
|
(term-ctor IntList Nil))))))
|
|
(do io/print_int (app head_or_zero xs))))))
|