tidy: 18g sub-arc — DESIGN ratification, latency bench, negative coverage
Resolves the architect's drift report on the 18g sub-arc: - DESIGN.md: ratify mode-metadata's codegen role. param_modes / ret_mode were already on Type::Fn since 18a; with 18d.4 / 18g they became load-bearing for drop-emission decisions in codegen. The new 'Mode metadata is load-bearing for codegen' subsection records the four seams (Iter A + Iter B + 18g.1 + 18g.2) and names the let-alias-of-borrow carve-out the gates do not propagate through. - bench/run.sh: post-throughput, invoke bench/latency_harness.py on the three canonical arms (Implicit @ gc, explicit @ rc, Implicit @ rc control). The Boehm-retirement bench numbers are now reproducible by anyone running the harness, not just by hand on a specific host. - Negative coverage: examples/rc_let_implicit_returning_app + alloc_rc_let_binder_for_implicit_returning_app_does_not_drop pin the asymmetry to the (own)-ret-mode test (live=0 there, live=1 here). The Borrow-ret-mode case is covered by the language design itself — typechecker rejects 'borrow- passthrough' shapes with consume-while-borrowed. JOURNAL entry records four items as deferred known debt: emit_inlined_partial_drop dynamic-tag fallback, carve-out diagnostics surface, bench-number stat-of-N, and the cross-family ordering observation about CLAUDE.md's tidy-iter rule. The 18-arc is formally closed with this tidy. Next iter is the orchestrator's Boehm-retirement decision (Path A vs Path B from JOURNAL 2026-05-08 18f entry, joined by the post-18g.2 re-bench numbers).
This commit is contained in:
@@ -1038,6 +1038,78 @@ Codegen for `Term::Ctor` / `Term::Lam` env / closure pair under
|
||||
inference is wired up. Until then, `--alloc=rc` deliberately
|
||||
leaks like the pre-Boehm era; this is purely about plumbing.
|
||||
|
||||
### Mode metadata is load-bearing for codegen (Iter 18d–18g)
|
||||
|
||||
`param_modes` and `ret_mode` on `Type::Fn` are not merely
|
||||
typechecker metadata — codegen consults both to decide where to
|
||||
emit drop calls. The 18d.4 / 18g shipping work moved them from
|
||||
"annotation that the typechecker enforces" to "annotation that
|
||||
codegen reads to keep RC correct". Recorded here so the schema
|
||||
metadata's role is explicit:
|
||||
|
||||
**`param_modes` — drop-emission gates.**
|
||||
|
||||
- **Iter B: Own-param dec at fn return.** When a fn body
|
||||
fall-throughs to a `ret` (no tail-call), every parameter with
|
||||
`param_modes[i] == Own` is dec'd before the `ret` iff its
|
||||
uniqueness `consume_count == 0` and the ret value is not the
|
||||
param itself. `Borrow` and `Implicit` parameters are skipped:
|
||||
`Borrow` retains the caller's ownership by contract;
|
||||
`Implicit` carries no static caller-handed-off-ownership
|
||||
signal (it's the back-compat lane).
|
||||
|
||||
- **Iter A: arm-close pattern-binder dec.** When a match-arm's
|
||||
body terminates without a tail-call, every ptr-typed
|
||||
pattern-bound binder pushed by the arm is dec'd at arm close
|
||||
iff its `consume_count == 0` and it is not the arm's tail
|
||||
value, **gated on the scrutinee's static ownership**. If the
|
||||
scrutinee is a fn-param, only `Own`-mode scrutinees enable
|
||||
the dec — `Borrow` and `Implicit` scrutinees would let the
|
||||
arm dec memory the caller still references.
|
||||
|
||||
- **Iter 18g.1: pre-tail-call shallow-dec.** When a match-arm's
|
||||
body IS a tail call, both Iter A and Iter B are skipped (the
|
||||
block is terminated). A separate seam in `lower_match` emits
|
||||
a shallow `ailang_rc_dec` on the scrutinee outer cell BEFORE
|
||||
the tail call, gated identically on the scrutinee mode plus
|
||||
the requirement that every ptr-typed slot in the active
|
||||
ctor's pattern is in `moved_slots[scrutinee]`.
|
||||
|
||||
**`ret_mode` — let-binder trackability.**
|
||||
|
||||
- **Iter 18g.2: `Term::App` drop at let-scope close.** A
|
||||
let-binder whose value is `Term::App { callee, .. }` is
|
||||
trackable for scope-close drop iff the callee's
|
||||
`ret_mode == Own`. The signal is the callee's static
|
||||
contract that ownership of the freshly heap-allocated cell
|
||||
flows to the caller. `Borrow`-returning calls remain
|
||||
non-trackable (the callee retains ownership; the caller
|
||||
holds a view, not an own ref). `Implicit`-returning calls
|
||||
remain non-trackable (back-compat lane).
|
||||
|
||||
The drop fn's symbol resolution for an Own-returning App:
|
||||
synthesise the call's return type, resolve `Type::Con { name }`
|
||||
to `drop_<owner>_<T>` (with cross-module qualification through
|
||||
the import map). Falls back to shallow `ailang_rc_dec` for
|
||||
returns that are not `Type::Con` (e.g. unresolved type vars on
|
||||
a polymorphic call's pre-monomorphisation site; the
|
||||
monomorphised copies resolve to concrete drop fns).
|
||||
|
||||
**What this widening does NOT do.**
|
||||
|
||||
- Does not change the canonical hash. `param_modes` /
|
||||
`ret_mode` were already hash-load-bearing as of Iter 18a; the
|
||||
18d–18g iters add codegen consumers, not new schema fields.
|
||||
- Does not introduce a new `Type` variant. Mode metadata stays
|
||||
flat on `Type::Fn` (see "Schema additions" above on why).
|
||||
- Does not cover let-aliases of borrowed values. A let-binder
|
||||
whose value is `Term::Var` referencing a `Borrow`-mode
|
||||
param is not yet propagated through; the param-mode gates
|
||||
treat such a binder as "owned" (its `current_param_modes`
|
||||
lookup misses, default = owned). This is a known carve-out
|
||||
shared by Iter A and 18g.1; closing it is a propagation pass
|
||||
through let-bindings that has not shipped yet.
|
||||
|
||||
### Migration plan
|
||||
|
||||
1. **Iter 18a:** `(borrow T)` / `(own T)` annotations as a
|
||||
|
||||
Reference in New Issue
Block a user