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:
+170
-13
@@ -1520,25 +1520,38 @@ fn alloc_rc_recursive_list_sum() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18c.4: actually exercise the recursive drop cascade at
|
||||
/// runtime. The let-binder `xs` of a 5-element `IntList` has
|
||||
/// `consume_count == 0` (only the match scrutinee, a borrow), so
|
||||
/// codegen emits `call void @drop_<m>_IntList(ptr xs)` at scope
|
||||
/// close. The drop fn's `Cons` arm recurses on the tail — for a
|
||||
/// 5-cell list that is 5 nested calls, exactly the recursive shape
|
||||
/// 18e will replace with an iterative worklist. The depth here is
|
||||
/// safely below any reasonable stack limit; longer lists are out
|
||||
/// of scope until 18e.
|
||||
/// Iter 18c.4 / 18d.4: exercise the recursive drop cascade at
|
||||
/// runtime AND the arm-close pattern-binder dec.
|
||||
///
|
||||
/// 18c.4 originally used this fixture to lock in the recursive
|
||||
/// `drop_<m>_IntList` cascade at `xs`'s let-close. 18d.3 introduced
|
||||
/// move-tracking and shipped a documented memory-hygiene regression:
|
||||
/// the `t` pattern-binder of `(Cons h t)` was unused (consume_count
|
||||
/// == 0) and the `xs` partial-drop now skipped slot 1 (moved into
|
||||
/// `t`), so the 4-cell tail leaked.
|
||||
///
|
||||
/// 18d.4 closes that regression by emitting an arm-close drop on
|
||||
/// `t` itself (symmetric to the let-close drop emission seam). At
|
||||
/// runtime the Cons arm now:
|
||||
/// 1. Loads slot 0 (h: Int → primitive, no drop).
|
||||
/// 2. Loads slot 1 (t: IntList → moved out of `xs`).
|
||||
/// 3. Prints h.
|
||||
/// 4. **NEW (18d.4):** calls
|
||||
/// `@drop_rc_list_drop_borrow_IntList(ptr %t)` — recursively
|
||||
/// cascades through the 4-cell tail, freeing each cell.
|
||||
/// 5. Branches to the join.
|
||||
/// And at `xs`'s let-close, the inlined partial drop skips slot 1
|
||||
/// (already freed by the arm) and dec's the outer Cons cell.
|
||||
///
|
||||
/// Properties guarded:
|
||||
/// 1. The binary runs to completion (no segfault from a
|
||||
/// mishandled recursive cascade or refcount underflow).
|
||||
/// 2. Stdout matches `--alloc=gc` byte-for-byte (`11` — the head
|
||||
/// of the 5-element list).
|
||||
/// 3. The match arm's pattern bindings (`h`, `t`) do NOT trip
|
||||
/// the dec machinery — `t` was loaded as a borrow inside the
|
||||
/// arm and must not be dec'd; we rely on `lower_match`'s
|
||||
/// pattern binding flow not adding a competing dec call.
|
||||
/// 3. The Cons arm's body emits the per-type drop on `t` between
|
||||
/// the `print h` call and the arm's branch back to the match
|
||||
/// join — the IR-shape signature of 18d.4's arm-close
|
||||
/// pattern-binder dec.
|
||||
#[test]
|
||||
fn alloc_rc_borrow_only_recursive_list_drop() {
|
||||
let example = "rc_list_drop_borrow.ail.json";
|
||||
@@ -1550,6 +1563,50 @@ fn alloc_rc_borrow_only_recursive_list_drop() {
|
||||
stdout_gc, stdout_rc,
|
||||
"alloc=rc must match alloc=gc on rc_list_drop_borrow"
|
||||
);
|
||||
|
||||
// Iter 18d.4 IR-shape assertion: the Cons arm of main's match
|
||||
// emits a drop on `t` after lowering the body. We re-lower under
|
||||
// rc and inspect main's IR — the arm sequence must contain
|
||||
// call void @ai/print_int(...) (the `print h`)
|
||||
// call void @drop_rc_list_drop_borrow_IntList(ptr %v...) (NEW: 18d.4 t-drop)
|
||||
// br label %mjoin.<id> (arm-close branch)
|
||||
// in that order, in the Cons arm's basic block.
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
|
||||
let json_path = workspace.join("examples").join(example);
|
||||
let ws = ailang_core::load_workspace(&json_path).expect("load workspace");
|
||||
let mut lifted_modules = std::collections::BTreeMap::new();
|
||||
for (mname, m) in &ws.modules {
|
||||
let desugared = ailang_core::desugar::desugar_module(m);
|
||||
let lifted = ailang_check::lift_letrecs(&desugared)
|
||||
.unwrap_or_else(|e| panic!("lift_letrecs in module `{mname}`: {e:?}"));
|
||||
lifted_modules.insert(mname.clone(), lifted);
|
||||
}
|
||||
let lifted_ws = ailang_core::Workspace {
|
||||
entry: ws.entry.clone(),
|
||||
modules: lifted_modules,
|
||||
root_dir: ws.root_dir.clone(),
|
||||
};
|
||||
let ir = ailang_codegen::lower_workspace_with_alloc(
|
||||
&lifted_ws,
|
||||
ailang_codegen::AllocStrategy::Rc,
|
||||
)
|
||||
.expect("lower workspace under rc");
|
||||
let main_start = ir
|
||||
.find("define i8 @ail_rc_list_drop_borrow_main(")
|
||||
.expect("main definition present");
|
||||
let main_end_offset = ir[main_start..]
|
||||
.find("\n}\n")
|
||||
.expect("main definition ends with `}`");
|
||||
let main_body = &ir[main_start..main_start + main_end_offset];
|
||||
// The arm-close drop must hit the per-type IntList drop fn (NOT
|
||||
// the shallow `ailang_rc_dec` fallback for dynamic-tag binders
|
||||
// with non-empty moved_slots — `t` has empty moved_slots in this
|
||||
// fixture because the arm body doesn't re-scrutinise it).
|
||||
assert!(
|
||||
main_body.contains("call void @drop_rc_list_drop_borrow_IntList(ptr "),
|
||||
"main's body must call `@drop_rc_list_drop_borrow_IntList(ptr ...)` for the unused Cons-arm `t` binder. main body was:\n{main_body}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18d.3: move-aware pattern bindings — let-close inlines a
|
||||
@@ -1641,3 +1698,103 @@ fn alloc_rc_partial_drop_skips_moved_keeps_wildcarded() {
|
||||
"main's body must call `@ailang_rc_dec(ptr ...)` for the outer Pair box at let-close. main body was:\n{main_body}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18d.4: Own-param dec at fn return. Symmetric to 18c.3/18c.4's
|
||||
/// `Term::Let`-scope-close drop emission and 18d.4's arm-close
|
||||
/// pattern-binder dec, fired at the lexical close of a fn body. The
|
||||
/// `head_or_zero` fn declares its only parameter `xs` as
|
||||
/// `(own (con IntList))` — the static "caller handed off ownership"
|
||||
/// signal. Under `--alloc=rc`, codegen now emits a drop call against
|
||||
/// the param SSA (`%arg_xs`) before the fn's `ret`.
|
||||
///
|
||||
/// Properties guarded:
|
||||
/// 1. The binary runs to completion under both `--alloc=gc` and
|
||||
/// `--alloc=rc` (no segfault from a mishandled cascade or
|
||||
/// refcount underflow when the cascade hits the moved tail).
|
||||
/// 2. Stdout matches `--alloc=gc` byte-for-byte (`11` — the head
|
||||
/// of the 5-element list).
|
||||
/// 3. `head_or_zero`'s body contains a drop call against
|
||||
/// `%arg_xs` BEFORE the `ret i64`. The drop may be the
|
||||
/// per-type `@drop_<m>_IntList(ptr %arg_xs)` (when no slots
|
||||
/// were moved out of `xs` — not the canonical case here) OR
|
||||
/// the inlined-partial fallback `@ailang_rc_dec(ptr %arg_xs)`
|
||||
/// (the canonical case here, since the Cons arm pattern-binds
|
||||
/// slot 1 into `t`, recording `moved_slots[xs] = {1}`). Either
|
||||
/// shape is acceptable per the iter brief; what matters is
|
||||
/// that ANY drop fires.
|
||||
#[test]
|
||||
fn alloc_rc_own_param_dec_at_fn_return() {
|
||||
let example = "rc_own_param_drop.ail.json";
|
||||
let stdout_gc = build_and_run_with_alloc(example, "gc");
|
||||
let stdout_rc = build_and_run_with_alloc(example, "rc");
|
||||
assert_eq!(stdout_gc.trim(), "11");
|
||||
assert_eq!(stdout_rc.trim(), "11");
|
||||
assert_eq!(
|
||||
stdout_gc, stdout_rc,
|
||||
"alloc=rc must match alloc=gc on rc_own_param_drop"
|
||||
);
|
||||
|
||||
// Re-lower under rc to inspect head_or_zero's IR shape.
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
|
||||
let json_path = workspace.join("examples").join(example);
|
||||
let ws = ailang_core::load_workspace(&json_path).expect("load workspace");
|
||||
let mut lifted_modules = std::collections::BTreeMap::new();
|
||||
for (mname, m) in &ws.modules {
|
||||
let desugared = ailang_core::desugar::desugar_module(m);
|
||||
let lifted = ailang_check::lift_letrecs(&desugared)
|
||||
.unwrap_or_else(|e| panic!("lift_letrecs in module `{mname}`: {e:?}"));
|
||||
lifted_modules.insert(mname.clone(), lifted);
|
||||
}
|
||||
let lifted_ws = ailang_core::Workspace {
|
||||
entry: ws.entry.clone(),
|
||||
modules: lifted_modules,
|
||||
root_dir: ws.root_dir.clone(),
|
||||
};
|
||||
let ir = ailang_codegen::lower_workspace_with_alloc(
|
||||
&lifted_ws,
|
||||
ailang_codegen::AllocStrategy::Rc,
|
||||
)
|
||||
.expect("lower workspace under rc");
|
||||
|
||||
// Locate head_or_zero's body in the IR.
|
||||
let fn_start = ir
|
||||
.find("define i64 @ail_rc_own_param_drop_head_or_zero(")
|
||||
.expect("head_or_zero definition present");
|
||||
let fn_end_offset = ir[fn_start..]
|
||||
.find("\n}\n")
|
||||
.expect("head_or_zero ends with `}`");
|
||||
let fn_body = &ir[fn_start..fn_start + fn_end_offset];
|
||||
|
||||
// The Own-param drop must fire on `%arg_xs` somewhere in
|
||||
// head_or_zero's body. Either the per-type drop (empty
|
||||
// moved_slots case) or the shallow `ailang_rc_dec` (non-empty
|
||||
// moved_slots case) is acceptable. The canonical fixture here
|
||||
// hits the latter: the Cons arm moves slot 1 into the arm-bound
|
||||
// `t`, so moved_slots[xs] = {1} at fn return.
|
||||
let has_per_type_drop =
|
||||
fn_body.contains("call void @drop_rc_own_param_drop_IntList(ptr %arg_xs)");
|
||||
let has_shallow_dec = fn_body.contains("call void @ailang_rc_dec(ptr %arg_xs)");
|
||||
assert!(
|
||||
has_per_type_drop || has_shallow_dec,
|
||||
"head_or_zero's body must drop the Own-mode param `xs` (either via per-type drop or shallow dec) before the fn's `ret`. Body was:\n{fn_body}"
|
||||
);
|
||||
|
||||
// The drop must precede the fn's `ret`. Scope: any `ret i64 ...`
|
||||
// in the body comes AFTER the drop call site. We check by
|
||||
// splitting on the `ret` instruction and ensuring at least one
|
||||
// drop call occurred before each ret.
|
||||
//
|
||||
// Implementation note: the body has a single `ret` (the join
|
||||
// block's value) since `head_or_zero` returns Int unconditionally
|
||||
// at the body root. The drop is emitted right before that ret
|
||||
// by the Iter 18d.4 emit_fn pre-ret seam.
|
||||
let ret_idx = fn_body.find("\n ret i64").expect("head_or_zero has a ret i64");
|
||||
let pre_ret = &fn_body[..ret_idx];
|
||||
let drop_in_pre_ret = pre_ret.contains("call void @drop_rc_own_param_drop_IntList(ptr %arg_xs)")
|
||||
|| pre_ret.contains("call void @ailang_rc_dec(ptr %arg_xs)");
|
||||
assert!(
|
||||
drop_in_pre_ret,
|
||||
"head_or_zero's `%arg_xs` drop must appear BEFORE the `ret i64` instruction. Pre-ret slice was:\n{pre_ret}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1037,8 +1037,19 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
|
||||
fn emit_fn(&mut self, f: &FnDef) -> Result<()> {
|
||||
let (param_tys, ret_ty) = match &f.ty {
|
||||
Type::Fn { params, ret, .. } => (params.clone(), (**ret).clone()),
|
||||
// Iter 18d.4: also lift `param_modes` out of the fn type. The
|
||||
// fn-return Own-param dec emission below consults it to decide
|
||||
// which params get a drop call before `ret`. `Implicit`
|
||||
// entries (legacy / unannotated) and `Borrow` entries are
|
||||
// skipped — only `Own` carries the static "caller handed off
|
||||
// ownership" signal.
|
||||
let (param_tys, ret_ty, param_modes) = match &f.ty {
|
||||
Type::Fn {
|
||||
params,
|
||||
ret,
|
||||
param_modes,
|
||||
..
|
||||
} => (params.clone(), (**ret).clone(), param_modes.clone()),
|
||||
other => {
|
||||
return Err(CodegenError::NotFnType(
|
||||
ailang_core::pretty::type_to_string(other),
|
||||
@@ -1112,6 +1123,93 @@ impl<'a> Emitter<'a> {
|
||||
f.name
|
||||
)));
|
||||
}
|
||||
// Iter 18d.4: fn-return Own-param dec. Symmetric to
|
||||
// 18c.3/18c.4's `Term::Let`-scope-close drop and 18d.4's
|
||||
// arm-close pattern-binder dec, fired at the lexical
|
||||
// close of a fn body. For each parameter with
|
||||
// `ParamMode::Own`, emit a drop call iff:
|
||||
// - alloc strategy is `Rc`,
|
||||
// - the parameter's lowered type is `ptr`,
|
||||
// - uniqueness inference recorded `consume_count == 0`
|
||||
// for the param in this fn's body (no internal use
|
||||
// consumed it; the param's slot owns the only ref the
|
||||
// callee received from the caller's hand-off),
|
||||
// - the param's SSA is not the body's tail value
|
||||
// (returning the param transfers ownership back to
|
||||
// the caller's frame; caller dec's, not us),
|
||||
// - the current block is still open.
|
||||
//
|
||||
// `Implicit`-mode params do NOT get this dec: they have
|
||||
// no static "caller handed off ownership" signal —
|
||||
// emitting a dec here might double-dec a value the caller
|
||||
// also dec's. `Borrow`-mode params definitely don't get
|
||||
// dec'd (the caller still owns them).
|
||||
//
|
||||
// Closes the 18c.3/18c.4 carve-out: "fn parameters still
|
||||
// don't get dec'd at fn return — the caller-handed-off-
|
||||
// ownership signal is the `(own T)` mode, but wiring it
|
||||
// through codegen is part of the wider mode-aware story."
|
||||
if matches!(self.alloc, AllocStrategy::Rc) {
|
||||
for (i, ((pname, plty), pty_ail)) in f
|
||||
.params
|
||||
.iter()
|
||||
.zip(llvm_param_tys.iter())
|
||||
.zip(param_tys.iter())
|
||||
.enumerate()
|
||||
{
|
||||
if plty != "ptr" {
|
||||
continue;
|
||||
}
|
||||
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
|
||||
if !matches!(mode, ParamMode::Own) {
|
||||
continue;
|
||||
}
|
||||
let consume_count = self
|
||||
.uniqueness
|
||||
.get(&(self.current_def.clone(), pname.clone()))
|
||||
.map(|info| info.consume_count)
|
||||
.unwrap_or(u32::MAX);
|
||||
if consume_count != 0 {
|
||||
continue;
|
||||
}
|
||||
let p_ssa = format!("%arg_{}", pname);
|
||||
if val == p_ssa {
|
||||
// The param IS the fn's return value —
|
||||
// ownership transfers back to the caller.
|
||||
continue;
|
||||
}
|
||||
let moves = self
|
||||
.moved_slots
|
||||
.get(pname)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if moves.is_empty() {
|
||||
// Route through the per-type drop fn for the
|
||||
// param's static type. `field_drop_call`
|
||||
// resolves `Type::Con` to `drop_<owner>_<T>`
|
||||
// and falls back to `ailang_rc_dec` for
|
||||
// closure / Var fields — closure-typed Own
|
||||
// params therefore use the same shallow free
|
||||
// 18c.4 set up for closure-typed ADT fields,
|
||||
// matching the iter brief's "closure-typed
|
||||
// Own params follow whichever debt path
|
||||
// 18c.4 set up" carve-out.
|
||||
let drop_call = self.field_drop_call(pty_ail);
|
||||
self.body.push_str(&format!(
|
||||
" call void @{drop_call}(ptr {p_ssa})\n"
|
||||
));
|
||||
} else {
|
||||
// Iter 18d.4 debt: dynamic-tag partial-drop —
|
||||
// see the symmetric arm-close branch in
|
||||
// `lower_match` for the rationale. Falls back
|
||||
// to shallow `ailang_rc_dec` of the outer
|
||||
// cell.
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {p_ssa})\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.body
|
||||
.push_str(&format!(" ret {val_ty} {val}\n}}\n\n"));
|
||||
} else {
|
||||
@@ -2324,7 +2422,13 @@ impl<'a> Emitter<'a> {
|
||||
// body closes (these binders are themselves freshly named
|
||||
// here — their move tracking starts empty and ends at arm
|
||||
// body close).
|
||||
let mut arm_pushed_binders: Vec<String> = Vec::new();
|
||||
//
|
||||
// Iter 18d.4: the metadata is widened to (name, SSA,
|
||||
// llvm-type, AILang-type) so that the arm-close drop
|
||||
// emission can route the call through the binder's
|
||||
// per-type drop symbol without re-walking `self.locals`.
|
||||
// The same tuple shape `self.locals` carries.
|
||||
let mut arm_pushed_binders: Vec<(String, String, String, Type)> = Vec::new();
|
||||
for (idx, binding) in bindings.iter().enumerate() {
|
||||
if let Some(bname) = binding {
|
||||
let raw_ail = cref.ail_fields.get(idx).cloned().unwrap_or(Type::unit());
|
||||
@@ -2364,12 +2468,97 @@ impl<'a> Emitter<'a> {
|
||||
.insert(idx);
|
||||
}
|
||||
}
|
||||
self.locals.push((bname.clone(), v, fty, bind_ail));
|
||||
arm_pushed_binders.push(bname.clone());
|
||||
self.locals.push((
|
||||
bname.clone(),
|
||||
v.clone(),
|
||||
fty.clone(),
|
||||
bind_ail.clone(),
|
||||
));
|
||||
arm_pushed_binders.push((bname.clone(), v, fty, bind_ail));
|
||||
pushed += 1;
|
||||
}
|
||||
}
|
||||
let (val, vty) = self.lower_term(&arm.body)?;
|
||||
// Iter 18d.4: arm-close pattern-binder dec. Symmetric to
|
||||
// 18c.3/18c.4's `Term::Let`-scope-close drop emission, but
|
||||
// fired at the lexical close of a match arm. For each
|
||||
// pattern-bound binder this arm pushed, emit a drop call
|
||||
// iff:
|
||||
// - alloc strategy is `Rc`,
|
||||
// - the binder's lowered type is `ptr` (heap-allocated),
|
||||
// - uniqueness inference recorded `consume_count == 0`
|
||||
// for the binder in this fn's body (no downstream use
|
||||
// consumed it; the binder's slot owns the only ref),
|
||||
// - the arm's tail SSA value is not the binder itself
|
||||
// (returning the binder transfers ownership to the
|
||||
// phi-join consumer; that consumer dec's, not us),
|
||||
// - the current block is still open (a tail-call /
|
||||
// `unreachable` already exited; nothing to emit).
|
||||
//
|
||||
// Closes the 18d.3-shipped regression on
|
||||
// `alloc_rc_borrow_only_recursive_list_drop`: the `t`
|
||||
// pattern-binder of `(Cons h t)` is unused, has
|
||||
// `consume_count == 0`, and previously leaked because
|
||||
// 18d.3's `moved_slots` interrupts the `xs`-cascade
|
||||
// through the tail slot. Now the arm itself dec's `t`,
|
||||
// freeing the 4-cell tail.
|
||||
if matches!(self.alloc, AllocStrategy::Rc) && !self.block_terminated {
|
||||
for (bname, b_ssa, b_lty, b_ail) in &arm_pushed_binders {
|
||||
if b_lty != "ptr" {
|
||||
continue;
|
||||
}
|
||||
let consume_count = self
|
||||
.uniqueness
|
||||
.get(&(self.current_def.clone(), bname.clone()))
|
||||
.map(|info| info.consume_count)
|
||||
.unwrap_or(u32::MAX);
|
||||
if consume_count != 0 {
|
||||
continue;
|
||||
}
|
||||
if &val == b_ssa {
|
||||
// The binder IS the arm's tail value —
|
||||
// ownership transfers to the join consumer.
|
||||
continue;
|
||||
}
|
||||
let moves = self
|
||||
.moved_slots
|
||||
.get(bname)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if moves.is_empty() {
|
||||
// Common case (canonical regression `t`):
|
||||
// route through the per-type drop fn for the
|
||||
// binder's static type. `field_drop_call`
|
||||
// resolves `Type::Con` to `drop_<owner>_<T>`
|
||||
// and falls back to `ailang_rc_dec` for
|
||||
// closure / Var fields (same fallback the
|
||||
// recursive cascade uses).
|
||||
let drop_call = self.field_drop_call(b_ail);
|
||||
self.body.push_str(&format!(
|
||||
" call void @{drop_call}(ptr {b_ssa})\n"
|
||||
));
|
||||
} else {
|
||||
// Iter 18d.4 debt: pattern-binder with
|
||||
// statically-recorded moved slots requires
|
||||
// tag-conditional partial-drop emission. The
|
||||
// 18d.3 `emit_inlined_partial_drop` helper
|
||||
// assumes a static ctor (it's keyed against a
|
||||
// `Term::Ctor` node), but a pattern-bound
|
||||
// binder's runtime tag is dynamic. Until a
|
||||
// dynamic-tag-aware partial-drop lands, fall
|
||||
// back to a shallow `ailang_rc_dec` of the
|
||||
// outer cell. The non-moved fields of the
|
||||
// active ctor (if any) leak under this path —
|
||||
// the canonical 18d.4 fixtures avoid this
|
||||
// case by construction (the regression `t`
|
||||
// is never re-scrutinised inside the arm,
|
||||
// so its `moved_slots` entry is empty).
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {b_ssa})\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// pop bindings
|
||||
for _ in 0..pushed {
|
||||
self.locals.pop();
|
||||
@@ -2377,7 +2566,7 @@ impl<'a> Emitter<'a> {
|
||||
// Iter 18d.3: arm-bound binders go out of scope at arm
|
||||
// body close. Drop their move-tracking entries (any moves
|
||||
// recorded against `h`/`t` belonged to this arm only).
|
||||
for bname in &arm_pushed_binders {
|
||||
for (bname, _, _, _) in &arm_pushed_binders {
|
||||
self.moved_slots.remove(bname);
|
||||
}
|
||||
// Iter 14e: if the arm body lowered to a `musttail call` +
|
||||
|
||||
@@ -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