From 6511838b58ab88c909368bff1cc29f9473d4c817 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 8 May 2026 10:37:34 +0200 Subject: [PATCH] =?UTF-8?q?JOURNAL:=20Iter=2018c.4=20=E2=80=94=20per-type?= =?UTF-8?q?=20drop=20fn=20+=20recursive=20dec=20cascade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the drop-symbol scheme (uniform @drop__ for every ADT, even no-boxed-children ones; pair+env drop fns for escaping closures), field_drop_call's per-type dispatch and the three fall-back cases (Str / Fn / Type::Var), the unchanged Term::Let emission gates, and the explicit deferral of stack-recursion to 18e's worklist allocator. Also lists the four known-debt items (stack-recursive drop, closure-typed ADT fields, Type::Var fields, fn parameters) so 18d's design surface is clear. --- docs/JOURNAL.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index d9a0c38..99f4a3a 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -6465,3 +6465,177 @@ After 18c.4 closes, the 18-arc continues with 18d (reuse hints 18-arc closes — at which point the new tidy-iter rule from CLAUDE.md kicks in: the next iter is `ailang-architect`-driven drift cleanup before 19 starts. + +## 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 call site routes through these drops +instead of `ailang_rc_dec` directly, so a recursive ADT (`List`, +`Tree`) under RC actually frees its tail cells when the outer +binder's drop fires. + +### The drop-symbol scheme + +For every `Def::Type` `T` in module `m`, codegen emits one +`define void @drop__(ptr %p)` under `--alloc=rc`. The body +is uniformly shaped: + +1. Null-guard the pointer (defensive — drop on null is a no-op). +2. Load tag from offset 0. +3. Switch on tag; one arm per ctor. +4. In each arm, for every pointer-typed field, load the field + and dispatch through `field_drop_call` (described below). +5. After every field is dec'd, `call void @ailang_rc_dec(ptr %p)` + to free the outer cell. +6. `ret void`. + +For closures, `Term::Lam` emits two drop fns when the closure +escapes (heap-allocated): `drop___env(ptr %env)` for +the captured-free-vars block, and `drop___pair(ptr +%pair)` for the `{ env, fn-ptr }` pair. The pair-drop dec's the +env via `drop___env`, then dec's the pair box. A new +`closure_drops: BTreeMap` field on `Emitter` +keys closure-pair SSA names to their pair-drop symbol so +`Term::Let` lookup is O(1). + +Decision (preempted in the brief): **always emit +`@drop__` for every ADT, even ones with no boxed +children.** A no-boxed-children ADT (like `Box(Int)`) gets a +drop fn whose body is just `null-guard; ailang_rc_dec; ret`. +Call sites uniformly call `drop__(ptr v)` and never +branch on "does this type have boxed children?". Two payoffs: +(1) the codegen call-site code is simpler; (2) any future +codegen pass that wants to thread additional cleanup through +the drop seam (e.g. atomic dec under threading) has one +canonical entry point per type. + +### `field_drop_call` — per-field dispatch + +Given a field's `Type`, `field_drop_call` produces the LLVM +instruction(s) that drop the field's value: + +- `Type::Con { name, .. }` (an ADT field) → call + `drop__` after qualifying `` via the + `import_map`. Recursive `IntList` → recursive `drop__IntList` + call, which is the cascade. +- `Type::Str` → `ailang_rc_dec` directly. Strings have no boxed + children and are not yet wrapped in their own per-type drop + (deferred — the per-type-drop seam is uniform but `Str` is a + primitive in DESIGN.md's eyes; no behaviour change). +- `Type::Fn` (a closure-typed field) → fall back to + `ailang_rc_dec`. The closure-pair dec route requires + `closure_drops` lookup which is keyed by *expression site*, + not *type*; a Fn-typed *field* doesn't carry the closure-pair + ID. Documented as a 18d/18e debt — closure-typed ADT fields + leak their captured envs. +- `Type::Var` (parameterised type, e.g. `Cons`'s head field + in a polymorphic `List`) → fall back to `ailang_rc_dec`. + We don't have monomorphisation yet, so `a` could be `Int` + (no-op) or `(con List Int)` (needs cascade). The conservative + choice is shallow free; full handling is monomorphisation + territory (orthogonal to RC, not in the 18-arc). + +The two fall-back cases are flagged at `field_drop_call` with +"# Iter 18c.4 debt" comments so they are greppable. + +### `Term::Let` switch-over + +The 18c.3 emission seam at `Term::Let` scope close now calls +`drop_symbol_for_binder(value, val_ssa)` instead of +`ailang_rc_dec` directly: + +- `Term::Ctor { type, .. }` binders → `drop__` + (looked up via `import_map`; falls back to current module if + the type is local). +- `Term::Lam` binders → the `closure_drops`-recorded pair-drop + symbol. +- Other expression shapes → unchanged from 18c.3 (codegen does + not own the lifecycle of a returned-from-callee box; that is + 18d's mode-aware story). + +The other emission gates from 18c.3 (`consume_count == 0`, +body-tail-not-binder, block-not-terminated, `non_escape` +membership) are unchanged. 18c.4 only swaps the *target* of the +call. + +### Recursion is stack-recursive (deferred to 18e) + +`drop__IntList`'s `Cons` arm calls `drop__IntList` on the +tail. For a 5-cell list, that's 5 stack frames. For a million- +cell list, that's a million frames and a stack overflow. **Iter +18e replaces this with a worklist allocator** — the recursive +call becomes a push-onto-worklist + iterative-pop loop. The +recursive call site in `field_drop_call` is commented with +"replaced in 18e by worklist-based iterative free". + +For 18c.4's shipping fixtures (5-element list), the depth is +safely below any reasonable stack. The worklist conversion is +mechanical once 18e introduces the worklist primitive. + +### Test additions + +- `examples/rc_list_drop.ail.json` — 5-element `IntList` summed + via `sum_list`; `xs` has `consume_count == 1` (passed to + `sum_list`), so codegen does NOT dec at main's let-close. + Fixture's purpose is to lock in the IR shape under + `--alloc=rc` (covered by the codegen unit test) and confirm + the recursive ADT compiles + runs correctly under both + allocators. Stdout: `15`. + +- `examples/rc_list_drop_borrow.ail.json` — same 5-element + `IntList`, but `main` only matches it (borrow) and prints the + head. `xs` has `consume_count == 0`, so codegen DOES emit + `call void @drop_rc_list_drop_borrow_IntList(ptr xs)` at + scope close. The drop fn's `Cons` arm recurses on the tail, + cascading through all 5 cells and freeing each in turn. The + E2E test asserts identical stdout vs `--alloc=gc` AND clean + exit (which is the implicit "no segfault, no underflow" check + for the recursive cascade). + +- `crates/ailang-codegen/src/lib.rs` unit test + `rc_alloc_emits_recursive_drop_fn_for_recursive_adt` — IR + shape lock-in. Asserts: + 1. `define void @drop_rclist_IntList(ptr %p)` is emitted + under `--alloc=rc`. + 2. The fn body contains a recursive call + `call void @drop_rclist_IntList(ptr %v...)` (the cascade). + 3. The fn body ends with + `call void @ailang_rc_dec(ptr %p)` (outer-box dec). + 4. Under `--alloc=gc`, NO `@drop_rclist_IntList` symbol + appears (negative complement — drop fns are an RC-only + emission). + +### Test deltas + +- E2E: 53 → 55 (+2: `alloc_rc_recursive_list_sum`, + `alloc_rc_borrow_only_recursive_list_drop`). +- ailang-codegen unit: 11 → 12 (+1: IR-shape test). +- All other buckets unchanged. +- `cargo test --workspace` green. + +### Known debt (deliberate, deferred) + +- **Stack-recursive drop** — Iter 18e replaces with worklist + allocator. Long lists overflow the stack today. +- **Closure-typed ADT fields** fall back to shallow + `ailang_rc_dec`. `field_drop_call`'s `Type::Fn` arm is the + greppable seam. +- **Parameterised type fields** (`Type::Var`) fall back to + shallow `ailang_rc_dec`. Full handling is monomorphisation, + orthogonal to RC. +- **Fn parameters** still don't get dec'd at fn return — the + caller-handed-off-ownership signal is the `(own T)` mode, but + wiring it through codegen is part of the wider mode-aware + story (18d alongside reuse hints). + +### Next + +Iter 18d: reuse hints + reuse analysis. The `(reuse-as +old-binder NewCtor)` form lets the author signal that a freed +cell can be overwritten in-place with a new ctor of the same +shape, avoiding the dec+malloc round-trip. Codegen lowers it to +a tag-overwrite + field-rewrite, eliding both the `@drop_` and +the `@ailang_rc_alloc` calls. Inference identifies the cases +where the rewrite is safe (the freed binder's last use is +exactly here, the new ctor has the same shape, no aliasing).