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:
@@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user