Iter 18e: (drop-iterative) annotation + worklist allocator

Closes the 18-arc's stack-recursion limit. Recursive drop
cascades from 18c.4 overflow on long ADT chains (Linux's 8 MB
default stack maxes out around 1M cells of List). The new
opt-in (drop-iterative) annotation on a Def::Type switches the
synthesised drop_<m>_<T> body from recursive to iterative-with-
explicit-worklist for that type.

Schema:
- TypeDef.drop_iterative: bool. Default false; serde-skip
  when false so existing fixtures' canonical JSON hashes stay
  stable.
- Form-A: (drop-iterative) clause inside (data T ...).

Worklist runtime (4 new ABI symbols in runtime/rc.c):
- ailang_drop_worklist_new(initial_capacity)
- ailang_drop_worklist_push(wl, ptr)
- ailang_drop_worklist_pop(wl) -> ptr
- ailang_drop_worklist_free(wl)

Heap stretchy buffer, doubling on overflow, null-filtering on
push. Lean 4 / Roc precedent documented in the runtime; the
slot-repurposing strategy was considered and rejected because
not every box has a free pointer-typed slot to thread the
worklist through (Cons head is i64, slot 1 is ptr but it's
the field we're following — no free slot).

Codegen (emit_iterative_drop_fn_for_type): for a
drop_iterative type, drop_<m>_<T>(ptr %p) emits a worklist
loop. Fields of the SAME annotated type push onto the
worklist (mono-typed); fields of DIFFERENT types call their
own drop fn directly (recursive on those, but only if THEY
are themselves recursive — i.e. one level of cascade jump
maximum). Mono-typed-worklist is sound for the deep-self-
recursion case the iter targets (List of List of T just
needs the spine flattened).

Tests:
- examples/rc_drop_iterative_long_list — 1M-cell List of Int
  with (drop-iterative) annotation.
- alloc_rc_drop_iterative_handles_million_cell_list E2E —
  builds + runs under --alloc=rc, asserts clean exit. With
  annotation: exits 0. Without annotation (control): SIGSEGV
  at exit code 139 (verified by hand). Worklist is load-
  bearing.
- iter18e_drop_iterative_emits_worklist_body_no_self_recursion
  IR-shape: worklist body has br to loop_head AND no direct
  recursive call into drop_<m>_<T>.
- iter18e_no_annotation_keeps_recursive_drop_body — control:
  unannotated variant still emits the 18c.4 recursive shape.
- 3 surface parse-tests for the annotation round-trip.

Test deltas: e2e 58 -> 61 (+3), surface 18 -> 21 (+3). All
other buckets unchanged. cargo test --workspace green.

Known debt (deliberate):
- Mono-typed worklist: cross-type drop-iterative fields call
  the other type's drop fn directly. A heterogeneous
  worklist would be more general but adds tag tracking
  complexity for a case (drop-iterative T containing
  drop-iterative T') that's narrower than the deep-self-
  recursion target. Documented in
  emit_iterative_drop_fn_for_type's doc.
- Closure / Type::Var / Type::Forall fields fall back to
  shallow ailang_rc_dec via field_drop_call — same as the
  recursive variant.
- Dynamic-tag partial-drop fallback (head_or_zero epilogue
  shallow dec when moved_slots non-empty) — out of scope per
  brief.
This commit is contained in:
2026-05-08 12:53:09 +02:00
parent 5e401a3520
commit ce6ab8ee44
12 changed files with 763 additions and 2 deletions
+176
View File
@@ -1798,3 +1798,179 @@ fn alloc_rc_own_param_dec_at_fn_return() {
"head_or_zero's `%arg_xs` drop must appear BEFORE the `ret i64` instruction. Pre-ret slice was:\n{pre_ret}"
);
}
/// Iter 18e: `(drop-iterative)` opt-in annotation. The fixture
/// declares `IntList` with `(drop-iterative)` and runs an N=1,000,000
/// cell list through `head_or_zero`. The fn signature is
/// `(own (con IntList))` — 18d.4 emits a drop on the param at fn
/// return; under 18e that drop dispatches into the worklist body of
/// `drop_<m>_<IntList>` (or, in the canonical case here, into the
/// arm-close drop on `t` since the Cons arm pattern-binds the tail).
/// Either way, freeing the 1M-cell chain runs the iterative loop in
/// `runtime/rc.c` instead of recursing 1M frames deep on the C
/// stack.
///
/// Why 1M and not 100K: at 100K the recursive cascade still fits
/// in Linux's 8MB default stack (~64B/frame). 1M overflows clean
/// without the annotation (SIGSEGV) and runs cleanly with it. The
/// test would be a false-negative — passing without the iterative
/// body — at 100K.
///
/// Properties guarded:
/// 1. `--alloc=rc` produces stdout `1\n` (the head of the
/// tail-recursively-built `[1, 2, ..., 1_000_000]`).
/// 2. The binary exits cleanly (status 0; no SIGSEGV from a
/// recursive cascade overflowing the C stack — which DOES
/// happen on the same fixture with the annotation removed,
/// hand-verified during 18e implementation).
/// 3. `--alloc=gc` produces the same stdout (allocator-equivalence
/// backstop; 1M cells at 24B each ≈ 24MB, well within Boehm's
/// capacity).
#[test]
fn alloc_rc_drop_iterative_handles_million_cell_list() {
let example = "rc_drop_iterative_long_list.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_rc.trim(), "1");
assert_eq!(stdout_gc.trim(), "1");
assert_eq!(
stdout_gc, stdout_rc,
"alloc=rc must match alloc=gc on rc_drop_iterative_long_list"
);
}
/// Iter 18e: IR-shape signature of `(drop-iterative)`. The drop fn
/// for the annotated type contains a worklist loop (`br label
/// %loop_head`, the runtime-helper calls, a backedge from each ctor
/// arm) and does NOT contain a recursive `call void @drop_<m>_<T>(...)`
/// against itself — the recursion has been replaced by worklist
/// dispatch.
#[test]
fn iter18e_drop_iterative_emits_worklist_body_no_self_recursion() {
let example = "rc_drop_iterative_long_list.ail.json";
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 the drop fn body. There is exactly one
// `define void @drop_rc_drop_iterative_long_list_IntList(`.
let drop_start = ir
.find("define void @drop_rc_drop_iterative_long_list_IntList(")
.expect("drop fn definition present");
let drop_end_offset = ir[drop_start..]
.find("\n}\n")
.expect("drop fn ends with `}`");
let drop_body = &ir[drop_start..drop_start + drop_end_offset];
// Worklist signatures: backedge label, push, pop, free, all
// present in the body.
assert!(
drop_body.contains("br label %loop_head"),
"iterative drop body must contain a `br label %loop_head` backedge. Body was:\n{drop_body}"
);
assert!(
drop_body.contains("call ptr @ailang_drop_worklist_new()"),
"iterative drop body must call ailang_drop_worklist_new. Body was:\n{drop_body}"
);
assert!(
drop_body.contains("call ptr @ailang_drop_worklist_pop(ptr %wl)"),
"iterative drop body must call ailang_drop_worklist_pop. Body was:\n{drop_body}"
);
assert!(
drop_body.contains("call void @ailang_drop_worklist_push(ptr %wl,"),
"iterative drop body must call ailang_drop_worklist_push for same-type fields. Body was:\n{drop_body}"
);
assert!(
drop_body.contains("call void @ailang_drop_worklist_free(ptr %wl)"),
"iterative drop body must call ailang_drop_worklist_free at finish. Body was:\n{drop_body}"
);
// Critical negative: NO recursive self-call in the drop body —
// the whole point of 18e is that the recursion was replaced by
// worklist dispatch. (Calls to other drop fns are still allowed,
// since cross-type ADT fields go through the regular
// `field_drop_call` path.)
assert!(
!drop_body.contains(
"call void @drop_rc_drop_iterative_long_list_IntList("
),
"iterative drop body must NOT recursively call itself — the recursion was replaced by worklist dispatch. Body was:\n{drop_body}"
);
}
/// Iter 18e: control — the same fixture WITHOUT the
/// `(drop-iterative)` annotation must still emit the recursive
/// 18c.4 body. We synthesise the unannotated module on the fly
/// (mutating the workspace's parsed AST) so this test is independent
/// of any on-disk fixture changes.
#[test]
fn iter18e_no_annotation_keeps_recursive_drop_body() {
let example = "rc_drop_iterative_long_list.ail.json";
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");
// Mutate every TypeDef in every module to clear the annotation.
let mut modules = std::collections::BTreeMap::new();
for (mname, m) in &ws.modules {
let mut mc = m.clone();
for d in &mut mc.defs {
if let ailang_core::ast::Def::Type(td) = d {
td.drop_iterative = false;
}
}
let desugared = ailang_core::desugar::desugar_module(&mc);
let lifted = ailang_check::lift_letrecs(&desugared)
.unwrap_or_else(|e| panic!("lift_letrecs in module `{mname}`: {e:?}"));
modules.insert(mname.clone(), lifted);
}
let lifted_ws = ailang_core::Workspace {
entry: ws.entry.clone(),
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 drop_start = ir
.find("define void @drop_rc_drop_iterative_long_list_IntList(")
.expect("drop fn definition present");
let drop_end_offset = ir[drop_start..]
.find("\n}\n")
.expect("drop fn ends with `}`");
let drop_body = &ir[drop_start..drop_start + drop_end_offset];
// The 18c.4 recursive shape signature: a self-call AND no
// worklist plumbing.
assert!(
drop_body.contains(
"call void @drop_rc_drop_iterative_long_list_IntList(ptr %v"
),
"without (drop-iterative), the body must still recursively cascade through the tail. Body was:\n{drop_body}"
);
assert!(
!drop_body.contains("ailang_drop_worklist_new"),
"without (drop-iterative), the body must not call worklist runtime helpers. Body was:\n{drop_body}"
);
}