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:
+113
@@ -118,3 +118,116 @@ void ailang_rc_dec(void *payload) {
|
||||
free(hdr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Iter 18e: drop worklist.
|
||||
*
|
||||
* Backs the `(drop-iterative)` data attribute. When a type is annotated
|
||||
* `(drop-iterative)`, codegen emits `drop_<m>_<T>` with an iterative-with-
|
||||
* worklist body in place of the recursive cascade. The worklist is a
|
||||
* heap-allocated stretchy buffer of `void*` pointers — one entry per
|
||||
* not-yet-processed cell. Each entry is mono-typed to T (the annotated
|
||||
* ADT being dropped); fields of T whose type is `T` itself are pushed,
|
||||
* fields whose type is a different ADT call that ADT's drop fn directly.
|
||||
* (See `Emitter::emit_iterative_drop_fn_for_type` in the codegen for the
|
||||
* IR shape and the same-type / different-type dispatch.)
|
||||
*
|
||||
* Strategy: heap-allocated buffer, doubled on overflow. We chose this
|
||||
* over a stack-allocated small buffer (overcomplicates the IR seam — the
|
||||
* codegen body would need to track "which buffer is live") and over Lean
|
||||
* 4's "thread the worklist through one of the cell's own pointer slots"
|
||||
* technique (requires the codegen to know which slot of each ctor is
|
||||
* "free to repurpose" — non-trivial since AILang ctors are heterogeneous
|
||||
* and slot 0 is always the tag). The runtime-helper approach keeps the
|
||||
* IR-level body of `drop_<m>_<T>` small: three calls (new / push / pop /
|
||||
* free) drive the loop.
|
||||
*
|
||||
* Precedent: Lean 4's `lean_dec_ref_cold` and Roc's iterative-free path
|
||||
* both use a worklist to break tail recursion in their drop cascades.
|
||||
* Lean threads the worklist through field slots (the "in-place" variant);
|
||||
* we use a separate heap buffer because AILang's ctor layout makes slot
|
||||
* repurposing fragile. The semantic invariant matches: every cell whose
|
||||
* refcount reaches zero is dec'd exactly once, regardless of cascade
|
||||
* depth, without consuming proportional C stack space.
|
||||
*
|
||||
* Single-threaded; non-atomic. Same scope as the rest of `runtime/rc.c`.
|
||||
* --------------------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
void **data; /* heap buffer of `cap` pointers; null once freed */
|
||||
size_t len; /* number of live entries (always <= cap) */
|
||||
size_t cap; /* current capacity in slots */
|
||||
} ailang_drop_worklist_t;
|
||||
|
||||
/* Initial capacity. 16 slots * 8 bytes = 128 bytes — small enough that
|
||||
* very-shallow drops don't waste memory, large enough that 16-deep
|
||||
* cascades (very common) never realloc. Doubled on overflow. */
|
||||
#define DROP_WORKLIST_INIT_CAP ((size_t)16)
|
||||
|
||||
void *ailang_drop_worklist_new(void) {
|
||||
ailang_drop_worklist_t *wl = malloc(sizeof(ailang_drop_worklist_t));
|
||||
if (wl == NULL) {
|
||||
fprintf(stderr,
|
||||
"ailang_drop_worklist_new: out of memory (header)\n");
|
||||
abort();
|
||||
}
|
||||
wl->data = malloc(DROP_WORKLIST_INIT_CAP * sizeof(void *));
|
||||
if (wl->data == NULL) {
|
||||
fprintf(stderr,
|
||||
"ailang_drop_worklist_new: out of memory (initial buffer)\n");
|
||||
abort();
|
||||
}
|
||||
wl->len = 0;
|
||||
wl->cap = DROP_WORKLIST_INIT_CAP;
|
||||
return (void *)wl;
|
||||
}
|
||||
|
||||
/* Push `payload` onto the worklist. Skips null payloads — pushed nulls
|
||||
* would dispatch on `load i64, ptr null` at pop time and segfault, so
|
||||
* we filter here. The check is symmetric with `ailang_rc_dec`'s null
|
||||
* guard (a null payload is a no-op everywhere in the rc runtime). */
|
||||
void ailang_drop_worklist_push(void *wl_opaque, void *payload) {
|
||||
if (payload == NULL) {
|
||||
return;
|
||||
}
|
||||
ailang_drop_worklist_t *wl = (ailang_drop_worklist_t *)wl_opaque;
|
||||
if (wl->len == wl->cap) {
|
||||
size_t new_cap = wl->cap * 2;
|
||||
void **new_data = realloc(wl->data, new_cap * sizeof(void *));
|
||||
if (new_data == NULL) {
|
||||
fprintf(stderr,
|
||||
"ailang_drop_worklist_push: out of memory (grow to %zu slots)\n",
|
||||
new_cap);
|
||||
abort();
|
||||
}
|
||||
wl->data = new_data;
|
||||
wl->cap = new_cap;
|
||||
}
|
||||
wl->data[wl->len] = payload;
|
||||
wl->len += 1;
|
||||
}
|
||||
|
||||
/* Pop one payload from the worklist. Returns NULL when the worklist is
|
||||
* empty. Since `push` filters nulls, a returned null is unambiguous and
|
||||
* can be used by the IR body as the loop-exit sentinel. */
|
||||
void *ailang_drop_worklist_pop(void *wl_opaque) {
|
||||
ailang_drop_worklist_t *wl = (ailang_drop_worklist_t *)wl_opaque;
|
||||
if (wl->len == 0) {
|
||||
return NULL;
|
||||
}
|
||||
wl->len -= 1;
|
||||
return wl->data[wl->len];
|
||||
}
|
||||
|
||||
/* Free the worklist itself. Called once at the end of the iterative
|
||||
* drop loop. Does NOT free any payloads still in the buffer — the IR
|
||||
* body must drain the buffer first via repeated `pop` calls before
|
||||
* calling free. */
|
||||
void ailang_drop_worklist_free(void *wl_opaque) {
|
||||
if (wl_opaque == NULL) {
|
||||
return;
|
||||
}
|
||||
ailang_drop_worklist_t *wl = (ailang_drop_worklist_t *)wl_opaque;
|
||||
free(wl->data);
|
||||
free(wl);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user