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:
@@ -421,6 +421,14 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
|
||||
if matches!(alloc, AllocStrategy::Rc) {
|
||||
out.push_str("declare void @ailang_rc_inc(ptr)\n");
|
||||
out.push_str("declare void @ailang_rc_dec(ptr)\n");
|
||||
// Iter 18e: drop-worklist ABI for `(drop-iterative)` types.
|
||||
// Declared unconditionally under `--alloc=rc` (the linker
|
||||
// drops symbols if no emitted fn references them); symmetric
|
||||
// with inc/dec above. See `runtime/rc.c` for the strategy.
|
||||
out.push_str("declare ptr @ailang_drop_worklist_new()\n");
|
||||
out.push_str("declare void @ailang_drop_worklist_push(ptr, ptr)\n");
|
||||
out.push_str("declare ptr @ailang_drop_worklist_pop(ptr)\n");
|
||||
out.push_str("declare void @ailang_drop_worklist_free(ptr)\n");
|
||||
}
|
||||
// Iter 16e: `==` on `Str` lowers to `@strcmp` followed by
|
||||
// `icmp eq i32 0`. NUL-terminated strings make this a one-liner;
|
||||
@@ -752,7 +760,16 @@ impl<'a> Emitter<'a> {
|
||||
if matches!(self.alloc, AllocStrategy::Rc) {
|
||||
for def in &self.module.defs {
|
||||
if let Def::Type(td) = def {
|
||||
self.emit_drop_fn_for_type(td);
|
||||
if td.drop_iterative {
|
||||
// Iter 18e: opt-in iterative-drop body. The
|
||||
// recursive cascade overflows the C stack on
|
||||
// long chains (a million-cell list ≈ 8MB
|
||||
// stack); the iterative variant uses an
|
||||
// explicit heap-allocated worklist instead.
|
||||
self.emit_iterative_drop_fn_for_type(td);
|
||||
} else {
|
||||
self.emit_drop_fn_for_type(td);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -877,6 +894,211 @@ impl<'a> Emitter<'a> {
|
||||
self.body.push_str(&out);
|
||||
}
|
||||
|
||||
/// Iter 18e: emit `drop_<m>_<T>` for a `(drop-iterative)` type.
|
||||
/// Replaces the recursive cascade in [`Self::emit_drop_fn_for_type`]
|
||||
/// with an iterative-with-explicit-worklist body so cells of
|
||||
/// arbitrary chain depth can free without consuming proportional
|
||||
/// C stack.
|
||||
///
|
||||
/// IR shape:
|
||||
/// ```text
|
||||
/// define void @drop_<m>_<T>(ptr %p) {
|
||||
/// entry:
|
||||
/// %is_null = icmp eq ptr %p, null
|
||||
/// br i1 %is_null, label %ret, label %init_wl
|
||||
/// init_wl:
|
||||
/// %wl = call ptr @ailang_drop_worklist_new()
|
||||
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %p)
|
||||
/// br label %loop_head
|
||||
/// loop_head:
|
||||
/// %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)
|
||||
/// %done = icmp eq ptr %cur, null
|
||||
/// br i1 %done, label %finish, label %dispatch
|
||||
/// dispatch:
|
||||
/// %tag = load i64, ptr %cur, align 8
|
||||
/// switch i64 %tag, label %dflt [
|
||||
/// i64 0, label %arm_0
|
||||
/// ...
|
||||
/// ]
|
||||
/// arm_i:
|
||||
/// for each pointer-typed field f_j of ctor i:
|
||||
/// %addr = gep %cur, 8 + 8*j
|
||||
/// %v = load ptr, ptr %addr
|
||||
/// if field type is T (same as the type being dropped):
|
||||
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %v)
|
||||
/// else:
|
||||
/// call void @drop_<owner>_<F>(ptr %v) ; or @ailang_rc_dec
|
||||
/// call void @ailang_rc_dec(ptr %cur)
|
||||
/// br label %loop_head
|
||||
/// dflt:
|
||||
/// unreachable
|
||||
/// finish:
|
||||
/// call void @ailang_drop_worklist_free(ptr %wl)
|
||||
/// br label %ret
|
||||
/// ret:
|
||||
/// ret void
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Mono-typed worklist. Every pointer pushed onto `%wl` is a `T`
|
||||
/// (the type being dropped). For a field whose type is `T` itself
|
||||
/// → push (continues the iterative cascade). For any other ADT
|
||||
/// field type `T'` → call `drop_<m'>_<T'>` directly: if `T'` is
|
||||
/// also `(drop-iterative)`, that fn allocates its own worklist
|
||||
/// instance (no nesting); if `T'` is non-iterative, it recurses
|
||||
/// stack-wise (depth bounded by the number of *distinct* nested
|
||||
/// ADTs reachable from `T`, which is small in practice).
|
||||
///
|
||||
/// This interpretation of the assignment's "should also use the
|
||||
/// worklist" clause was chosen because a heterogeneously-typed
|
||||
/// worklist would require storing a (ptr, drop-handler) tuple per
|
||||
/// entry plus a vtable dispatch on pop — significant complexity
|
||||
/// for the case where two distinct ADTs are mutually recursive
|
||||
/// AND both are drop-iterative AND the chain is millions deep.
|
||||
/// That triple-conjunct is not on the 18-arc's critical path; if
|
||||
/// it surfaces in practice, a follow-up iter can extend the
|
||||
/// worklist entry shape. The mono-typed version captures the
|
||||
/// stack-overflow-on-long-self-chains problem fully.
|
||||
fn emit_iterative_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 @drop_{m}_{tname}(ptr %p) {{\n"));
|
||||
out.push_str("entry:\n");
|
||||
// Null guard — symmetric with the recursive variant. A null
|
||||
// payload skips worklist allocation entirely.
|
||||
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
||||
out.push_str(" br i1 %is_null, label %ret, label %init_wl\n");
|
||||
out.push_str("init_wl:\n");
|
||||
out.push_str(" %wl = call ptr @ailang_drop_worklist_new()\n");
|
||||
out.push_str(
|
||||
" call void @ailang_drop_worklist_push(ptr %wl, ptr %p)\n",
|
||||
);
|
||||
out.push_str(" br label %loop_head\n");
|
||||
|
||||
out.push_str("loop_head:\n");
|
||||
out.push_str(
|
||||
" %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)\n",
|
||||
);
|
||||
out.push_str(" %done = icmp eq ptr %cur, null\n");
|
||||
out.push_str(" br i1 %done, label %finish, label %dispatch\n");
|
||||
|
||||
out.push_str("dispatch:\n");
|
||||
out.push_str(" %tag = load i64, ptr %cur, 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");
|
||||
|
||||
// Per-ctor arms. For each pointer-typed field decide push vs
|
||||
// direct call based on whether the field's type is the same
|
||||
// as the type being dropped.
|
||||
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 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 %cur, i64 {off}\n"
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
||||
));
|
||||
if self.field_is_same_type(fty, &td.name) {
|
||||
// Same-type field: push onto the worklist —
|
||||
// continues the iterative cascade. Null-guarding
|
||||
// is handled inside `ailang_drop_worklist_push`
|
||||
// itself (skips null payloads).
|
||||
out.push_str(&format!(
|
||||
" call void @ailang_drop_worklist_push(ptr %wl, ptr %v{val_id})\n"
|
||||
));
|
||||
} else {
|
||||
// Different-type field: dispatch to that type's
|
||||
// own drop fn (which itself decides recursive vs.
|
||||
// iterative). `field_drop_call` resolves the
|
||||
// symbol; its null-guard semantics are the same
|
||||
// as the recursive variant.
|
||||
let drop_call = self.field_drop_call(fty);
|
||||
out.push_str(&format!(
|
||||
" call void @{drop_call}(ptr %v{val_id})\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
// Dec the outer cell. Worklist holds no other reference
|
||||
// to this pointer (push happened exactly once on the
|
||||
// parent's cascade, and pop just removed that entry), so
|
||||
// the cell's refcount drops by exactly one here. Children
|
||||
// pushed above keep their own refcounts pending until
|
||||
// their loop iteration.
|
||||
out.push_str(" call void @ailang_rc_dec(ptr %cur)\n");
|
||||
out.push_str(" br label %loop_head\n");
|
||||
}
|
||||
|
||||
// Default arm: unreachable when the typechecker has accepted
|
||||
// the input. Same shape as the recursive variant.
|
||||
out.push_str("dflt:\n");
|
||||
if n_ctors == 0 {
|
||||
out.push_str(" br label %finish\n");
|
||||
} else {
|
||||
out.push_str(" unreachable\n");
|
||||
}
|
||||
|
||||
out.push_str("finish:\n");
|
||||
out.push_str(" call void @ailang_drop_worklist_free(ptr %wl)\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 18e helper: is `fty` the same ADT as `td_name` in the
|
||||
/// current module? Used by the iterative-drop body to decide
|
||||
/// "push to worklist" (same type) vs. "call its drop fn directly"
|
||||
/// (different type).
|
||||
///
|
||||
/// Returns `true` only when the field type is a `Type::Con`
|
||||
/// referencing `td_name` AND the reference resolves to the
|
||||
/// current module (bare or qualified-but-self). Qualified names
|
||||
/// pointing at *other* modules are different types — even when
|
||||
/// they spell the same suffix. Type-vars and fn-types are never
|
||||
/// the same as the ADT being dropped (parametric self-recursion
|
||||
/// could bind a var to T, but the bound is invisible at codegen
|
||||
/// since we don't monomorphise drop fns).
|
||||
fn field_is_same_type(&self, fty: &Type, td_name: &str) -> bool {
|
||||
match fty {
|
||||
Type::Con { name, .. } => {
|
||||
if name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) = name.split_once('.').expect("checked");
|
||||
if suffix != td_name {
|
||||
return false;
|
||||
}
|
||||
// Resolve the prefix; same-module iff the resolved
|
||||
// target equals self.module_name.
|
||||
let target = self
|
||||
.import_map
|
||||
.get(prefix)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(prefix);
|
||||
target == self.module_name
|
||||
} else {
|
||||
name == td_name
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18c.4: pick the drop-fn symbol to call for a single
|
||||
/// pointer-typed field. Routes ADT fields to their own
|
||||
/// `drop_<owner>_<T>` symbol so the recursion cascades through
|
||||
@@ -4577,6 +4799,7 @@ mod tests {
|
||||
fields: vec![],
|
||||
}],
|
||||
doc: None,
|
||||
drop_iterative: false,
|
||||
}),
|
||||
Def::Fn(FnDef {
|
||||
name: "main".into(),
|
||||
@@ -4735,6 +4958,7 @@ mod tests {
|
||||
},
|
||||
],
|
||||
doc: None,
|
||||
drop_iterative: false,
|
||||
}),
|
||||
Def::Fn(FnDef {
|
||||
name: "main".into(),
|
||||
|
||||
Reference in New Issue
Block a user