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
+20 -1
View File
@@ -1343,9 +1343,28 @@ pub fn elaborate_workspace(
.map_err(|e| vec![internal_diag(format!("monomorphise_workspace: {e}"))])?;
// 3. lower each post-mono module to MIR.
let env = build_check_env(&mono);
// Workspace-flat `module -> def -> Type` table — the shape
// `uniqueness::infer_module_with_cross` consumes to resolve a
// cross-module callee's `param_modes` (so a Borrow-mode arg is
// walked as a borrow, not a consume). Identical in content to
// codegen's `module_def_ail_types`; built here from the post-mono
// workspace so the uniqueness pass runs in the producer (mir.3a).
let mut cross_module_types: std::collections::BTreeMap<
String,
std::collections::BTreeMap<String, ailang_core::ast::Type>,
> = std::collections::BTreeMap::new();
for (mname, m) in &mono.modules {
let mut def_tys = std::collections::BTreeMap::new();
for def in &m.defs {
if let ailang_core::ast::Def::Fn(f) = def {
def_tys.insert(f.name.clone(), f.ty.clone());
}
}
cross_module_types.insert(mname.clone(), def_tys);
}
let mut modules = std::collections::BTreeMap::new();
for (name, m) in &mono.modules {
let mir = lower_to_mir::lower_module(m, &env)
let mir = lower_to_mir::lower_module(m, &env, &cross_module_types)
.map_err(|e| vec![internal_diag(format!("lower_to_mir in `{name}`: {e}"))])?;
modules.insert(name.clone(), mir);
}
+18 -1
View File
@@ -408,7 +408,11 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
/// env (built by `build_check_env`); this fn sets `env.imports` for
/// the module and seeds per-fn `locals` from the declared params,
/// mirroring mono.rs:771-816.
pub fn lower_module(module: &Module, env: &Env) -> Result<MirModule> {
pub fn lower_module(
module: &Module,
env: &Env,
cross_module_types: &std::collections::BTreeMap<String, std::collections::BTreeMap<String, Type>>,
) -> Result<MirModule> {
let mut env = env.clone();
env.current_module = module.name.clone();
// Seed `env.globals` from the current module's fns so `synth`'s
@@ -458,6 +462,14 @@ pub fn lower_module(module: &Module, env: &Env) -> Result<MirModule> {
for m in all_modules {
env.imports.entry(m.clone()).or_insert(m);
}
// mir.3a: run the single uniqueness engine ONCE on this post-mono
// module. The resulting per-(def, binder) consume_count is attached
// to each `MirDef.consume` below; codegen reads it instead of
// re-running `infer_module_with_cross`. Same pass, same post-mono
// input as codegen used — a pure relocation, so drop placement is
// unchanged.
let uniqueness =
crate::uniqueness::infer_module_with_cross(module, cross_module_types);
let mut defs = Vec::new();
let mut consts = Vec::new();
for def in &module.defs {
@@ -521,6 +533,11 @@ pub fn lower_module(module: &Module, env: &Env) -> Result<MirModule> {
sig: f.ty.clone(),
params: f.params.clone(),
body,
consume: uniqueness
.iter()
.filter(|((d, _), _)| d == &f.name)
.map(|((_, b), info)| (b.clone(), info.consume_count))
.collect(),
});
}
Ok(MirModule { name: module.name.clone(), ast: module.clone(), defs, consts })