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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -149,6 +149,23 @@ fn callee_classification_builtin_and_static() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mirdef_consume_is_populated_for_let_binder() {
|
||||
// mir.3a: lower_to_mir runs the uniqueness pass and attaches the
|
||||
// per-binder consume_count to MirDef.consume. `main`'s `(let c …)`
|
||||
// binder must appear in the map (the exact count is validated
|
||||
// end-to-end by the RC-stats leak pins; this pin guards that the
|
||||
// producer fills the field at all, so codegen has it to read).
|
||||
let ws = elaborate_fixture("new_counter_user_adt");
|
||||
let m = ws.modules.get("new_counter_user_adt").expect("module");
|
||||
let main = m.defs.iter().find(|d| d.name == "main").expect("main");
|
||||
assert!(
|
||||
main.consume.contains_key("c"),
|
||||
"MirDef.consume must carry the `c` let-binder, got {:?}",
|
||||
main.consume,
|
||||
);
|
||||
}
|
||||
|
||||
/// Recursively search for a `MTerm::Str { rep: Static }` reachable
|
||||
/// from a loop binder init (the seed).
|
||||
fn find_static_str_seed(t: &MTerm) -> bool {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -55,6 +55,14 @@ pub struct MirDef {
|
||||
pub sig: Type,
|
||||
pub params: Vec<String>,
|
||||
pub body: MTerm,
|
||||
/// Per-binder consume count for this def's body, keyed by binder
|
||||
/// name (let-binders, params, pattern-binders). `0` means the
|
||||
/// binder is exclusively borrowed/unused in its scope and codegen
|
||||
/// must emit the scope-close `dec` under `--alloc=rc`. Filled by
|
||||
/// `lower_to_mir` from `ailang_check::uniqueness::infer_module_with_cross`
|
||||
/// (mir.3a) — the single uniqueness engine, run once on the
|
||||
/// post-mono body. Codegen reads it instead of re-running the pass.
|
||||
pub consume: BTreeMap<String, u32>,
|
||||
}
|
||||
|
||||
/// One MIR node — structural counterpart of `ast::Term`, plus the
|
||||
|
||||
@@ -322,6 +322,23 @@ class and deletes the matching codegen re-derivation.
|
||||
> type-scoped names in the current corpus, so the acceptance
|
||||
> "type_home_module grep-clean" holds.
|
||||
|
||||
> mir.3 split (discovered in planning, `docs/plans/0117-mir.3a-consume-count-relocation.md`):
|
||||
> mir.3 ships in two iterations. **mir.3a** relocates `consume_count`
|
||||
> only — the deletion of the second `infer_module_with_cross` run
|
||||
> depends solely on it (all three codegen drop sites read `consume_count`
|
||||
> from `self.uniqueness`; the *modes* they also test come from the
|
||||
> type, not the uniqueness pass, which carries no mode). The per-binder
|
||||
> `consume_count` lands on a new `MirDef.consume: BTreeMap<String, u32>`
|
||||
> (binder-keyed, matching how the drop sites and the `UniquenessTable`
|
||||
> key it — the spec's `MArg.consume_count` field is per-arg-position
|
||||
> and does not reach the binder-keyed drop sites, so it stays a mir.1
|
||||
> default). lower_to_mir runs the single uniqueness pass on the
|
||||
> post-mono body — a pure relocation of the identical computation
|
||||
> codegen ran, so drop placement is byte-identical. **mir.3b** fills
|
||||
> `MArg.mode` / `MTerm::Let.mode` from the type modes and switches
|
||||
> `emit_call`'s anon-temp borrow-slot gate onto `MArg.mode` (additive;
|
||||
> no further re-deriver deletion).
|
||||
|
||||
mir.5 carries the ledger work as part of the milestone (per CLAUDE.md:
|
||||
contract changes ship with the iteration that needs them):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user