tidy: rewrite stale 18b/18c.x doc-headers to match shipped state

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_<m>_<T>` 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.
This commit is contained in:
2026-05-08 13:34:32 +02:00
parent 973e075f43
commit 6b3ff3bbed
3 changed files with 68 additions and 45 deletions
+11 -8
View File
@@ -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_<m>_<T>` 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;
+15 -7
View File
@@ -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_<owner>_<T>(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"
));
+42 -30
View File
@@ -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 18b18e:
*
* 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_<m>_<T>(ptr)` functions emitted by
* codegen; on dec-to-zero the cascade walks pointer-typed fields
* before freeing the outer cell.
* - 18d.118d.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_<m>_<T>(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_<m>_<T>` 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;