feat(codegen): relocate per-binder consume_count into MIR; delete codegen's second uniqueness run (mir.3a)

First of two iterations delivering spec 0060's mir.3 row (plan
docs/plans/0117-mir.3a-consume-count-relocation.md). The per-binder
consume_count — the only thing codegen read from its second
infer_module_with_cross run — now lives on a new binder-keyed
MirDef.consume: BTreeMap<String,u32>, filled once by lower_to_mir on the
post-mono body. Codegen builds its existing (def,binder)->consume_count
lookup from those maps and the three scope-close drop sites (own-param,
let-binder, match-arm) read it; the codegen uniqueness run, its
UniquenessTable field, and the import are deleted.

mir.3 is split into two iterations (recorded in spec 0060): the named
deletion depends ONLY on consume_count. All three drop sites read
consume_count from self.uniqueness, while the modes they also gate on
(param_modes for the own-param dec, scrutinee_is_owned for the match
dec) come from the type, not the uniqueness pass (UniquenessInfo carries
no mode). So mir.3a relocates consume_count and ships the deletion;
mir.3b (next) fills MArg.mode/MTerm::Let.mode and switches emit_call's
anon-temp gate onto MArg.mode. The split halves the change at the
codebase's highest-risk surface (RC/drop correctness, the raw-buf leak
saga) and makes each half independently verifiable against the live=0
leak pins.

Design: consume_count lands on MirDef (binder-keyed), NOT on MArg. The
drop sites key by (def, binder_name) — a per-binder aggregate — while the
spec-sketched MArg.consume_count is per-arg-position and never reaches
them; the per-def map matches the UniquenessTable's own keying and all
three binder kinds (param/let/pattern) uniformly. MArg.consume_count
stays a mir.1 default. Source = run check's infer_module_with_cross
inside lower_to_mir post-mono (the synth_pure single-engine-re-walked
pattern); retaining a check-time result is blocked (check's uniqueness
is pre-mono and runs on-demand-from-codegen, never in check_workspace,
so it would miss the mono specialisations). cross_module_types
(= codegen's module_def_ail_types) is rebuilt in elaborate_workspace
from the post-mono workspace.

Safety property held: this is a PURE RELOCATION of an identical
computation — infer_module_with_cross ran on the post-mono module in
codegen and now runs on the same post-mono module in lower_to_mir, so
drop placement is byte-identical. cross_module_types matches
module_def_ail_types exactly (both insert f.name->f.ty over post-mono
Def::Fn), confirmed by the leak pins staying green.

Verification (orchestrator, post-implement inspect): diff matches the
plan; codegen grep-clean for infer_module_with_cross + UniquenessTable
(now ailang-check-internal); cargo build --workspace clean (no errors,
no unused warnings — module_def_ail_types survives for emit_call's
anon-temp param_modes gate); cargo test --workspace 701 passed / 0
failed / 3 ignored (+1 = the new producer pin
mirdef_consume_is_populated_for_let_binder; no #[ignore] added). The
live=0 drop-correctness witnesses all green:
alloc_rc_loop_valued_rawbuf_binder_drops_at_scope_close (#43/#47),
alloc_rc_heap_loop_binder_replaced_by_recur_does_not_leak,
alloc_rc_print_int_does_not_leak_show_result_str. #49 stays #[ignore]
(mir.4); #51/#53 build guards green.
This commit is contained in:
2026-05-31 19:48:57 +02:00
parent 69749fc3e1
commit 6bc0b501cb
7 changed files with 108 additions and 29 deletions
+26 -25
View File
@@ -39,8 +39,6 @@ use ailang_core::Workspace;
use ailang_mir::{Callee, MArg, MTerm, MirDef, MirWorkspace};
use std::collections::{BTreeMap, BTreeSet};
use ailang_check::uniqueness::{infer_module_with_cross, UniquenessTable};
mod drop;
mod escape;
mod intercepts;
@@ -839,13 +837,15 @@ struct Emitter<'a> {
/// Decided at the top-level entry point (`lower_workspace_inner`)
/// and propagated to every site that emits a `call ptr @<alloc>(...)`.
alloc: AllocStrategy,
/// per-binder uniqueness side-table for the current
/// module, keyed by `(def_name, binder_name)`. Built once per
/// emitter and consulted by `Term::Let` lowering to decide whether
/// to emit `call void @ailang_rc_dec(ptr %v)` at scope close. The
/// table is module-scoped because the inference is whole-fn local;
/// no cross-module entries appear.
uniqueness: UniquenessTable,
/// per-binder consume_count for the current module, keyed by
/// `(def_name, binder_name)`. Consulted by `Term::Let` lowering
/// (and the own-param / match-arm drop sites) to decide whether to
/// emit `call void @ailang_rc_dec(ptr %v)` at scope close. The table
/// is module-scoped because the inference is whole-fn local; no
/// cross-module entries appear. Built from `MirDef.consume` (mir.3a)
/// — codegen no longer re-runs the uniqueness pass; `lower_to_mir`
/// owns the single run.
consume: std::collections::BTreeMap<(String, String), u32>,
/// per-closure-pair drop-function symbol. Keyed by the
/// closure-pair SSA value (e.g. `%v17`) the most-recent
/// `lower_lambda` call returned. Consulted by the `Term::Let`
@@ -1016,17 +1016,18 @@ impl<'a> Emitter<'a> {
}
}
// build the per-module uniqueness side-table once
// per emitter. The inference is pure (no I/O, no global state),
// so doing it here is cheap and keeps codegen's input self-
// contained. Pass the workspace-flat cross-module signature
// table so a qualified cross-module borrow callee (e.g. the
// monomorphised intrinsic `raw_buf.RawBuf_get__Int` reached
// from a `main` in another module) resolves to its registered
// `param_modes` instead of defaulting every arg to Consume —
// which otherwise inflates the borrow binder's consume_count
// and gates off its scope-close drop.
let uniqueness = infer_module_with_cross(module, module_def_ail_types);
// mir.3a: read the per-binder consume_count off MIR (filled by
// lower_to_mir from the single uniqueness pass) instead of
// re-running the uniqueness pass here.
let consume: std::collections::BTreeMap<(String, String), u32> = mir_bodies
.iter()
.flat_map(|d| {
let dname = d.name.clone();
d.consume
.iter()
.map(move |(b, c)| ((dname.clone(), b.clone()), *c))
})
.collect();
Self {
module,
@@ -1054,7 +1055,7 @@ impl<'a> Emitter<'a> {
deferred_thunks: Vec::new(),
non_escape: NonEscapeSet::new(),
alloc,
uniqueness,
consume,
closure_drops: BTreeMap::new(),
moved_slots: BTreeMap::new(),
current_param_modes: BTreeMap::new(),
@@ -1508,9 +1509,9 @@ impl<'a> Emitter<'a> {
continue;
}
let consume_count = self
.uniqueness
.consume
.get(&(self.current_def.clone(), pname.clone()))
.map(|info| info.consume_count)
.copied()
.unwrap_or(u32::MAX);
if consume_count != 0 {
continue;
@@ -1875,9 +1876,9 @@ impl<'a> Emitter<'a> {
// so the refcount story is unchanged.
if trackable && val_ty == "ptr" && !self.block_terminated {
let consume_count = self
.uniqueness
.consume
.get(&(self.current_def.clone(), name.clone()))
.map(|info| info.consume_count)
.copied()
.unwrap_or(u32::MAX); // Defensive: skip if missing.
let body_returns_binder = match &r {
Ok((body_ssa, _)) => body_ssa == &val_ssa,
+2 -2
View File
@@ -794,9 +794,9 @@ impl<'a> Emitter<'a> {
continue;
}
let consume_count = self
.uniqueness
.consume
.get(&(self.current_def.clone(), bname.clone()))
.map(|info| info.consume_count)
.copied()
.unwrap_or(u32::MAX);
if consume_count != 0 {
continue;