diff --git a/bench/orchestrator-stats/2026-05-31-iter-mir.3b.json b/bench/orchestrator-stats/2026-05-31-iter-mir.3b.json new file mode 100644 index 0000000..d9748de --- /dev/null +++ b/bench/orchestrator-stats/2026-05-31-iter-mir.3b.json @@ -0,0 +1,18 @@ +{ + "iter_id": "mir.3b", + "date": "2026-05-31", + "mode": "standard", + "outcome": "DONE", + "tasks_total": 4, + "tasks_completed": 4, + "reloops_per_task": { + "1": 0, + "2": 0, + "3": 1, + "4": 0 + }, + "review_loops_spec": 0, + "review_loops_quality": 0, + "blocked_reason": null, + "notes": "Task 1 carried a plan-vs-reality deviation (DONE_WITH_CONCERNS): the plan's Step-2 code block placed the `m_args` construction AFTER `m_callee`, with an implementer-note to 'confirm sig is in scope'. In the actual mir.2 code `sig` is moved into the resolved `Callee` variant (`Callee::Static{..,sig}` / `Builtin{..,sig}`), so `param_mode_at(&sig, i)` cannot run after the move. The minimal faithful fix was to build `m_args` BEFORE the `m_callee` match (so `&sig` is still valid), preserving the plan's body verbatim. Task 3 needed one navigation re-loop: the producer-pin's `find_app_with_fn` first searched for fn-name stem `get`/`get__*`, but the mono pass mangles `RawBuf.get` to `RawBuf_get__Int` (module `raw_buf`); the navigator was retargeted to stem `RawBuf_get` (matching `RawBuf_get__*`). Full workspace suite: 702 passed / 0 failed / 3 ignored (mir.3a was 701/0/3; +1 = the new app_arg_carries_callee_borrow_mode producer pin). Acceptance witnesses green: raw_buf_owned_drop_balances_rc_stats (anon-temp gate, live==0), raw_buf_loop_no_leak_pin, loop_recur_heap_binder_no_leak_pin, print_no_leak_pin; #49 loop_recur_str_binder stays #[ignore]; #51/#53 build guards green. module_def_ail_types fully deleted, grep-clean, zero unused-import warnings (Type/ParamMode retain live readers at the own-param drop)." +} diff --git a/crates/ail/tests/codegen_import_map_fallback_pin.rs b/crates/ail/tests/codegen_import_map_fallback_pin.rs index 525fe6f..fc01a10 100644 --- a/crates/ail/tests/codegen_import_map_fallback_pin.rs +++ b/crates/ail/tests/codegen_import_map_fallback_pin.rs @@ -4,7 +4,7 @@ //! //! Property protected: post-mono synthesised body cross-module //! references resolve at codegen via the fallback to -//! `module_user_fns` / `module_def_ail_types` when the prefix is +//! `module_user_fns` when the prefix is //! NOT in the current module's `import_map`. Specifically, the //! synthesised `prelude.print__` body references //! `.show__` even though `prelude` does not diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 97f050b..e4e6112 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1346,8 +1346,7 @@ pub fn elaborate_workspace( // 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 + // walked as a borrow, not a consume). 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, diff --git a/crates/ailang-check/src/lower_to_mir.rs b/crates/ailang-check/src/lower_to_mir.rs index d4a8f4c..003afaa 100644 --- a/crates/ailang-check/src/lower_to_mir.rs +++ b/crates/ailang-check/src/lower_to_mir.rs @@ -106,6 +106,22 @@ fn arg(term: MTerm) -> MArg { MArg { term, mode: Mode::Owned, consume_count: 1 } } +/// The MIR `Mode` of a callee's parameter at position `i`, read off +/// the callee's `Type::Fn.param_modes`. `Borrow` maps to +/// `Mode::Borrow`; `Own` and `Implicit` (the `Implicit ≡ Own` +/// contract, `ast.rs` ParamMode) both map to `Mode::Owned`. Out of +/// range / non-fn → `Owned` (the consume default). Used by the App arm +/// to fill `MArg.mode` so codegen reads the slot mode off MIR. +fn param_mode_at(sig: &Type, i: usize) -> Mode { + match sig { + Type::Fn { param_modes, .. } => match param_modes.get(i) { + Some(ailang_core::ast::ParamMode::Borrow) => Mode::Borrow, + _ => Mode::Owned, + }, + _ => Mode::Owned, + } +} + /// The resolved identity of an App callee, mirroring synth's /// `Term::Var` resolution ladder (`lib.rs:3409-3582`). `lower_term` /// turns this into the matching `Callee` variant. Resolution uses @@ -194,6 +210,22 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result { Term::App { callee, args, tail } => { let class = classify_callee(ctx, callee); let sig = ctx.synth_pure(callee)?; + // mir.3b: each App arg carries the callee's slot mode, read + // off the resolved callee fn-type. codegen's anon-temp drop + // gate reads this instead of re-looking-up the callee's + // param_modes from a sig table. Built before `m_callee` + // consumes `sig` into the resolved `Callee` variant. + let m_args = args + .iter() + .enumerate() + .map(|(i, a)| { + Ok(MArg { + term: lower_term(ctx, a)?, + mode: param_mode_at(&sig, i), + consume_count: 1, + }) + }) + .collect::>>()?; let m_callee = match class { CalleeClass::Builtin { name } => Callee::Builtin { name, sig }, CalleeClass::Static { module, fn_name } => { @@ -203,10 +235,6 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result { Callee::Indirect(Box::new(lower_term(ctx, callee)?)) } }; - let m_args = args - .iter() - .map(|a| Ok(arg(lower_term(ctx, a)?))) - .collect::>>()?; MTerm::App { callee: m_callee, args: m_args, tail: *tail, ty } } diff --git a/crates/ailang-check/src/uniqueness.rs b/crates/ailang-check/src/uniqueness.rs index 6ebf438..7d113dd 100644 --- a/crates/ailang-check/src/uniqueness.rs +++ b/crates/ailang-check/src/uniqueness.rs @@ -111,8 +111,8 @@ pub fn infer_module(m: &Module) -> UniquenessTable { } /// Same as [`infer_module`], but with access to the workspace-flat -/// cross-module signature table (`module -> def -> Type`, the shape -/// codegen builds as `module_def_ail_types`). The per-module pass +/// cross-module signature table (`module -> def -> Type`, built by the +/// caller — `lower_to_mir` from the post-mono workspace). The per-module pass /// only registers same-module + builtin signatures in `globals`; a /// call to a *cross-module* callee (a qualified `.` name, /// e.g. the monomorphised intrinsic `raw_buf.RawBuf_get__Int` reached diff --git a/crates/ailang-check/tests/lower_to_mir_ty.rs b/crates/ailang-check/tests/lower_to_mir_ty.rs index d091531..5bc9f53 100644 --- a/crates/ailang-check/tests/lower_to_mir_ty.rs +++ b/crates/ailang-check/tests/lower_to_mir_ty.rs @@ -6,7 +6,7 @@ //! pins protect the producer; codegen consumption arrives in mir.1b. use ailang_check::elaborate_workspace; -use ailang_mir::{Callee, MTerm, MirWorkspace, StrRep}; +use ailang_mir::{Callee, MTerm, MirWorkspace, Mode, StrRep}; use ailang_surface::load_workspace; use std::path::{Path, PathBuf}; @@ -166,6 +166,54 @@ fn mirdef_consume_is_populated_for_let_binder() { ); } +#[test] +fn app_arg_carries_callee_borrow_mode() { + // mir.3b: lower_to_mir fills MArg.mode from the callee's param_modes. + // In `(app RawBuf.get 0)`, RawBuf.get's param 0 is a + // borrow receiver, so the outer App's arg 0 must be Mode::Borrow — + // the value codegen's anon-temp drop gate now reads off MIR. + let mir = elaborate_fixture("raw_buf_drop_min"); + // main is `(app print (let buf (new RawBuf …) + // (app RawBuf.get (app RawBuf.set …) 0)))`, so the RawBuf.get App + // is nested under the print App's arg and the let body. Mono mangles + // `RawBuf.get` to `RawBuf_get__Int`. + let outer = find_app_with_fn(body(&mir, "raw_buf_drop_min", "main"), "RawBuf_get") + .expect("RawBuf.get App node"); + let MTerm::App { args, .. } = outer else { + unreachable!("find_app_with_fn returns an App"); + }; + assert!( + matches!(args[0].mode, Mode::Borrow), + "RawBuf.get arg 0 (borrow receiver) must be Mode::Borrow, got {:?}", + args[0].mode, + ); +} + +/// First `MTerm::App` whose resolved callee (`Static`/`Builtin`) names a +/// fn equal to (or a monomorphised specialisation of) `fn_name`, +/// searching App args and Let init/body. The fixture's `RawBuf.get` +/// callee may be mangled by mono (e.g. `get__Int`), so the match is on +/// the fn-name stem. +fn find_app_with_fn<'a>(t: &'a MTerm, fn_name: &str) -> Option<&'a MTerm> { + let names = match t { + MTerm::App { callee: Callee::Static { fn_name: n, .. }, .. } => Some(n.as_str()), + MTerm::App { callee: Callee::Builtin { name: n, .. }, .. } => Some(n.as_str()), + _ => None, + }; + if let Some(n) = names { + if n == fn_name || n.starts_with(&format!("{fn_name}__")) { + return Some(t); + } + } + match t { + MTerm::App { args, .. } => args.iter().find_map(|a| find_app_with_fn(&a.term, fn_name)), + MTerm::Let { init, body, .. } => { + find_app_with_fn(init, fn_name).or_else(|| find_app_with_fn(body, fn_name)) + } + _ => None, + } +} + /// 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 d0c334f..df67287 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -36,7 +36,7 @@ use ailang_core::ast::*; use ailang_core::Workspace; -use ailang_mir::{Callee, MArg, MTerm, MirDef, MirWorkspace}; +use ailang_mir::{Callee, MArg, MTerm, MirDef, MirWorkspace, Mode}; use std::collections::{BTreeMap, BTreeSet}; mod drop; @@ -325,11 +325,7 @@ fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Targe // one per concrete instantiation); any residual `Type::Forall` ty // is a stale source def kept for round-trip / `ail diff` purposes // and is intentionally not lowered. - // - `module_def_ail_types`: AILang `Type` for every fn-typed def. - // Retained for the call-site owned-temp drop gate in `emit_call` - // (reads a callee's `param_modes`) and for the mir.3 mode work. let mut module_user_fns: BTreeMap> = BTreeMap::new(); - let mut module_def_ail_types: BTreeMap> = BTreeMap::new(); // cross-module ctor table. Maps module name → ctor name → // CtorRef (with `type_name` *unqualified*, since the ctor is defined // in that module). Cross-module ctor lookups resolve through this @@ -353,11 +349,9 @@ fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Targe } module_mir_consts.insert(mname.clone(), mir_const_bodies); let mut user_fns = BTreeMap::new(); - let mut ail_types = BTreeMap::new(); let mut ctors = BTreeMap::new(); for def in &m.defs { if let Def::Fn(f) = def { - ail_types.insert(f.name.clone(), f.ty.clone()); if let Type::Fn { params, ret, .. } = &f.ty { let psig: Result> = params.iter().map(llvm_type).collect(); let rsig = llvm_type(ret); @@ -398,7 +392,6 @@ fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Targe } } module_user_fns.insert(mname.clone(), user_fns); - module_def_ail_types.insert(mname.clone(), ail_types); module_ctor_index.insert(mname.clone(), ctors); module_consts.insert(mname.clone(), consts); } @@ -436,7 +429,6 @@ fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Targe &module_mir_consts, mname, &module_user_fns, - &module_def_ail_types, &module_ctor_index, &module_consts, import_map, @@ -776,10 +768,6 @@ struct Emitter<'a> { str_counter: u64, /// Top-level functions per module of the workspace, for call resolution. module_user_fns: &'a BTreeMap>, - /// AILang types of every fn-typed top-level def, per module. Carries - /// `Forall` for polymorphic defs (used to derive substitutions at - /// monomorphic call sites). Populated in pass 1 of `lower_workspace`. - module_def_ail_types: &'a BTreeMap>, /// Import map of the current module (alias/module name → actual module name). import_map: BTreeMap, /// ADT table: type_name -> list of ctors in definition order. @@ -982,7 +970,6 @@ impl<'a> Emitter<'a> { module_mir_consts: &'a BTreeMap>, module_name: &'a str, module_user_fns: &'a BTreeMap>, - module_def_ail_types: &'a BTreeMap>, module_ctor_index: &'a BTreeMap>, module_consts: &'a BTreeMap>, import_map: BTreeMap, @@ -1042,7 +1029,6 @@ impl<'a> Emitter<'a> { counter: 0, str_counter: 0, module_user_fns, - module_def_ail_types, import_map, types, module_ctor_index, @@ -2697,27 +2683,24 @@ impl<'a> Emitter<'a> { // result SSA), so this can never dec a value that flows out // as the surrounding fn's result. if !tail { - if let Some(Type::Fn { param_modes, .. }) = self - .module_def_ail_types - .get(target_module) - .and_then(|m| m.get(target_def)) - .cloned() - { - for (i, (arg, (arg_ssa, arg_ty))) in - args.iter().zip(compiled_args.iter()).enumerate() + // mir.3b: the per-arg slot mode rides on the MIR arg + // (`MArg.mode`, filled by lower_to_mir from the callee's + // `param_modes`), so the borrow-slot test reads it directly + // — no re-lookup of the callee's signature from a codegen + // sig table. `Borrow` slots borrow the arg, so an Own-ret + // heap temp landing in one is dropped here; `Own`/`Implicit` + // slots consume the arg (the callee dec's it). + for (arg, (arg_ssa, arg_ty)) in args.iter().zip(compiled_args.iter()) { + let is_borrow_slot = matches!(arg.mode, Mode::Borrow); + if is_borrow_slot + && arg_ty == "ptr" + && arg_ssa != &dst + && self.is_rc_heap_allocated(&arg.term) { - let is_borrow_slot = - matches!(param_modes.get(i), Some(ParamMode::Borrow)); - if is_borrow_slot - && arg_ty == "ptr" - && arg_ssa != &dst - && self.is_rc_heap_allocated(&arg.term) - { - let drop_sym = self.drop_symbol_for_binder(&arg.term, arg_ssa); - self.body.push_str(&format!( - " call void @{drop_sym}(ptr {arg_ssa})\n" - )); - } + let drop_sym = self.drop_symbol_for_binder(&arg.term, arg_ssa); + self.body.push_str(&format!( + " call void @{drop_sym}(ptr {arg_ssa})\n" + )); } } } diff --git a/docs/specs/0060-typed-mir.md b/docs/specs/0060-typed-mir.md index 0e474f9..aa82ff3 100644 --- a/docs/specs/0060-typed-mir.md +++ b/docs/specs/0060-typed-mir.md @@ -339,6 +339,19 @@ class and deletes the matching codegen re-derivation. > `emit_call`'s anon-temp borrow-slot gate onto `MArg.mode` (additive; > no further re-deriver deletion). +> mir.3b refinement (discovered in planning, `docs/plans/0118-mir.3b-marg-mode-and-table-delete.md`): +> `MArg.mode` is filled for App args only — the callee's `Type::Fn.param_modes` +> is the sole real per-arg mode source; Do args (`EffectOpSig` has no +> param modes), Ctor args (no per-field mode), and Recur args (loop +> binders carry no mode) stay `Mode::Owned`. `MTerm::Let.mode` is NOT +> filled: a let-binder has no author-declared mode and no codegen +> consumer (the let-drop gate reads `consume`, not `Let.mode`), so it +> stays `Owned`. Switching `emit_call`'s anon-temp borrow-slot gate +> onto `MArg.mode` removes the only reader of codegen's +> `module_def_ail_types` table, so that table is **deleted in mir.3b**, +> not mir.5 — the mir.5 row's "last re-derivation residue" shrinks to +> the element-type / `Term::New` (`New.elem`) work. + mir.5 carries the ledger work as part of the milestone (per CLAUDE.md: contract changes ship with the iteration that needs them):