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.
Schema floor for explicit reuse hints. Adds Term::ReuseAs
{ source: Box<Term>, body: Box<Term> } with serde tag "reuse-as",
form-A (reuse-as <source> <body>). Schema choice (wrapper, not
modifier-on-Ctor) and rationale recorded in DESIGN.md.
Codegen is identity under all --alloc strategies — lower body,
drop source on the floor. Iter 18d.2 will lower this as the
in-place rewrite under --alloc=rc.
User-facing diagnostics:
- typecheck reuse-as-non-allocating-body: body must be a
Term::Ctor or Term::Lam, the two AST shapes that allocate.
Other shapes (literal, var, app, ...) emit this with a
suggested_rewrite that drops the wrapper.
- linearity reuse-as-source-not-bare-var: source must be a
Term::Var referring to an in-scope binder. Anything else
(literal, nested expression) is rejected here. Suggested
rewrite drops the wrapper.
- linearity use-after-consume at a reuse-as site: source must
not have been consumed earlier in the body. Suggested rewrite
drops the wrapper.
- Visiting reuse-as marks source consumed for the rest of the
body, so a subsequent use of the same binder flags
use-after-consume against it.
Uniqueness inference treats source as Consume — the consume
shows up in the side table for the codegen consumer.
Tests:
- 4 surface parse-tests (round-trip, no-args, one-arg, full
parse_term/term_to_form_a round-trip).
- 1 typecheck unit test (non-allocating body).
- 2 linearity unit tests (non-var source, after-consume).
- 1 integration test (happy-path map_inc in all-explicit mode
is linearity-clean).
- 1 E2E test running the canonical map_inc-via-reuse-as fixture
under --alloc=gc, asserting stdout 9.
- New fixture examples/reuse_as_demo.{ailx,ail.json}.
Test deltas: E2E 55 -> 56, ailang-check unit 43 -> 46,
ailang-check workspace 8 -> 9, surface 14 -> 18. cargo test
--workspace green. No existing fixture's canonical JSON changed.
Ends the leak-everything era under --alloc=rc. Codegen now emits
ailang_rc_inc at every Term::Clone site and ailang_rc_dec at
end-of-scope of trackable RC-allocated let-binders. Default
--alloc=boehm path is byte-identical to before this iter.
Two parts:
(A) New module ailang-check::uniqueness — post-typecheck
dataflow producing UniquenessTable: BTreeMap<(def, binder),
UniquenessInfo>. consume_count is max-over-paths: at if/match
the walker saves state, walks each arm against the snapshot,
and merges by taking the per-binder max. Term::Clone is a
Borrow on its inner term — the explicit clone is the user's
signal that a fresh ref is being produced via inc.
(B) Codegen emission in ailang-codegen. Term::Clone emits inc
under --alloc=rc, gated on val_ty == "ptr" and
!val_ssa.starts_with('@') (top-level fn closure-pair globals
live in the LLVM data segment, not in runtime/rc.c's heap).
Term::Let emits dec at scope close iff the value is
rc_alloc'd (Term::Ctor / Term::Lam in escape set under
--alloc=rc), val_ty == "ptr", consume_count == 0 (no callee
or outer site already took ownership), body's tail SSA is
not the binder itself (caller-takes-ownership case), and the
block isn't already terminated.
Header declares of @ailang_rc_inc / @ailang_rc_dec are gated on
--alloc=rc so Boehm/Bump IR shape is unchanged.
Fixture examples/rc_box_drop.ail.json: single-cell Box(Int) ADT
where shallow free is sufficient (no boxed children). E2E test
alloc_rc_emits_dec_for_unique_let_bound_box runs it under both
gc and rc, asserts byte-identical stdout, clean exit, expected
output (42).
Scope cuts deferred to 18c.4: per-type drop fn / recursive dec
cascades for ADTs with boxed children (List, Tree still leak
their tails). Fn params have no dec emission yet (no static
"caller handed off ownership" signal under Implicit mode);
under explicit (own T) the signal exists but wiring is 18d.
Tests: E2E 52 -> 53, ailang-check unit 38 -> 43 (+5 uniqueness),
all other buckets unchanged. cargo test --workspace green.