feat(codegen): fill MArg.mode for App args, read the anon-temp drop gate off it, delete module_def_ail_types (mir.3b)

Second of two iterations delivering spec 0060's mir.3 row (plan
docs/plans/0118-mir.3b-marg-mode-and-table-delete.md). lower_to_mir's
Term::App arm fills each MArg.mode from the resolved callee's
param_modes (the sig = synth_pure(callee) it already holds); codegen's
emit_call anon-temp borrow-slot drop gate reads arg.mode instead of
re-looking-up the callee's param_modes from the module_def_ail_types
table; and — since that gate was the table's only reader — the
module_def_ail_types field plus all its construction and threading are
deleted.

Two refinements to the spec's mir.3b sketch, settled in planning from a
focused recon and recorded in spec 0060:

1. module_def_ail_types is deleted in mir.3b, not mir.5. A grep proved
   self.module_def_ail_types had exactly ONE reader (the anon-temp
   gate); the own-param drop reads param_modes from the def's own f.ty,
   not this table. Once the gate moves onto MArg.mode the table is dead,
   so it is removed now rather than carried as dead code to mir.5. The
   mir.5 row's "last re-derivation residue" shrinks to the
   element-type / Term::New (New.elem) work.

2. MArg.mode is filled for App args only; MTerm::Let.mode is not filled.
   Only App args have a real per-arg mode source (the callee
   Type::Fn.param_modes). Do args (EffectOpSig has no param modes), Ctor
   args (no per-field mode), and Recur args (loop binders carry no mode)
   stay Mode::Owned. Let.mode has no source (ast::Term::Let has no mode
   field) and no consumer (the let-drop gate reads consume, not
   Let.mode), so it stays Owned — filling it would invent a value
   nobody reads.

Safety property held: MArg.mode is filled from the same callee fn-type
the gate read from the table, so the drop fires identically. For
(app RawBuf.get (app RawBuf.set …) 0), RawBuf.get's param 0 is borrow,
so args[0].mode = Borrow — exactly the value module_def_ail_types
yielded. Confirmed by the anon-temp witness staying green.

ParamMode -> Mode conversion: Borrow -> Mode::Borrow; Own and Implicit
(the Implicit ≡ Own contract) -> Mode::Owned. The own-param drop keeps
reading ParamMode off f.ty (Type/ParamMode imports retained, live
readers at lib.rs:1356/1507).

Also retires three now-stale doc comments that named the deleted
module_def_ail_types field (ailang-check/src/lib.rs, uniqueness.rs, and
the codegen_import_map_fallback_pin test doc) — a direct consequence of
the deletion, reworded to the current architecture.

Verification (orchestrator, post-implement inspect): diff matches the
plan (App-arg m_args built before m_callee consumes sig — the benign
borrow-order deviation the plan's note flagged); module_def_ail_types
grep-clean across all of crates/; cargo build --workspace clean (no
unused/dead_code); cargo test --workspace 702 passed / 0 failed / 3
ignored (+1 = the new producer pin app_arg_carries_callee_borrow_mode;
no #[ignore] added). The anon-temp drop-correctness witness
raw_buf_owned_drop_balances_rc_stats stays live=0, now driven by
MArg.mode; the other RC-stats leak pins green; #49 stays #[ignore]
(mir.4); #51/#53 build guards green.

mir.3 is now complete (3a relocated consume_count + deleted the second
uniqueness run; 3b filled the mode annotation + deleted
module_def_ail_types). Remaining: mir.4 (#49 StrRep RED->GREEN),
mir.5 (element-type/Term::New + ledger).
This commit is contained in:
2026-05-31 20:10:25 +02:00
parent 0bd7f47fad
commit eba1be8d9d
8 changed files with 134 additions and 45 deletions
@@ -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)."
}
@@ -4,7 +4,7 @@
//! //!
//! Property protected: post-mono synthesised body cross-module //! Property protected: post-mono synthesised body cross-module
//! references resolve at codegen via the fallback to //! 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 //! NOT in the current module's `import_map`. Specifically, the
//! synthesised `prelude.print__<UserType>` body references //! synthesised `prelude.print__<UserType>` body references
//! `<user_module>.show__<UserType>` even though `prelude` does not //! `<user_module>.show__<UserType>` even though `prelude` does not
+1 -2
View File
@@ -1346,8 +1346,7 @@ pub fn elaborate_workspace(
// Workspace-flat `module -> def -> Type` table — the shape // Workspace-flat `module -> def -> Type` table — the shape
// `uniqueness::infer_module_with_cross` consumes to resolve a // `uniqueness::infer_module_with_cross` consumes to resolve a
// cross-module callee's `param_modes` (so a Borrow-mode arg is // cross-module callee's `param_modes` (so a Borrow-mode arg is
// walked as a borrow, not a consume). Identical in content to // walked as a borrow, not a consume). Built here from the post-mono
// codegen's `module_def_ail_types`; built here from the post-mono
// workspace so the uniqueness pass runs in the producer (mir.3a). // workspace so the uniqueness pass runs in the producer (mir.3a).
let mut cross_module_types: std::collections::BTreeMap< let mut cross_module_types: std::collections::BTreeMap<
String, String,
+32 -4
View File
@@ -106,6 +106,22 @@ fn arg(term: MTerm) -> MArg {
MArg { term, mode: Mode::Owned, consume_count: 1 } 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 /// The resolved identity of an App callee, mirroring synth's
/// `Term::Var` resolution ladder (`lib.rs:3409-3582`). `lower_term` /// `Term::Var` resolution ladder (`lib.rs:3409-3582`). `lower_term`
/// turns this into the matching `Callee` variant. Resolution uses /// turns this into the matching `Callee` variant. Resolution uses
@@ -194,6 +210,22 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
Term::App { callee, args, tail } => { Term::App { callee, args, tail } => {
let class = classify_callee(ctx, callee); let class = classify_callee(ctx, callee);
let sig = ctx.synth_pure(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::<Result<Vec<_>>>()?;
let m_callee = match class { let m_callee = match class {
CalleeClass::Builtin { name } => Callee::Builtin { name, sig }, CalleeClass::Builtin { name } => Callee::Builtin { name, sig },
CalleeClass::Static { module, fn_name } => { CalleeClass::Static { module, fn_name } => {
@@ -203,10 +235,6 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
Callee::Indirect(Box::new(lower_term(ctx, callee)?)) Callee::Indirect(Box::new(lower_term(ctx, callee)?))
} }
}; };
let m_args = args
.iter()
.map(|a| Ok(arg(lower_term(ctx, a)?)))
.collect::<Result<Vec<_>>>()?;
MTerm::App { callee: m_callee, args: m_args, tail: *tail, ty } MTerm::App { callee: m_callee, args: m_args, tail: *tail, ty }
} }
+2 -2
View File
@@ -111,8 +111,8 @@ pub fn infer_module(m: &Module) -> UniquenessTable {
} }
/// Same as [`infer_module`], but with access to the workspace-flat /// Same as [`infer_module`], but with access to the workspace-flat
/// cross-module signature table (`module -> def -> Type`, the shape /// cross-module signature table (`module -> def -> Type`, built by the
/// codegen builds as `module_def_ail_types`). The per-module pass /// caller — `lower_to_mir` from the post-mono workspace). The per-module pass
/// only registers same-module + builtin signatures in `globals`; a /// only registers same-module + builtin signatures in `globals`; a
/// call to a *cross-module* callee (a qualified `<mod>.<def>` name, /// call to a *cross-module* callee (a qualified `<mod>.<def>` name,
/// e.g. the monomorphised intrinsic `raw_buf.RawBuf_get__Int` reached /// e.g. the monomorphised intrinsic `raw_buf.RawBuf_get__Int` reached
+49 -1
View File
@@ -6,7 +6,7 @@
//! pins protect the producer; codegen consumption arrives in mir.1b. //! pins protect the producer; codegen consumption arrives in mir.1b.
use ailang_check::elaborate_workspace; 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 ailang_surface::load_workspace;
use std::path::{Path, PathBuf}; 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 <set-result> 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 /// Recursively search for a `MTerm::Str { rep: Static }` reachable
/// from a loop binder init (the seed). /// from a loop binder init (the seed).
fn find_static_str_seed(t: &MTerm) -> bool { fn find_static_str_seed(t: &MTerm) -> bool {
+18 -35
View File
@@ -36,7 +36,7 @@
use ailang_core::ast::*; use ailang_core::ast::*;
use ailang_core::Workspace; 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}; use std::collections::{BTreeMap, BTreeSet};
mod drop; 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 // one per concrete instantiation); any residual `Type::Forall` ty
// is a stale source def kept for round-trip / `ail diff` purposes // is a stale source def kept for round-trip / `ail diff` purposes
// and is intentionally not lowered. // 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<String, BTreeMap<String, FnSig>> = BTreeMap::new(); let mut module_user_fns: BTreeMap<String, BTreeMap<String, FnSig>> = BTreeMap::new();
let mut module_def_ail_types: BTreeMap<String, BTreeMap<String, Type>> = BTreeMap::new();
// cross-module ctor table. Maps module name → ctor name → // cross-module ctor table. Maps module name → ctor name →
// CtorRef (with `type_name` *unqualified*, since the ctor is defined // CtorRef (with `type_name` *unqualified*, since the ctor is defined
// in that module). Cross-module ctor lookups resolve through this // 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); module_mir_consts.insert(mname.clone(), mir_const_bodies);
let mut user_fns = BTreeMap::new(); let mut user_fns = BTreeMap::new();
let mut ail_types = BTreeMap::new();
let mut ctors = BTreeMap::new(); let mut ctors = BTreeMap::new();
for def in &m.defs { for def in &m.defs {
if let Def::Fn(f) = def { if let Def::Fn(f) = def {
ail_types.insert(f.name.clone(), f.ty.clone());
if let Type::Fn { params, ret, .. } = &f.ty { if let Type::Fn { params, ret, .. } = &f.ty {
let psig: Result<Vec<String>> = params.iter().map(llvm_type).collect(); let psig: Result<Vec<String>> = params.iter().map(llvm_type).collect();
let rsig = llvm_type(ret); 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_user_fns.insert(mname.clone(), user_fns);
module_def_ail_types.insert(mname.clone(), ail_types);
module_ctor_index.insert(mname.clone(), ctors); module_ctor_index.insert(mname.clone(), ctors);
module_consts.insert(mname.clone(), consts); module_consts.insert(mname.clone(), consts);
} }
@@ -436,7 +429,6 @@ fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Targe
&module_mir_consts, &module_mir_consts,
mname, mname,
&module_user_fns, &module_user_fns,
&module_def_ail_types,
&module_ctor_index, &module_ctor_index,
&module_consts, &module_consts,
import_map, import_map,
@@ -776,10 +768,6 @@ struct Emitter<'a> {
str_counter: u64, str_counter: u64,
/// Top-level functions per module of the workspace, for call resolution. /// Top-level functions per module of the workspace, for call resolution.
module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>, module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>,
/// 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<String, BTreeMap<String, Type>>,
/// Import map of the current module (alias/module name → actual module name). /// Import map of the current module (alias/module name → actual module name).
import_map: BTreeMap<String, String>, import_map: BTreeMap<String, String>,
/// ADT table: type_name -> list of ctors in definition order. /// ADT table: type_name -> list of ctors in definition order.
@@ -982,7 +970,6 @@ impl<'a> Emitter<'a> {
module_mir_consts: &'a BTreeMap<String, BTreeMap<String, MTerm>>, module_mir_consts: &'a BTreeMap<String, BTreeMap<String, MTerm>>,
module_name: &'a str, module_name: &'a str,
module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>, module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>,
module_def_ail_types: &'a BTreeMap<String, BTreeMap<String, Type>>,
module_ctor_index: &'a BTreeMap<String, BTreeMap<String, CtorRef>>, module_ctor_index: &'a BTreeMap<String, BTreeMap<String, CtorRef>>,
module_consts: &'a BTreeMap<String, BTreeMap<String, ConstDef>>, module_consts: &'a BTreeMap<String, BTreeMap<String, ConstDef>>,
import_map: BTreeMap<String, String>, import_map: BTreeMap<String, String>,
@@ -1042,7 +1029,6 @@ impl<'a> Emitter<'a> {
counter: 0, counter: 0,
str_counter: 0, str_counter: 0,
module_user_fns, module_user_fns,
module_def_ail_types,
import_map, import_map,
types, types,
module_ctor_index, module_ctor_index,
@@ -2697,27 +2683,24 @@ impl<'a> Emitter<'a> {
// result SSA), so this can never dec a value that flows out // result SSA), so this can never dec a value that flows out
// as the surrounding fn's result. // as the surrounding fn's result.
if !tail { if !tail {
if let Some(Type::Fn { param_modes, .. }) = self // mir.3b: the per-arg slot mode rides on the MIR arg
.module_def_ail_types // (`MArg.mode`, filled by lower_to_mir from the callee's
.get(target_module) // `param_modes`), so the borrow-slot test reads it directly
.and_then(|m| m.get(target_def)) // — no re-lookup of the callee's signature from a codegen
.cloned() // sig table. `Borrow` slots borrow the arg, so an Own-ret
{ // heap temp landing in one is dropped here; `Own`/`Implicit`
for (i, (arg, (arg_ssa, arg_ty))) in // slots consume the arg (the callee dec's it).
args.iter().zip(compiled_args.iter()).enumerate() 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 = let drop_sym = self.drop_symbol_for_binder(&arg.term, arg_ssa);
matches!(param_modes.get(i), Some(ParamMode::Borrow)); self.body.push_str(&format!(
if is_borrow_slot " call void @{drop_sym}(ptr {arg_ssa})\n"
&& 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"
));
}
} }
} }
} }
+13
View File
@@ -339,6 +339,19 @@ class and deletes the matching codegen re-derivation.
> `emit_call`'s anon-temp borrow-slot gate onto `MArg.mode` (additive; > `emit_call`'s anon-temp borrow-slot gate onto `MArg.mode` (additive;
> no further re-deriver deletion). > 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: mir.5 carries the ledger work as part of the milestone (per CLAUDE.md:
contract changes ship with the iteration that needs them): contract changes ship with the iteration that needs them):