Iter 18d.4: pattern-binder + Own-param dec at scope close
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.
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"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"name":"h","t":"var"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"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.","kind":"fn","name":"head_or_zero","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Int"}}},{"body":{"body":{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"head_or_zero","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"name":"xs","t":"let","value":{"args":[{"lit":{"kind":"int","value":11},"t":"lit"},{"args":[{"lit":{"kind":"int","value":22},"t":"lit"},{"args":[{"lit":{"kind":"int","value":33},"t":"lit"},{"args":[{"lit":{"kind":"int","value":44},"t":"lit"},{"args":[{"lit":{"kind":"int","value":55},"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":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}},"doc":"Build a 5-element IntList; pass to head_or_zero (transferring ownership); print the result.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"rc_own_param_drop","schema":"ailang/v0"}
|
||||
@@ -0,0 +1,62 @@
|
||||
; 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))))))
|
||||
Reference in New Issue
Block a user