From ba63a16366f0e841faf7f40201811b21fda8f5f7 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 8 May 2026 12:33:48 +0200 Subject: [PATCH] Iter 18d.4: pattern-binder + Own-param dec at scope close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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__ 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. --- crates/ail/tests/e2e.rs | 183 +++++++++++++++++++++++-- crates/ailang-codegen/src/lib.rs | 201 +++++++++++++++++++++++++++- examples/rc_own_param_drop.ail.json | 1 + examples/rc_own_param_drop.ailx | 62 +++++++++ 4 files changed, 428 insertions(+), 19 deletions(-) create mode 100644 examples/rc_own_param_drop.ail.json create mode 100644 examples/rc_own_param_drop.ailx diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index b4a1843..84fb8a6 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -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__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__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. (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__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}" + ); +} diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 4333c46..8ad2d53 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -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__` + // 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 = 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__` + // 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` + diff --git a/examples/rc_own_param_drop.ail.json b/examples/rc_own_param_drop.ail.json new file mode 100644 index 0000000..b51d58f --- /dev/null +++ b/examples/rc_own_param_drop.ail.json @@ -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"} \ No newline at end of file diff --git a/examples/rc_own_param_drop.ailx b/examples/rc_own_param_drop.ailx new file mode 100644 index 0000000..89b6f54 --- /dev/null +++ b/examples/rc_own_param_drop.ailx @@ -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__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))))))