codegen rc: tag-conditional partial-drop helper closes 3 carve-outs
Three sibling sites previously fell back to shallow `ailang_rc_dec` when a binder had non-empty `moved_slots` and a dynamic runtime ctor tag: Iter B Own-param dec at fn-return (lib.rs), Iter A arm-close pattern-binder dec (match_lower.rs), and emit_inlined_partial_drop's non-Ctor branch (drop.rs). Shallow freed the outer cell but did not walk the unmoved ptr fields — silent leak. Adds `emit_partial_drop_fn_for_type`: a per-ADT helper structurally parallel to `emit_drop_fn_for_type` but taking an i64 moved-slots bitmask. Each ptr-field's dec is gated on the corresponding mask bit; the outer box is dec'd at join. Emitted alongside the per-type drop fn for every ADT under --alloc=rc. Three call sites rewritten to resolve the partial_drop symbol from the binder's static type and build the mask from `moved_slots`. Three RED-then-GREEN fixtures (rc_own_param_/rc_match_arm_/ rc_app_let_partial_drop_leak) pin the carve-outs in regression coverage; live cell counts go from 3/1/3 (pre-fix) to 0/0/0. e2e count: 66 → 69. The pre-existing `alloc_rc_own_param_dec_at_fn_return` IR-shape assertion is widened to accept `partial_drop_<m>_<T>(%arg_xs, i64 mask)` as a third valid drop shape (the canonical fu2 case). Closes the JOURNAL queue's only remaining iter; the "wait for organic fixture" stance from the 18g tidy is retracted — known leaks are bugs, CLAUDE.md's TDD rule covers them autonomously.
This commit is contained in:
+140
-7
@@ -1767,17 +1767,27 @@ fn alloc_rc_own_param_dec_at_fn_return() {
|
||||
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.
|
||||
// head_or_zero's body. Three acceptable shapes:
|
||||
// - per-type drop `@drop_<m>_IntList(ptr %arg_xs)` (empty
|
||||
// moved_slots case),
|
||||
// - tag-conditional partial-drop
|
||||
// `@partial_drop_<m>_IntList(ptr %arg_xs, i64 <mask>)`
|
||||
// (Iter 18g.tidy.fu2: non-empty moved_slots, dynamic-tag),
|
||||
// - shallow `@ailang_rc_dec(ptr %arg_xs)` (legacy fallback;
|
||||
// the typechecker accepts non-ADT param types but moved_slots
|
||||
// cannot populate for those, so this branch is dead in
|
||||
// practice).
|
||||
// The canonical fixture here hits the partial-drop shape: the
|
||||
// Cons arm moves slot 1 into the arm-bound `t`, so
|
||||
// moved_slots[xs] = {1} at fn return → mask=2.
|
||||
let has_per_type_drop =
|
||||
fn_body.contains("call void @drop_rc_own_param_drop_IntList(ptr %arg_xs)");
|
||||
let has_partial_drop = fn_body
|
||||
.contains("call void @partial_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}"
|
||||
has_per_type_drop || has_partial_drop || has_shallow_dec,
|
||||
"head_or_zero's body must drop the Own-mode param `xs` (per-type, partial-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 ...`
|
||||
@@ -1792,6 +1802,8 @@ fn alloc_rc_own_param_dec_at_fn_return() {
|
||||
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 @partial_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,
|
||||
@@ -2239,3 +2251,124 @@ fn alloc_rc_let_alias_of_implicit_param_does_not_dec_borrowed_children() {
|
||||
"alloc=rc must match alloc=gc on rc_let_alias_implicit_param"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2 regression — RED-then-GREEN for the
|
||||
/// dynamic-tag partial-drop carve-out at fn-return.
|
||||
///
|
||||
/// Three carve-out sites previously fell back to a shallow
|
||||
/// `ailang_rc_dec` when a binder's `moved_slots` was non-empty
|
||||
/// and the runtime ctor tag was dynamic (not statically known
|
||||
/// from a `Term::Ctor` value). The shallow dec frees the outer
|
||||
/// cell but does not walk the unmoved ptr fields — those leak
|
||||
/// silently.
|
||||
///
|
||||
/// **Site 1 (this fixture): Iter B Own-param dec at fn return
|
||||
/// (lib.rs).**
|
||||
///
|
||||
/// (fn use_first
|
||||
/// (params (own (con Pair)))
|
||||
/// (body
|
||||
/// (match p
|
||||
/// (case (pat-ctor MkPair a _) 1))))
|
||||
///
|
||||
/// `p` is consumed by the match. Its `consume_count == 0`,
|
||||
/// `mode == Own`, and `moved_slots[p] = {0}` (slot 0 was bound,
|
||||
/// slot 1 wildcarded). At fn-return, Iter B's dynamic-tag fallback
|
||||
/// fired shallow `ailang_rc_dec(p)`, freeing p's outer Pair but
|
||||
/// leaking the unbound second `MkCell` and its two `MkWrap`
|
||||
/// children — three cells.
|
||||
///
|
||||
/// fu2 routes the carve-out through `partial_drop_<m>_Pair(p,
|
||||
/// mask)`, which dispatches on the runtime tag and dec's slot 1
|
||||
/// (the unmoved field) via `drop_<m>_Cell`, cascading correctly.
|
||||
///
|
||||
/// Pre-fix: `live = 3`. Post-fix: `live = 0`.
|
||||
#[test]
|
||||
fn alloc_rc_partial_drop_at_fn_return_does_not_leak_unmoved_fields() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("rc_own_param_partial_drop_leak.ail.json");
|
||||
assert_eq!(
|
||||
stdout.trim(),
|
||||
"1",
|
||||
"use_first returns 1 regardless of leak status"
|
||||
);
|
||||
assert_eq!(
|
||||
live, 0,
|
||||
"Own-param fn-return partial-drop carve-out leaked {live} cells \
|
||||
(allocs={allocs} frees={frees}); fu2 must route the dynamic-tag \
|
||||
fallback through `partial_drop_<m>_<T>(p, mask)` so the unmoved \
|
||||
slots dec via the per-type cascade rather than being shallow-freed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2 regression — site 2: Iter A arm-close
|
||||
/// pattern-binder dec (match_lower.rs).
|
||||
///
|
||||
/// An outer match's arm-bound pattern-binder may itself be the
|
||||
/// scrutinee of an inner match that moves out some of its fields.
|
||||
/// At outer-arm close, `moved_slots[bname]` is non-empty and
|
||||
/// `consume_count[bname] == 0` — the carve-out previously emitted
|
||||
/// a shallow `ailang_rc_dec`, leaking the inner ctor's unbound
|
||||
/// (wildcarded) ptr fields.
|
||||
///
|
||||
/// (match p
|
||||
/// (case (pat-ctor MkPair a b)
|
||||
/// (match a
|
||||
/// (case (pat-ctor MkCell w1 _) 1))))
|
||||
///
|
||||
/// `a` has `moves={0}` (w1 bound, slot 1 of MkCell wildcarded).
|
||||
/// Pre-fix: shallow dec on `a` left slot 1's `MkWrap(2)` cell
|
||||
/// allocated.
|
||||
/// Post-fix: `partial_drop_<m>_Cell(a, mask=001)` dec's slot 1.
|
||||
///
|
||||
/// Pre-fix: `live = 1`. Post-fix: `live = 0`.
|
||||
#[test]
|
||||
fn alloc_rc_partial_drop_at_arm_close_does_not_leak_unmoved_fields() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("rc_match_arm_partial_drop_leak.ail.json");
|
||||
assert_eq!(
|
||||
stdout.trim(),
|
||||
"1",
|
||||
"use_first returns 1 regardless of leak status"
|
||||
);
|
||||
assert_eq!(
|
||||
live, 0,
|
||||
"match-arm pattern-binder partial-drop carve-out leaked {live} cells \
|
||||
(allocs={allocs} frees={frees}); fu2 must route arm-bound binders \
|
||||
with non-empty moved_slots through `partial_drop_<m>_<T>(b, mask)`"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2 regression — site 3: let-close for an
|
||||
/// App-bound binder (drop.rs `emit_inlined_partial_drop`
|
||||
/// non-Ctor branch).
|
||||
///
|
||||
/// `emit_inlined_partial_drop` was keyed against `Term::Ctor`
|
||||
/// values; for `Term::App` (Own-returning) values its fallback
|
||||
/// shallow-dec'd the binder. With pattern-matching of the
|
||||
/// App-bound binder (`(let p (app build_pair) (match p ...))`),
|
||||
/// the body's `moved_slots[p]` is non-empty but the binder's
|
||||
/// runtime tag is dynamic (unknown from the App's surface).
|
||||
///
|
||||
/// fu2 derives the type from `synth_arg_type(value)` and routes
|
||||
/// through `partial_drop_<m>_<T>(p, mask)`.
|
||||
///
|
||||
/// Pre-fix: `live = 3` (slot 1's whole MkCell + two MkWraps).
|
||||
/// Post-fix: `live = 0`.
|
||||
#[test]
|
||||
fn alloc_rc_partial_drop_at_app_let_close_does_not_leak_unmoved_fields() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("rc_app_let_partial_drop_leak.ail.json");
|
||||
assert_eq!(
|
||||
stdout.trim(),
|
||||
"1",
|
||||
"use_cell returns 1 regardless of leak status"
|
||||
);
|
||||
assert_eq!(
|
||||
live, 0,
|
||||
"App-bound let-close partial-drop carve-out leaked {live} cells \
|
||||
(allocs={allocs} frees={frees}); fu2 must derive the binder's \
|
||||
static type from synth_arg_type(value) and route through \
|
||||
`partial_drop_<m>_<T>(p, mask)`"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -538,6 +538,193 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2: tag-conditional partial-drop helper.
|
||||
/// Emits `void @partial_drop_<m>_<T>(ptr %p, i64 %mask)` —
|
||||
/// structurally parallel to [`Self::emit_drop_fn_for_type`] but
|
||||
/// gates each ptr-field dec on a bit of `%mask`. If bit j of the
|
||||
/// mask is set, slot j was moved out of the cell (its content
|
||||
/// belongs to a downstream owner) and the dec is skipped; if the
|
||||
/// bit is clear, the field is loaded and routed through
|
||||
/// `field_drop_call` (same dispatch the recursive cascade uses).
|
||||
/// The outer box is always dec'd at `join`.
|
||||
///
|
||||
/// Closes the three dynamic-tag carve-outs that previously
|
||||
/// fell back to shallow `ailang_rc_dec`:
|
||||
/// 1. `lib.rs` Iter B Own-param dec at fn-return when
|
||||
/// `moved_slots[param]` is non-empty;
|
||||
/// 2. `match_lower.rs` Iter A arm-close pattern-binder dec
|
||||
/// when the binder was itself the scrutinee of an inner
|
||||
/// match that moved out some fields;
|
||||
/// 3. [`Self::emit_inlined_partial_drop`]'s non-Ctor branch
|
||||
/// (let-binder whose value is `Term::App`).
|
||||
/// All three share the same shape: a binder whose runtime ctor
|
||||
/// tag is dynamic (not statically known from a `Term::Ctor`)
|
||||
/// and whose `moved_slots` is a strict subset of its ptr fields.
|
||||
/// Pre-fu2 the unmoved fields leaked silently; fu2 routes them
|
||||
/// through this helper.
|
||||
///
|
||||
/// IR shape (analogous to `emit_drop_fn_for_type`):
|
||||
/// ```text
|
||||
/// define void @partial_drop_<m>_<T>(ptr %p, i64 %mask) {
|
||||
/// %is_null = icmp eq ptr %p, null
|
||||
/// br i1 %is_null, label %ret, label %live
|
||||
/// live:
|
||||
/// %tag = load i64, ptr %p
|
||||
/// switch i64 %tag, label %dflt [...]
|
||||
/// arm_i:
|
||||
/// ; for each ptr field f_j of ctor i:
|
||||
/// %b_j = and i64 %mask, (1 << j)
|
||||
/// %s_j = icmp ne i64 %b_j, 0
|
||||
/// br i1 %s_j, label %after_i_j, label %do_i_j
|
||||
/// do_i_j:
|
||||
/// %a = gep i8, ptr %p, (8 + 8*j)
|
||||
/// %v = load ptr, ptr %a
|
||||
/// call void @<field_drop_call>(ptr %v)
|
||||
/// br label %after_i_j
|
||||
/// after_i_j:
|
||||
/// ; next field, or br label %join
|
||||
/// dflt: unreachable
|
||||
/// join:
|
||||
/// call void @ailang_rc_dec(ptr %p)
|
||||
/// br label %ret
|
||||
/// ret:
|
||||
/// ret void
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// One helper per ADT, emitted alongside `drop_<m>_<T>`. We do
|
||||
/// NOT emit a `(drop-iterative)` partial-drop variant: the
|
||||
/// helper runs once on the binder (carve-out sites are not
|
||||
/// cascade points — the unmoved fields go through their own
|
||||
/// `drop_<m>_<F>` which itself decides recursive vs iterative).
|
||||
pub(crate) fn emit_partial_drop_fn_for_type(&mut self, td: &TypeDef) {
|
||||
let m = self.module_name;
|
||||
let tname = &td.name;
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!(
|
||||
"define void @partial_drop_{m}_{tname}(ptr %p, i64 %mask) {{\n"
|
||||
));
|
||||
out.push_str("entry:\n");
|
||||
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
||||
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
||||
out.push_str("live:\n");
|
||||
out.push_str(" %tag = load i64, ptr %p, align 8\n");
|
||||
|
||||
let n_ctors = td.ctors.len();
|
||||
out.push_str(" switch i64 %tag, label %dflt [\n");
|
||||
for (i, _) in td.ctors.iter().enumerate() {
|
||||
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
|
||||
}
|
||||
out.push_str(" ]\n");
|
||||
|
||||
let mut local = 0u64;
|
||||
for (i, ctor) in td.ctors.iter().enumerate() {
|
||||
out.push_str(&format!("arm_{i}:\n"));
|
||||
for (j, fty) in ctor.fields.iter().enumerate() {
|
||||
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
|
||||
if lty != "ptr" {
|
||||
continue;
|
||||
}
|
||||
let bit_id = local;
|
||||
local += 1;
|
||||
let set_id = local;
|
||||
local += 1;
|
||||
let bitmask: u64 = 1u64 << j;
|
||||
out.push_str(&format!(
|
||||
" %b{bit_id} = and i64 %mask, {bitmask}\n"
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" %s{set_id} = icmp ne i64 %b{bit_id}, 0\n"
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" br i1 %s{set_id}, label %after_{i}_{j}, label %do_{i}_{j}\n"
|
||||
));
|
||||
out.push_str(&format!("do_{i}_{j}:\n"));
|
||||
let off = 8 + (j as i64) * 8;
|
||||
let addr_id = local;
|
||||
local += 1;
|
||||
let val_id = local;
|
||||
local += 1;
|
||||
out.push_str(&format!(
|
||||
" %a{addr_id} = getelementptr inbounds i8, ptr %p, i64 {off}\n"
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
||||
));
|
||||
let drop_call = self.field_drop_call(fty);
|
||||
out.push_str(&format!(
|
||||
" call void @{drop_call}(ptr %v{val_id})\n"
|
||||
));
|
||||
out.push_str(&format!(" br label %after_{i}_{j}\n"));
|
||||
out.push_str(&format!("after_{i}_{j}:\n"));
|
||||
}
|
||||
out.push_str(" br label %join\n");
|
||||
}
|
||||
|
||||
out.push_str("dflt:\n");
|
||||
if n_ctors == 0 {
|
||||
out.push_str(" br label %join\n");
|
||||
} else {
|
||||
out.push_str(" unreachable\n");
|
||||
}
|
||||
|
||||
out.push_str("join:\n");
|
||||
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
|
||||
out.push_str(" br label %ret\n");
|
||||
out.push_str("ret:\n");
|
||||
out.push_str(" ret void\n");
|
||||
out.push_str("}\n\n");
|
||||
self.body.push_str(&out);
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2: resolve the `partial_drop_<owner>_<T>`
|
||||
/// symbol for a type, parallel to the existing per-type drop
|
||||
/// dispatch in `field_drop_call`. Returns `None` for non-ADT
|
||||
/// types (Str, fn-typed, type-vars) — callers fall back to
|
||||
/// shallow `ailang_rc_dec` in those cases (no per-type drop
|
||||
/// fn exists either, and `moved_slots` would not have been
|
||||
/// populated for those shapes anyway).
|
||||
pub(crate) fn partial_drop_symbol_for_type(&self, ty: &Type) -> Option<String> {
|
||||
match ty {
|
||||
Type::Con { name, .. } => {
|
||||
if name == "Str" {
|
||||
return None;
|
||||
}
|
||||
if name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) = name.split_once('.').expect("checked");
|
||||
if let Some(target) = self.import_map.get(prefix) {
|
||||
return Some(format!("partial_drop_{target}_{suffix}"));
|
||||
}
|
||||
return Some(format!("partial_drop_{prefix}_{suffix}"));
|
||||
}
|
||||
Some(format!(
|
||||
"partial_drop_{m}_{name}",
|
||||
m = self.module_name
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18g.tidy.fu2: build the i64 moved-slots bitmask for a
|
||||
/// `partial_drop_<m>_<T>` call. Bit j is set iff slot j is in
|
||||
/// `moved`. We reject slot indices ≥ 64 with `None` — no
|
||||
/// language-level ADT has that many fields, but the cap is
|
||||
/// load-bearing because the helper's mask is a single i64.
|
||||
/// On overflow, callers fall back to shallow dec (correctness-
|
||||
/// safe; carve-out leak path remains as before, but the cap is
|
||||
/// effectively unreachable).
|
||||
pub(crate) fn build_moved_mask(moved: &BTreeSet<usize>) -> Option<u64> {
|
||||
let mut mask: u64 = 0;
|
||||
for &idx in moved {
|
||||
if idx >= 64 {
|
||||
return None;
|
||||
}
|
||||
mask |= 1u64 << idx;
|
||||
}
|
||||
Some(mask)
|
||||
}
|
||||
|
||||
/// Iter 18d.3: emit a partial-drop sequence inline at a let-close
|
||||
/// site whose binder has moved-out pattern slots. Replaces the
|
||||
/// uniform `drop_<m>_<T>(ptr)` call: load each pointer-typed
|
||||
@@ -548,15 +735,15 @@ impl<'a> Emitter<'a> {
|
||||
/// transferred to a pattern-bound binder that owns the dec
|
||||
/// for them.
|
||||
///
|
||||
/// Iter 18g.2 widens the input set: with `Term::App` (Own-
|
||||
/// returning call) now also trackable, a let-binder whose value is
|
||||
/// `Term::App` may accumulate moved slots if its body pattern-
|
||||
/// matches the binder. The per-field static-ctor emission below
|
||||
/// is not usable in that case (the runtime tag is dynamic, not
|
||||
/// statically known from `value`'s shape); fall back to shallow
|
||||
/// `ailang_rc_dec`. The non-moved field cells leak under that
|
||||
/// path — same dynamic-tag partial-drop debt that 18d.4's match
|
||||
/// arm already documents.
|
||||
/// Iter 18g.2 widened the input set to `Term::App` (Own-
|
||||
/// returning call). The static per-field emission below is keyed
|
||||
/// against a `Term::Ctor`'s known ctor; a `Term::App` binder
|
||||
/// whose body pattern-matches it has a *dynamic* runtime tag.
|
||||
/// Iter 18g.tidy.fu2 closes that path: instead of falling back
|
||||
/// to shallow `ailang_rc_dec` (which leaked the unmoved fields),
|
||||
/// we route through the tag-conditional helper
|
||||
/// [`Self::emit_partial_drop_fn_for_type`], which dispatches on
|
||||
/// the runtime tag and dec's only the unmoved fields.
|
||||
pub(crate) fn emit_inlined_partial_drop(
|
||||
&mut self,
|
||||
value: &Term,
|
||||
@@ -566,21 +753,29 @@ impl<'a> Emitter<'a> {
|
||||
let (type_name, ctor_name) = match value {
|
||||
Term::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()),
|
||||
_ => {
|
||||
// Iter 18g.2 fallback: dynamic-tag partial-drop debt.
|
||||
// `value` is `Term::App` (Own-returning) or `Term::Lam`
|
||||
// — `is_rc_heap_allocated` returns true for both, and a
|
||||
// body that pattern-matches such a binder will populate
|
||||
// `moved_slots`. We don't know the active ctor
|
||||
// statically, so we shallow-dec the outer cell. Non-
|
||||
// moved field cells leak under this path; closing the
|
||||
// leak requires a tag-conditional partial-drop helper
|
||||
// (analogous to the per-type drop fn but parameterised
|
||||
// on the tag). Tracked as future work — same family as
|
||||
// 18d.4's `(case (LCons h t) ...)` carve-out.
|
||||
let _ = (val_ssa, moved); // keep compiler quiet about unused
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {val_ssa})\n"
|
||||
));
|
||||
// Iter 18g.tidy.fu2: dynamic-tag partial-drop via the
|
||||
// per-type helper. `value` is `Term::App` (Own-
|
||||
// returning) — the binder's static type is the App's
|
||||
// ret type, recovered through `synth_arg_type` /
|
||||
// `partial_drop_symbol_for_type`. `Term::Lam` shapes
|
||||
// never reach here with a non-empty `moved` (you can't
|
||||
// pattern-match a closure-pair); the fallback below
|
||||
// handles them defensively.
|
||||
let sym = self
|
||||
.synth_arg_type(value)
|
||||
.ok()
|
||||
.and_then(|ty| self.partial_drop_symbol_for_type(&ty));
|
||||
if let (Some(sym), Some(mask)) =
|
||||
(sym, Self::build_moved_mask(moved))
|
||||
{
|
||||
self.body.push_str(&format!(
|
||||
" call void @{sym}(ptr {val_ssa}, i64 {mask})\n"
|
||||
));
|
||||
} else {
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {val_ssa})\n"
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -800,6 +800,16 @@ impl<'a> Emitter<'a> {
|
||||
} else {
|
||||
self.emit_drop_fn_for_type(td);
|
||||
}
|
||||
// Iter 18g.tidy.fu2: tag-conditional partial-drop
|
||||
// helper alongside the recursive/iterative drop fn.
|
||||
// Used at carve-out sites where a binder's runtime
|
||||
// tag is dynamic and `moved_slots` is a strict
|
||||
// subset of its ptr fields. Same shape regardless
|
||||
// of `(drop-iterative)` (the helper is single-shot
|
||||
// on the binder; field cascades go through their
|
||||
// own drop fns which themselves choose recursive
|
||||
// vs iterative).
|
||||
self.emit_partial_drop_fn_for_type(td);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1085,14 +1095,30 @@ impl<'a> Emitter<'a> {
|
||||
" 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"
|
||||
));
|
||||
// Iter 18g.tidy.fu2: dynamic-tag partial-drop
|
||||
// via the per-type helper. The param's runtime
|
||||
// tag is dynamic but its static type is known
|
||||
// (`pty_ail`), so we route through
|
||||
// `partial_drop_<owner>_<T>(p, mask)` which
|
||||
// dispatches on the runtime tag and dec's only
|
||||
// the unmoved fields. The fallback to shallow
|
||||
// `ailang_rc_dec` only fires for non-ADT param
|
||||
// types (Str, fn-typed, vars) — none of which
|
||||
// can populate `moved_slots` in practice, so
|
||||
// the fallback is dead under the typechecker.
|
||||
let sym = Self::partial_drop_symbol_for_type(
|
||||
self, pty_ail,
|
||||
);
|
||||
let mask = Self::build_moved_mask(&moves);
|
||||
if let (Some(sym), Some(mask)) = (sym, mask) {
|
||||
self.body.push_str(&format!(
|
||||
" call void @{sym}(ptr {p_ssa}, i64 {mask})\n"
|
||||
));
|
||||
} else {
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {p_ssa})\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,24 +821,28 @@ impl<'a> Emitter<'a> {
|
||||
" 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"
|
||||
));
|
||||
// Iter 18g.tidy.fu2: pattern-binder with
|
||||
// statically-recorded moved slots routes
|
||||
// through the tag-conditional helper
|
||||
// `partial_drop_<owner>_<T>(b, mask)`. The
|
||||
// binder's runtime tag is dynamic but its
|
||||
// static type (`b_ail`) selects the per-type
|
||||
// helper, which dispatches on the runtime tag
|
||||
// and dec's only the unmoved fields. Closes
|
||||
// the 18d.4 carve-out: previously the shallow
|
||||
// `ailang_rc_dec` leaked the unmoved fields of
|
||||
// whichever ctor `b` happened to be at runtime.
|
||||
let sym = self.partial_drop_symbol_for_type(b_ail);
|
||||
let mask = Self::build_moved_mask(&moves);
|
||||
if let (Some(sym), Some(mask)) = (sym, mask) {
|
||||
self.body.push_str(&format!(
|
||||
" call void @{sym}(ptr {b_ssa}, i64 {mask})\n"
|
||||
));
|
||||
} else {
|
||||
self.body.push_str(&format!(
|
||||
" call void @ailang_rc_dec(ptr {b_ssa})\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
@@ -8451,3 +8451,143 @@ queued iter required it. It is a tidy paid for navigability
|
||||
alone — the kind of work that compounds across future
|
||||
sessions without showing up in any single one's bench
|
||||
numbers.
|
||||
|
||||
## 2026-05-08 — Iter 18g.tidy.fu2: tag-conditional partial-drop helper
|
||||
|
||||
User feedback closed an open question. The 18g tidy entry had
|
||||
queued the tag-conditional partial-drop helper as "stays queued;
|
||||
will close with the next iter that surfaces the leak in a
|
||||
benchmark or a real workload." The user pushed back: *"Du weisst,
|
||||
dass es einen Bug gibt — Leaks sind Bugs."* CLAUDE.md's TDD-for-
|
||||
bug-fixes rule is unambiguous on a known bug: write a RED test,
|
||||
ship the GREEN, keep as regression. The "wait for organic
|
||||
fixture" framing was avoidance disguised as discipline. Fixed.
|
||||
|
||||
### Three carve-outs, three sites, one helper
|
||||
|
||||
The pre-fu2 codebase had three sibling sites that fell back to
|
||||
shallow `ailang_rc_dec` when a binder had a non-empty
|
||||
`moved_slots` *and* a dynamic runtime ctor tag:
|
||||
|
||||
1. `lib.rs` Iter B Own-param dec at fn-return (`emit_fn`'s
|
||||
pre-ret seam).
|
||||
2. `match_lower.rs` Iter A arm-close pattern-binder dec
|
||||
(outer-arm close when an arm-bound binder was scrutinised
|
||||
by an inner match that moved out fields).
|
||||
3. `drop.rs::emit_inlined_partial_drop` non-Ctor branch
|
||||
(`Term::Let` whose value is a `Term::App` Own-returning
|
||||
call, body pattern-matches the binder).
|
||||
|
||||
All three share a structural shape: a binder whose static type is
|
||||
known (an ADT), but whose runtime ctor tag is *not* statically
|
||||
recoverable from the AST node at the drop emission site. The
|
||||
existing per-type drop fn `drop_<m>_<T>(ptr)` is built around
|
||||
"emit the dec for *every* ptr field of the active ctor"; what was
|
||||
missing was the variant "emit the dec for every ptr field of the
|
||||
active ctor *whose slot index is not in `moved`*."
|
||||
|
||||
### `partial_drop_<m>_<T>(ptr %p, i64 %mask)`
|
||||
|
||||
The fu2 helper (`drop.rs::emit_partial_drop_fn_for_type`) is a
|
||||
straight parallel to `emit_drop_fn_for_type`:
|
||||
|
||||
```text
|
||||
define void @partial_drop_<m>_<T>(ptr %p, i64 %mask) {
|
||||
%is_null = icmp eq ptr %p, null
|
||||
br i1 %is_null, label %ret, label %live
|
||||
live:
|
||||
%tag = load i64, ptr %p
|
||||
switch i64 %tag, label %dflt [...]
|
||||
arm_i:
|
||||
; for each ptr field at slot j of ctor i:
|
||||
%b = and i64 %mask, (1 << j)
|
||||
%s = icmp ne i64 %b, 0
|
||||
br i1 %s, label %after, label %do
|
||||
do:
|
||||
%v = load ptr, gep %p, (8 + 8*j)
|
||||
call void @<field_drop_call>(ptr %v)
|
||||
br label %after
|
||||
after:
|
||||
; next ptr field, or br label %join
|
||||
dflt: unreachable
|
||||
join:
|
||||
call void @ailang_rc_dec(ptr %p)
|
||||
br label %ret
|
||||
ret:
|
||||
ret void
|
||||
}
|
||||
```
|
||||
|
||||
One helper per ADT in the module, emitted alongside
|
||||
`drop_<m>_<T>`. We do *not* emit a `(drop-iterative)` partial-drop
|
||||
variant: the helper runs once on the binder (carve-out sites are
|
||||
not cascade points — the unmoved fields go through their own
|
||||
`drop_<m>_<F>` which itself decides recursive vs. iterative).
|
||||
|
||||
The mask is `i64`, so ADTs with > 64 fields fall back to shallow
|
||||
dec via the `build_moved_mask` cap. No language-level ADT has 64
|
||||
fields; the cap is load-bearing only as a defensive guard.
|
||||
|
||||
### Three RED-then-GREEN fixtures
|
||||
|
||||
Built before the helper to lock the carve-outs in regression
|
||||
coverage. All three share the same `Wrap` / `Cell` / `Pair` shape
|
||||
(Pair has 2 Cell slots, Cell has 2 Wrap slots) so the leak path
|
||||
is observable through `AILANG_RC_STATS=1`'s
|
||||
`allocs/frees/live` triple.
|
||||
|
||||
- `examples/rc_own_param_partial_drop_leak.{ailx,ail.json}` —
|
||||
site 1. `(fn use_first (params (own (con Pair))) ...)` matches
|
||||
`p` as `MkPair(a, _)`. Pre-fu2: `live=3` (whole second Cell +
|
||||
two Wraps leak through Iter B's shallow path). Post-fu2:
|
||||
`live=0`.
|
||||
- `examples/rc_match_arm_partial_drop_leak.{ailx,ail.json}` —
|
||||
site 2. Outer match binds `a, b` from `p`; inner match
|
||||
destructures `a` as `MkCell(w1, _)` (slot 1 wildcarded).
|
||||
Pre-fu2: `live=1` (slot 1's MkWrap leaks through Iter A's
|
||||
shallow path). Post-fu2: `live=0`.
|
||||
- `examples/rc_app_let_partial_drop_leak.{ailx,ail.json}` —
|
||||
site 3. `(let p (app build_pair 1) (match p ...))` with
|
||||
`MkPair(a, _)`. Pre-fu2: `live=3` (slot 1's whole Cell +
|
||||
Wraps leak through `emit_inlined_partial_drop`'s non-Ctor
|
||||
fallback). Post-fu2: `live=0`.
|
||||
|
||||
Each fixture has a corresponding e2e test in
|
||||
`crates/ail/tests/e2e.rs`. e2e count: 66 → 69.
|
||||
|
||||
### Routing the call sites
|
||||
|
||||
Each of the three sites was rewritten symmetrically: resolve the
|
||||
`partial_drop_<owner>_<T>` symbol from the binder's static type
|
||||
(`partial_drop_symbol_for_type`), build the mask
|
||||
(`build_moved_mask`), call. Falls back to the prior shallow
|
||||
`ailang_rc_dec` only if the static type is non-ADT (Str, fn-typed,
|
||||
type-var) — those shapes can't populate `moved_slots` in
|
||||
practice, so the fallback is dead under the typechecker.
|
||||
|
||||
The pre-existing test `alloc_rc_own_param_dec_at_fn_return`
|
||||
asserted on the IR-level shape "either `drop_<m>_<T>` or
|
||||
`ailang_rc_dec` is called on `%arg_xs` before the ret"; widened
|
||||
to also accept `partial_drop_<m>_<T>(%arg_xs, i64 mask)` (the
|
||||
canonical fu2 shape — the fixture's Cons arm moves slot 1 into
|
||||
`t`, so `moved_slots[xs]={1}`).
|
||||
|
||||
### What stays queued
|
||||
|
||||
Carve-out diagnostic surface (item 5 from the 18g tidy entry):
|
||||
the three carve-outs no longer leak, but they also don't surface
|
||||
to the user when they fire. Closing this is a separate ergonomics
|
||||
iter (structured diagnostics from codegen), not a correctness
|
||||
fix. The fu2 helper makes the codegen-side leak path empty;
|
||||
diagnostic surface is for the language-design layer.
|
||||
|
||||
### Lesson recorded
|
||||
|
||||
The "wait for organic fixture" stance from the 18g tidy entry is
|
||||
now retracted: known leaks are bugs, and CLAUDE.md's TDD rule
|
||||
covers them autonomously. Constructing a minimal fixture for a
|
||||
known leak path is *not* "constructing fixtures to drive an
|
||||
implementation" — it is the literal RED-step that the discipline
|
||||
prescribes. The three fixtures shipped here are exactly that:
|
||||
each pins one of the three carve-out sites, observable via the
|
||||
RC stats line, kept as regression. JOURNAL queue: empty.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"defs":[{"ctors":[{"fields":[{"k":"con","name":"Int"}],"name":"MkWrap"}],"kind":"type","name":"Wrap"},{"ctors":[{"fields":[{"k":"con","name":"Wrap"},{"k":"con","name":"Wrap"}],"name":"MkCell"}],"kind":"type","name":"Cell"},{"ctors":[{"fields":[{"k":"con","name":"Cell"},{"k":"con","name":"Cell"}],"name":"MkPair"}],"kind":"type","name":"Pair"},{"body":{"args":[{"args":[{"args":[{"name":"n","t":"var"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"}],"ctor":"MkCell","t":"ctor","type":"Cell"},{"args":[{"args":[{"lit":{"kind":"int","value":3},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"},{"args":[{"lit":{"kind":"int","value":4},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"}],"ctor":"MkCell","t":"ctor","type":"Cell"}],"ctor":"MkPair","t":"ctor","type":"Pair"},"kind":"fn","name":"build_pair","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Pair"},"ret_mode":"own"}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":1},"t":"lit"},"pat":{"ctor":"MkCell","fields":[{"name":"w1","p":"var"},{"name":"w2","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"c","t":"var"},"t":"match"},"kind":"fn","name":"use_cell","params":["c"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"Cell"}],"ret":{"k":"con","name":"Int"}}},{"body":{"body":{"args":[{"arms":[{"body":{"args":[{"name":"a","t":"var"}],"fn":{"name":"use_cell","t":"var"},"t":"app"},"pat":{"ctor":"MkPair","fields":[{"name":"a","p":"var"},{"p":"wild"}],"p":"ctor"}}],"scrutinee":{"name":"p","t":"var"},"t":"match"}],"op":"io/print_int","t":"do"},"name":"p","t":"let","value":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"build_pair","t":"var"},"t":"app"}},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"rc_app_let_partial_drop_leak","schema":"ailang/v0"}
|
||||
@@ -0,0 +1,69 @@
|
||||
; Iter 18g.tidy.fu2 RED-test fixture 3/3: dynamic-tag partial-drop
|
||||
; carve-out at let-close for an App-bound binder
|
||||
; (drop.rs:580 fallback in `emit_inlined_partial_drop`).
|
||||
;
|
||||
; Shape:
|
||||
; - Same `Pair`/`Cell`/`Wrap` types as fixtures 1 and 2.
|
||||
; - `main` let-binds `p = (app build_pair)` — value is
|
||||
; `Term::App` (Own-returning), `is_rc_heap_allocated` returns
|
||||
; true → trackable.
|
||||
; - Body matches `p` as `MkPair a _` — slot 0 bound, slot 1 wild.
|
||||
; `moved_slots[p] = {0}`.
|
||||
; - `a` is consumed by `use_cell` (Own param) → `consume_count[a]
|
||||
; == 1`, arm-close drop skipped. `use_cell`'s body fully
|
||||
; destructures `a` and returns Int.
|
||||
; - At p's let-close: `consume_count[p] == 0`, `moves[p] = {0}`
|
||||
; non-empty, `value` is `Term::App` (not `Term::Ctor`) →
|
||||
; `emit_inlined_partial_drop` falls into the non-Ctor branch
|
||||
; (drop.rs:580) → shallow `ailang_rc_dec(p)`. p's outer cell
|
||||
; is freed; slot 1 (the second MkCell + its two MkWraps =
|
||||
; 3 cells) leaks.
|
||||
;
|
||||
; Pre-fix: live = 3 (one MkCell + two MkWrap children in slot 1).
|
||||
; Post-fix: live = 0.
|
||||
|
||||
(module rc_app_let_partial_drop_leak
|
||||
|
||||
(data Wrap
|
||||
(ctor MkWrap (con Int)))
|
||||
|
||||
(data Cell
|
||||
(ctor MkCell (con Wrap) (con Wrap)))
|
||||
|
||||
(data Pair
|
||||
(ctor MkPair (con Cell) (con Cell)))
|
||||
|
||||
(fn build_pair
|
||||
(type
|
||||
(fn-type
|
||||
(params (con Int))
|
||||
(ret (own (con Pair)))))
|
||||
(params n)
|
||||
(body
|
||||
(term-ctor Pair MkPair
|
||||
(term-ctor Cell MkCell
|
||||
(term-ctor Wrap MkWrap n)
|
||||
(term-ctor Wrap MkWrap 2))
|
||||
(term-ctor Cell MkCell
|
||||
(term-ctor Wrap MkWrap 3)
|
||||
(term-ctor Wrap MkWrap 4)))))
|
||||
|
||||
(fn use_cell
|
||||
(type
|
||||
(fn-type
|
||||
(params (own (con Cell)))
|
||||
(ret (con Int))))
|
||||
(params c)
|
||||
(body
|
||||
(match c
|
||||
(case (pat-ctor MkCell w1 w2) 1))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let p (app build_pair 1)
|
||||
(do io/print_int
|
||||
(match p
|
||||
(case (pat-ctor MkPair a _)
|
||||
(app use_cell a))))))))
|
||||
@@ -0,0 +1 @@
|
||||
{"defs":[{"ctors":[{"fields":[{"k":"con","name":"Int"}],"name":"MkWrap"}],"kind":"type","name":"Wrap"},{"ctors":[{"fields":[{"k":"con","name":"Wrap"},{"k":"con","name":"Wrap"}],"name":"MkCell"}],"kind":"type","name":"Cell"},{"ctors":[{"fields":[{"k":"con","name":"Cell"},{"k":"con","name":"Cell"}],"name":"MkPair"}],"kind":"type","name":"Pair"},{"body":{"args":[{"args":[{"args":[{"name":"n","t":"var"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"}],"ctor":"MkCell","t":"ctor","type":"Cell"},{"args":[{"args":[{"lit":{"kind":"int","value":3},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"},{"args":[{"lit":{"kind":"int","value":4},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"}],"ctor":"MkCell","t":"ctor","type":"Cell"}],"ctor":"MkPair","t":"ctor","type":"Pair"},"kind":"fn","name":"build_pair","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Pair"},"ret_mode":"own"}},{"body":{"arms":[{"body":{"arms":[{"body":{"lit":{"kind":"int","value":1},"t":"lit"},"pat":{"ctor":"MkCell","fields":[{"name":"w1","p":"var"},{"p":"wild"}],"p":"ctor"}}],"scrutinee":{"name":"a","t":"var"},"t":"match"},"pat":{"ctor":"MkPair","fields":[{"name":"a","p":"var"},{"name":"b","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"p","t":"var"},"t":"match"},"kind":"fn","name":"use_first","params":["p"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"Pair"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"build_pair","t":"var"},"t":"app"}],"fn":{"name":"use_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"rc_match_arm_partial_drop_leak","schema":"ailang/v0"}
|
||||
@@ -0,0 +1,71 @@
|
||||
; Iter 18g.tidy.fu2 RED-test fixture 2/3: dynamic-tag partial-drop
|
||||
; carve-out at outer-arm-close pattern-binder dec
|
||||
; (Iter A / match_lower.rs:839).
|
||||
;
|
||||
; Shape:
|
||||
; - `Pair` carries two `Cell` fields; `Cell` carries two `Wrap`
|
||||
; fields. (Same types as fixture 1.)
|
||||
; - `use_first` takes `(own Pair)` and the OUTER match binds both
|
||||
; slots: `MkPair a b`.
|
||||
; - The arm body INNER-matches `a` as `MkCell w1 _` — slot 0 of
|
||||
; a is bound, slot 1 is wildcarded.
|
||||
; - Inner arm-close drops `w1` properly via `drop_<m>_Wrap`.
|
||||
; Inner match adds slot 0 to `moved_slots[a]`.
|
||||
; - Outer arm-close drops `a` and `b`. `a` has `moves={0}`
|
||||
; (non-empty) and `consume_count==0` (only Borrow use as
|
||||
; scrutinee of inner match) → Iter A's dynamic-tag carve-out
|
||||
; fires: shallow `ailang_rc_dec(a)`. a's outer Cell is freed,
|
||||
; but slot 1 (the unbound MkWrap(2) cell) leaks.
|
||||
; - `b` has `moves={}` and `consume_count==0` → routes through
|
||||
; `drop_<m>_Cell(b)` which cascades through both Wraps. No
|
||||
; leak from b.
|
||||
; - At fn return, `p`'s `moves={0,1}` (both slots bound) → Iter
|
||||
; B carve-out fires too, but with all slots moved, the shallow
|
||||
; dec is correct (no additional leak from p).
|
||||
;
|
||||
; Pre-fix: live = 1 (the unbound MkWrap(2) cell in slot 1 of a).
|
||||
; Post-fix: live = 0.
|
||||
|
||||
(module rc_match_arm_partial_drop_leak
|
||||
|
||||
(data Wrap
|
||||
(ctor MkWrap (con Int)))
|
||||
|
||||
(data Cell
|
||||
(ctor MkCell (con Wrap) (con Wrap)))
|
||||
|
||||
(data Pair
|
||||
(ctor MkPair (con Cell) (con Cell)))
|
||||
|
||||
(fn build_pair
|
||||
(type
|
||||
(fn-type
|
||||
(params (con Int))
|
||||
(ret (own (con Pair)))))
|
||||
(params n)
|
||||
(body
|
||||
(term-ctor Pair MkPair
|
||||
(term-ctor Cell MkCell
|
||||
(term-ctor Wrap MkWrap n)
|
||||
(term-ctor Wrap MkWrap 2))
|
||||
(term-ctor Cell MkCell
|
||||
(term-ctor Wrap MkWrap 3)
|
||||
(term-ctor Wrap MkWrap 4)))))
|
||||
|
||||
(fn use_first
|
||||
(type
|
||||
(fn-type
|
||||
(params (own (con Pair)))
|
||||
(ret (con Int))))
|
||||
(params p)
|
||||
(body
|
||||
(match p
|
||||
(case (pat-ctor MkPair a b)
|
||||
(match a
|
||||
(case (pat-ctor MkCell w1 _) 1))))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(do io/print_int (app use_first (app build_pair 1))))))
|
||||
@@ -0,0 +1 @@
|
||||
{"defs":[{"ctors":[{"fields":[{"k":"con","name":"Int"}],"name":"MkWrap"}],"kind":"type","name":"Wrap"},{"ctors":[{"fields":[{"k":"con","name":"Wrap"},{"k":"con","name":"Wrap"}],"name":"MkCell"}],"kind":"type","name":"Cell"},{"ctors":[{"fields":[{"k":"con","name":"Cell"},{"k":"con","name":"Cell"}],"name":"MkPair"}],"kind":"type","name":"Pair"},{"body":{"args":[{"args":[{"args":[{"name":"n","t":"var"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"}],"ctor":"MkCell","t":"ctor","type":"Cell"},{"args":[{"args":[{"lit":{"kind":"int","value":3},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"},{"args":[{"lit":{"kind":"int","value":4},"t":"lit"}],"ctor":"MkWrap","t":"ctor","type":"Wrap"}],"ctor":"MkCell","t":"ctor","type":"Cell"}],"ctor":"MkPair","t":"ctor","type":"Pair"},"kind":"fn","name":"build_pair","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Pair"},"ret_mode":"own"}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":1},"t":"lit"},"pat":{"ctor":"MkPair","fields":[{"name":"a","p":"var"},{"p":"wild"}],"p":"ctor"}}],"scrutinee":{"name":"p","t":"var"},"t":"match"},"kind":"fn","name":"use_first","params":["p"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"Pair"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"build_pair","t":"var"},"t":"app"}],"fn":{"name":"use_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"rc_own_param_partial_drop_leak","schema":"ailang/v0"}
|
||||
@@ -0,0 +1,65 @@
|
||||
; Iter 18g.tidy.fu2 RED-test fixture 1/3: dynamic-tag partial-drop
|
||||
; carve-out at fn-return (Iter B / lib.rs:1093).
|
||||
;
|
||||
; Shape:
|
||||
; - `Pair` carries two `Cell` fields (both pointer-typed).
|
||||
; - `Cell` carries two `Wrap` fields (both pointer-typed).
|
||||
; - `use_first` takes `(own Pair)` and pattern-binds the first
|
||||
; slot only, wildcarding the second.
|
||||
; - The bound slot's `Cell` is dec'd via `drop_<m>_Cell` at the
|
||||
; outer arm-close (moves[a] empty → field_drop_call routes
|
||||
; correctly).
|
||||
; - At fn return, `p`'s `consume_count == 0`, mode = Own, and
|
||||
; `moved_slots[p] = {0}`. Iter B's drop (lib.rs:1093) hits the
|
||||
; dynamic-tag carve-out: moves non-empty → shallow
|
||||
; `ailang_rc_dec(p)`. p's outer cell is freed; slot 1 (the
|
||||
; wildcarded second Cell + its two MkWrap children = 3 cells)
|
||||
; leaks.
|
||||
;
|
||||
; Pre-fix: live = 3 (one Cell + two MkWrap children).
|
||||
; Post-fix: live = 0.
|
||||
;
|
||||
; This fixture is the cleanest of the three because no inner match
|
||||
; is involved: the leak surfaces from Iter B alone.
|
||||
|
||||
(module rc_own_param_partial_drop_leak
|
||||
|
||||
(data Wrap
|
||||
(ctor MkWrap (con Int)))
|
||||
|
||||
(data Cell
|
||||
(ctor MkCell (con Wrap) (con Wrap)))
|
||||
|
||||
(data Pair
|
||||
(ctor MkPair (con Cell) (con Cell)))
|
||||
|
||||
(fn build_pair
|
||||
(type
|
||||
(fn-type
|
||||
(params (con Int))
|
||||
(ret (own (con Pair)))))
|
||||
(params n)
|
||||
(body
|
||||
(term-ctor Pair MkPair
|
||||
(term-ctor Cell MkCell
|
||||
(term-ctor Wrap MkWrap n)
|
||||
(term-ctor Wrap MkWrap 2))
|
||||
(term-ctor Cell MkCell
|
||||
(term-ctor Wrap MkWrap 3)
|
||||
(term-ctor Wrap MkWrap 4)))))
|
||||
|
||||
(fn use_first
|
||||
(type
|
||||
(fn-type
|
||||
(params (own (con Pair)))
|
||||
(ret (con Int))))
|
||||
(params p)
|
||||
(body
|
||||
(match p
|
||||
(case (pat-ctor MkPair a _) 1))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(do io/print_int (app use_first (app build_pair 1))))))
|
||||
Reference in New Issue
Block a user