diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 9d7e2cf..97f050b 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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, + > = 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); } diff --git a/crates/ailang-check/src/lower_to_mir.rs b/crates/ailang-check/src/lower_to_mir.rs index 24b438e..d4a8f4c 100644 --- a/crates/ailang-check/src/lower_to_mir.rs +++ b/crates/ailang-check/src/lower_to_mir.rs @@ -408,7 +408,11 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result { /// 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 { +pub fn lower_module( + module: &Module, + env: &Env, + cross_module_types: &std::collections::BTreeMap>, +) -> Result { 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 { 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 { 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 }) diff --git a/crates/ailang-check/tests/lower_to_mir_ty.rs b/crates/ailang-check/tests/lower_to_mir_ty.rs index 17b4ca1..d091531 100644 --- a/crates/ailang-check/tests/lower_to_mir_ty.rs +++ b/crates/ailang-check/tests/lower_to_mir_ty.rs @@ -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 { diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 53e7717..d0c334f 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -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: 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, diff --git a/crates/ailang-codegen/src/match_lower.rs b/crates/ailang-codegen/src/match_lower.rs index b561a64..d3519f7 100644 --- a/crates/ailang-codegen/src/match_lower.rs +++ b/crates/ailang-codegen/src/match_lower.rs @@ -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; diff --git a/crates/ailang-mir/src/lib.rs b/crates/ailang-mir/src/lib.rs index 45a155d..1ddc291 100644 --- a/crates/ailang-mir/src/lib.rs +++ b/crates/ailang-mir/src/lib.rs @@ -55,6 +55,14 @@ pub struct MirDef { pub sig: Type, pub params: Vec, 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, } /// One MIR node — structural counterpart of `ast::Term`, plus the diff --git a/docs/specs/0060-typed-mir.md b/docs/specs/0060-typed-mir.md index ec8354f..0e474f9 100644 --- a/docs/specs/0060-typed-mir.md +++ b/docs/specs/0060-typed-mir.md @@ -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` +> (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):