From 6b3ff3bbed4cacfbbabc26d7d4b767d0b7d11fa0 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 8 May 2026 13:34:32 +0200 Subject: [PATCH] tidy: rewrite stale 18b/18c.x doc-headers to match shipped state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First half of the post-18-arc tidy-iter (per the new CLAUDE.md iter-cycle rule). Architect's drift review flagged module-doc headers describing 18b's leak-everything snapshot or 18c.x's "deferred" debt that has since shipped. Doc-only changes; cargo build clean, cargo test --workspace green at e2e=61, no behavioural change. - runtime/rc.c top-of-file header: rewrote from "Iter 18b deliberately stops at the layout and the alloc... programs leak every allocation" (false post-18c.3) to a stage summary spanning 18b–18e. Fixed `--memory=rc` reference (renamed to `--alloc=rc` in 18b's CLI work). Updated ailang_rc_inc / ailang_rc_dec block comments to point at `drop__` and the worklist as the cascade owners, not at "18c will wire this up". - ailang-check uniqueness.rs module-doc: replaced the "deferred to later iters" block (which named 18c.4 + 18d as future work, both shipped) with a current "what this pass does NOT do" block. Cross-fn reasoning is still genuinely deferred; per-type drop fns and recursive cascades are NOT this pass's job by design (codegen does them, not the inference). - ailang-codegen emit_drop_fn_for_type doc + in-body comment: rewrote "Iter 18e replaces the recursive call with an iterative worklist free" to describe the actual shipped behaviour — the 18e (drop-iterative) annotation routes annotated types through emit_iterative_drop_fn_for_type; unannotated types stay recursive by orchestrator design (cheaper IR, no worklist alloc). Held back for the second half of the tidy-iter (pending the ailang-bencher determinism result): - DESIGN.md Decision 10 line 700 says modes are "mandatory" but lines 940–952 admit they're opt-in with deferred mandatoriness. The bench result either supports tightening the mandatoriness claim or backs down to "opt-in with performance benefit" — orchestrator-level decision blocked on the bench data. - Dynamic-tag partial-drop debt is captured in JOURNAL but should be surfaced in DESIGN as a known precision gap. --- crates/ailang-check/src/uniqueness.rs | 19 ++++--- crates/ailang-codegen/src/lib.rs | 22 +++++--- runtime/rc.c | 72 ++++++++++++++++----------- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/crates/ailang-check/src/uniqueness.rs b/crates/ailang-check/src/uniqueness.rs index 003d09a..daed723 100644 --- a/crates/ailang-check/src/uniqueness.rs +++ b/crates/ailang-check/src/uniqueness.rs @@ -45,15 +45,18 @@ //! treated as Consume — the same conservative decision the linearity //! check makes. //! -//! ## What this pass deliberately doesn't do (deferred to later iters) +//! ## What this pass does NOT do //! -//! - **Cross-fn reasoning.** The inference is whole-fn local. It -//! does not reason about a callee's body when classifying a -//! binder passed into the callee. -//! - **Per-type drop fns / recursive `dec` cascades.** That is -//! 18c.4. Codegen using this table emits *shallow* `dec` only — -//! the refcount of the box itself, not its boxed children. -//! - **Reuse hints.** `(reuse-as)` is 18d. +//! - **Cross-fn reasoning.** The inference is whole-fn local. It does +//! not reason about a callee's body when classifying a binder passed +//! into the callee. Cross-fn ownership currently flows through +//! explicit mode annotations on the callee's signature (18a). +//! - **Per-type drop fns and recursive `dec` cascades.** Those are +//! emitted by codegen (`emit_drop_fn_for_type`), not consulted from +//! this side table. The inference's role is "should codegen emit a +//! dec on this binder leaving scope at all?"; the *shape* of the +//! dec (uniform `drop__` call vs inlined partial drop) is a +//! codegen-side decision driven by `moved_slots`. use ailang_core::ast::{Arm, Def, FnDef, Module, ParamMode, Pattern, Term, Type}; use std::collections::BTreeMap; diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 98b6c2c..b8b6707 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -810,10 +810,14 @@ impl<'a> Emitter<'a> { /// Recursion: when a ctor field's type is the same ADT (or any /// ADT in the workspace), the emitted call to /// `@drop__(field)` is recursive at the IR level and - /// will overflow the stack on long lists. Iter 18e replaces the - /// recursive call with an iterative worklist free; for 18c.4 the - /// recursion is intentional and shallow — the assignment lifts - /// the bound to "5-element list" fixtures only. + /// will overflow the stack on long lists. The 18e + /// `(drop-iterative)` annotation routes such types through + /// [`Self::emit_iterative_drop_fn_for_type`] instead, which + /// replaces the recursive call with a worklist push. ADTs WITHOUT + /// the annotation continue to use this recursive form — the + /// orchestrator's choice: opt-in iterative drop where the depth + /// is known to grow, recursive cascade everywhere else (cheaper + /// IR, no worklist allocation). fn emit_drop_fn_for_type(&mut self, td: &TypeDef) { let m = self.module_name; let tname = &td.name; @@ -862,9 +866,13 @@ impl<'a> Emitter<'a> { " %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n" )); let drop_call = self.field_drop_call(fty); - // Iter 18e replaces this recursive call with an - // iterative worklist push. For 18c.4 it stays - // recursive — fine for the shipping fixtures. + // Recursive call into the field's drop fn. If the + // field's type is itself `(drop-iterative)`, that drop + // fn is the worklist variant — recursion stops at one + // level. Otherwise this is the unbounded recursive + // cascade; safe only on bounded-depth ADTs (the + // `(drop-iterative)` annotation exists for the + // unbounded ones). out.push_str(&format!( " call void @{drop_call}(ptr %v{val_id})\n" )); diff --git a/runtime/rc.c b/runtime/rc.c index f7c097a..5bcb381 100644 --- a/runtime/rc.c +++ b/runtime/rc.c @@ -1,20 +1,29 @@ -/* AILang reference-counting runtime — Iter 18b. +/* AILang reference-counting runtime. * - * This is the allocator + counter primitives for `ail build --memory=rc`. - * It establishes the memory layout (8-byte refcount header preceding - * every allocation) and the three runtime entry points the codegen will - * eventually call: ailang_rc_alloc / ailang_rc_inc / ailang_rc_dec. + * Allocator + counter primitives for `ail build --alloc=rc`, plus the + * worklist allocator for `(drop-iterative)` data types. The runtime + * shape is set by Decision 10 (RC + uniqueness inference) and grew + * across Iter 18b–18e: * - * Iter 18b deliberately stops at the *layout* and the *alloc*. The - * codegen routes `Term::Ctor` / `Term::Lam` env / closure-pair sites - * through `ailang_rc_alloc` instead of `GC_malloc` / `bump_malloc`, but - * does NOT yet emit `inc` or `dec` calls anywhere. Programs running - * under `--memory=rc` therefore leak every allocation — the same - * behaviour as the pre-Boehm era. This is intentional: the next iter - * (18c) ships uniqueness inference and the codegen pass that emits - * inc/dec. 18b is purely about plumbing the allocator and validating - * that compiled programs still produce correct output under the new - * allocator. + * - 18b shipped the 8-byte-refcount-header layout and + * `ailang_rc_alloc`. Programs leaked everything because codegen + * did not yet emit inc/dec. + * - 18c.3 added `ailang_rc_inc` / `ailang_rc_dec` emission at + * `Term::Clone` and at `Term::Let` scope close (when the binder is + * a unique RC-allocated value). + * - 18c.4 added per-type `drop__(ptr)` functions emitted by + * codegen; on dec-to-zero the cascade walks pointer-typed fields + * before freeing the outer cell. + * - 18d.1–18d.4 added explicit `(reuse-as)` rewrites and move-aware + * pattern-binder + Own-param dec at scope close, all built on top + * of these runtime primitives without changing the ABI. + * - 18e added `ailang_drop_worklist_*` for ADTs annotated + * `(drop-iterative)`. Codegen swaps the recursive cascade for a + * worklist loop; deep ADT chains free without stack growth. + * + * The ABI defined here is stable; adding behaviour to the codegen + * (further uniqueness elision, atomic refcounts under threading, etc.) + * will not require runtime changes unless the layout itself shifts. * * Layout: * @@ -80,28 +89,31 @@ void ailang_rc_inc(void *payload) { if (payload == NULL) { return; } - /* Heuristic for "static, do not touch": the static closure-pair env - * pointers (Iter 8b) live in the LLVM data segment, not in heap - * memory we allocated. We cannot trivially distinguish them at - * runtime without a flag bit; for Iter 18b, we accept that inc on - * static memory is undefined behaviour. Iter 18c's codegen will - * elide inc/dec for known-static pointers, so this path will not - * be reached for them in practice. */ + /* Static closure-pair env pointers (Iter 8b) live in the LLVM data + * segment, not in heap memory we allocated. Codegen elides inc/dec + * for known-static pointers (the `@`-prefix gate added in 18c.3), + * so this path is not reached for them in practice. The runtime + * itself has no header-bit flag distinguishing static from heap; + * if codegen ever loses the elision, inc on a static pointer is + * undefined behaviour. */ ailang_rc_header_t *hdr = header_of(payload); *hdr += 1; } /* Refcount -= 1. If it reaches zero, frees the underlying block. * - * Iter 18b deliberately does NOT recursively dec child references. - * That requires per-type traversal info (which fields are pointer- - * typed, which are unboxed), which is added in Iter 18c when the - * codegen learns to emit per-ctor `dec` cascades. For now, free-on- - * zero just frees the box; any boxed children leak. + * This function performs only the *outer* free. The per-type + * `drop__(ptr)` functions emitted by codegen (Iter 18c.4) are + * what walk pointer-typed children before calling `ailang_rc_dec` on + * the outer cell. For ADTs annotated `(drop-iterative)`, codegen + * emits a worklist loop using `ailang_drop_worklist_*` (Iter 18e) + * instead of recursive cascade, allowing arbitrarily deep ADT chains + * to free without stack growth. * - * Iter 18b never emits `dec` calls from codegen, so this fn is - * effectively dead code in 18b. It exists so the runtime ABI is - * complete and 18c can wire codegen up against a stable surface. */ + * Calling `ailang_rc_dec` directly on a pointer whose type has boxed + * children will leak those children. Codegen routes through + * `drop__` when it knows the type; this entry point is the + * shared bottom they all converge on. */ void ailang_rc_dec(void *payload) { if (payload == NULL) { return;