Iter 18c.4: per-type drop fn + recursive dec cascade
Closes 18c.3's main scope cut. Codegen now emits a per-ADT and
per-closure drop function under --alloc=rc; the Term::Let
scope-close emission routes through these drops instead of
ailang_rc_dec directly, so a recursive ADT (List, Tree) under
--alloc=rc actually frees its tail cells when the outer binder
is dec'd.
Drop-symbol scheme:
- For every Def::Type T in module m: emit one
define void @drop_<m>_<T>(ptr %p) under --alloc=rc.
Body: null-guard, load tag, switch on tag, per-ctor arm dec's
every pointer-typed field via field_drop_call, then dec's the
outer cell and rets.
- For every escaping Term::Lam: emit drop_<m>_<lam>_env(ptr) for
the captured-free-vars block + drop_<m>_<lam>_pair(ptr) for the
{env, fn-ptr} closure pair. A new closure_drops side table on
Emitter keys closure-pair SSAs to their pair-drop symbol.
Decision (preempted in dispatch): always emit @drop_<m>_<T> for
every ADT, even ones with no boxed children. The body for
no-boxed-children ADTs is null-guard + dec + ret. Call sites are
uniform; future codegen changes that thread cleanup through the
drop seam (atomic dec under threading, etc.) have one canonical
entry per type.
field_drop_call dispatches on field type:
- Type::Con { name, .. } → drop_<owner>_<name> (recursion).
- Type::Str / Type::Fn / Type::Var → fall back to ailang_rc_dec.
These three fall-backs are 18d/18e/monomorphisation debt; they
are flagged in-source for grep.
Term::Let scope-close emission is the same as 18c.3 modulo the
target: Term::Ctor binders → drop_<owner>_<type>; Term::Lam
binders → the closure_drops-recorded pair-drop symbol. All
other emission gates from 18c.3 (consume_count == 0,
body-tail-not-binder, block-not-terminated, non-escape) are
unchanged.
Recursion is stack-recursive — drop_IntList's Cons arm calls
itself on the tail. For 18c.4's 5-element fixtures the depth is
safe; long lists are out of scope until 18e replaces the
recursive call with a worklist allocator. The recursive site is
commented for grep.
Tests:
- examples/rc_list_drop.ail.json — 5-element IntList summed via
sum_list (xs has consume_count == 1; codegen skips the let-
close drop call but the IR shape is locked in by the unit test
below).
- examples/rc_list_drop_borrow.ail.json — same 5-element IntList,
match-only consumption (consume_count == 0). Codegen emits
drop_<m>_IntList(xs) at scope close; the cascade runs over all
5 cells at runtime.
- crates/ail/tests/e2e.rs — alloc_rc_recursive_list_sum +
alloc_rc_borrow_only_recursive_list_drop. Both assert
--alloc=rc stdout matches --alloc=gc and the binaries exit
cleanly.
- crates/ailang-codegen/src/lib.rs — unit test asserting the IR
shape: define void @drop_rclist_IntList header, recursive
self-call inside, outer ailang_rc_dec at the tail; negative-
complement under --alloc=gc.
Tests: E2E 53 -> 55, codegen unit 11 -> 12. cargo test
--workspace green.
This commit is contained in:
@@ -1379,3 +1379,64 @@ fn alloc_rc_emits_dec_for_unique_let_bound_box() {
|
||||
assert_eq!(stdout_rc.trim(), "42");
|
||||
assert_eq!(stdout_gc, stdout_rc, "alloc=rc must match alloc=gc on rc_box_drop");
|
||||
}
|
||||
|
||||
/// Iter 18c.4: per-type drop fns + recursive `dec` cascade. Builds a
|
||||
/// 5-element `IntList` and reduces it via `sum_list`. Under
|
||||
/// `--alloc=rc` the codegen emits `define void @drop_<m>_IntList`
|
||||
/// whose `Cons` arm recursively calls itself on the `tail` field —
|
||||
/// the first iter where a recursive ADT under RC frees its tail
|
||||
/// cells when their drop fn is invoked. Stdout must match `--alloc=gc`
|
||||
/// (`15`) and the binary must exit cleanly.
|
||||
///
|
||||
/// Note: in this fixture the binder `xs` has `consume_count == 1`
|
||||
/// (passed to `sum_list`), so codegen does NOT emit a drop call at
|
||||
/// `main`'s let-close — the cells stay leaked at process exit (the
|
||||
/// caller / fn-param dec story is 18d). The fixture's purpose is to
|
||||
/// (a) lock in the recursive-ADT IR-shape (covered in the codegen
|
||||
/// IR-shape test below) and (b) confirm the program still runs
|
||||
/// correctly. The companion `rc_list_drop_borrow` fixture exercises
|
||||
/// the dec path itself.
|
||||
#[test]
|
||||
fn alloc_rc_recursive_list_sum() {
|
||||
let example = "rc_list_drop.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_gc.trim(), "15");
|
||||
assert_eq!(stdout_rc.trim(), "15");
|
||||
assert_eq!(
|
||||
stdout_gc, stdout_rc,
|
||||
"alloc=rc must match alloc=gc on rc_list_drop"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 18c.4: actually exercise the recursive drop cascade at
|
||||
/// runtime. The let-binder `xs` of a 5-element `IntList` has
|
||||
/// `consume_count == 0` (only the match scrutinee, a borrow), so
|
||||
/// codegen emits `call void @drop_<m>_IntList(ptr xs)` at scope
|
||||
/// close. The drop fn's `Cons` arm recurses on the tail — for a
|
||||
/// 5-cell list that is 5 nested calls, exactly the recursive shape
|
||||
/// 18e will replace with an iterative worklist. The depth here is
|
||||
/// safely below any reasonable stack limit; longer lists are out
|
||||
/// of scope until 18e.
|
||||
///
|
||||
/// Properties guarded:
|
||||
/// 1. The binary runs to completion (no segfault from a
|
||||
/// mishandled recursive cascade or refcount underflow).
|
||||
/// 2. Stdout matches `--alloc=gc` byte-for-byte (`11` — the head
|
||||
/// of the 5-element list).
|
||||
/// 3. The match arm's pattern bindings (`h`, `t`) do NOT trip
|
||||
/// the dec machinery — `t` was loaded as a borrow inside the
|
||||
/// arm and must not be dec'd; we rely on `lower_match`'s
|
||||
/// pattern binding flow not adding a competing dec call.
|
||||
#[test]
|
||||
fn alloc_rc_borrow_only_recursive_list_drop() {
|
||||
let example = "rc_list_drop_borrow.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_gc.trim(), "11");
|
||||
assert_eq!(stdout_rc.trim(), "11");
|
||||
assert_eq!(
|
||||
stdout_gc, stdout_rc,
|
||||
"alloc=rc must match alloc=gc on rc_list_drop_borrow"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user