Files
AILang/crates/ailang-mir/src/lib.rs
T
Brummel a6fd93adba feat(codegen): pre-resolve the App callee in MIR; delete is_static_callee + type_home_module (mir.2)
Second re-deriver removal of the typed-MIR milestone (spec
docs/specs/0060-typed-mir.md, plan docs/plans/0116-mir.2-callee-static.md).
Static-callee resolution moves out of codegen and into lower_to_mir:
every Term::App callee is classified once, mirroring check's own synth
Term::Var ladder (lib.rs:3409-3582), and emitted as a resolved Callee.
Codegen reads the callee identity off MIR and routes each kind to the
right lowering. The codegen re-derivers is_static_callee and
type_home_module are deleted, and the lower_app <-> is_static_callee
lockstep pair is struck from CLAUDE.md.

Callee reshape (beyond the spec's original {module, fn_name} sketch —
spec 0060's Concrete-code-shapes block is updated in this iteration):

  pub enum Callee {
      Static  { module, fn_name, sig: Type },  // user/prelude fn -> emit_call, module pre-resolved
      Builtin { name, sig: Type },             // operator/intrinsic -> inline opcode lowering
      Indirect(Box<MTerm>),                    // dynamic: fn-pointer / closure / shadowed name
  }

Two forced refinements, both settled in planning:

- sig:Type on each resolved variant. drop::synth_callee_ret_mode reads
  the callee ret_mode, today off Callee::Indirect(inner).ty(). A
  resolved callee has no sub-term, so the sig (= synth_pure(callee),
  mode-preserving) is the lossless replacement. This is NOT a mir.3
  pull-forward: the MArg param-mode / consume_count fields stay at their
  mir.1 defaults. Without it, every statically-resolved call would lose
  its drop signal and the RawBuf / int_to_str RC-stats pins mir.1b fixed
  would regress.

- Builtin variant. Operators and the str-num builtins (+, not,
  str_concat, ...) are lowered inline by name and have no module/fn_name;
  a separate variant is cleaner than a sentinel module. The classifier
  decides Builtin vs Static from CHECK'S OWN resolution (a name in
  env.globals with no owning module is a builtin), never a copy of
  codegen's old is_static_callee allowlist. The recon's divergence audit
  confirmed check's builtin set and codegen's inline-arm set match
  exactly, so the single-engine rule is mechanically satisfiable with no
  env threading gap.

type_home_module is fully deletable: a workspace-wide grep confirmed no
program references a type-scoped T.fn as a value, so its only other
consumer (resolve_top_level_fn) collapses to the module-name fallback
with no behaviour change. (The spec's "x3 mirrors" wording over-counted;
the live surface was the definition plus two call sites.)

Alternatives rejected during planning: sentinel module in Static
(ugly); builtins left as Indirect (breaks — no fn-pointer for an
operator); name-rewrite Static{name,sig} keeping lower_app's by-name
dispatch (blurs builtin/user and still needs sig). The mir.1b
re-synth-strictness trap (post-mono synth_pure re-unify surfacing
mode-strip / bare-vs-qualified / metavar leaks) did NOT fire under the
new producer path — qualify_local_types mode-preservation and the $u
wildcard normalisation from mir.1b held.

Verification (orchestrator, post-implement inspect): diff matches the
plan; grep-clean for is_static_callee + type_home_module across
ailang-codegen and ail; cargo build --workspace green; cargo test
--workspace 700 passed / 0 failed / 3 ignored (no #[ignore] added this
iteration — the 3 are the pre-existing #49 Str-leg pin + two doctests).
Acceptance witnesses green: #53 (mono_new_over_user_adt_builds_and_runs),
#51 (rawbuf_new_int_size_only_builds_and_runs still builds), show_print
cross-module (print_user_adt_runs_end_to_end), the new mir.2 pins
(mir2_resolved_callee_kinds_run_end_to_end, callee_classification_*,
strengthened new_over_user_adt_carries_node_types asserting Callee::Static).
#49 heap-Str loop binder stays #[ignore] (lifts at mir.4).

Two new examples/ fixtures back the new pins (classify_pin.ail for the
producer classification pin, mir2_callee_kinds.ail for the E2E),
ail-parse / round-trip clean.

Forward note (cycle-close architect): crates/ail/tests/codegen_import_map_fallback_pin.rs:14-15
doc prose names lower_app's cross-module arm and synth_with_extras as
failure-mode targets — both now deleted (mir.2 / mir.1b). The pin's
protected invariant is still valid and green; the prose is stale and
wants a doc-honesty reword, left out of this iteration's footprint.
2026-05-31 19:27:56 +02:00

193 lines
6.7 KiB
Rust

//! Typed mid-level IR — the single artefact that crosses the
//! `check` → `codegen` boundary. Produced by
//! `ailang_check::lower_to_mir` from a monomorphised, typechecked
//! `Workspace`; consumed by `ailang_codegen`. Every node carries the
//! facts codegen used to re-derive: its type (`ty`), the resolved
//! callee (`Callee`, filled mir.2), parameter modes / consume counts
//! (`Mode` / `consume_count`, filled mir.3), and string representation
//! (`StrRep`, filled mir.4). MIR is never authored, never hashed,
//! never round-tripped — it is a compiler-internal derived form.
use ailang_core::ast::{Literal, Module, Pattern, Type};
use std::collections::BTreeMap;
/// A whole monomorphised program, lowered to MIR.
#[derive(Debug, Clone)]
pub struct MirWorkspace {
pub entry: String,
pub modules: BTreeMap<String, MirModule>,
}
#[derive(Debug, Clone)]
pub struct MirModule {
pub name: String,
/// The post-mono AST module. codegen reads structural data
/// (Def::Type / Def::Const / ctor layouts / intrinsic-fn markers /
/// imports) from here; the typed fn bodies come from `defs`. This
/// keeps `MirWorkspace` the single artefact codegen consumes —
/// structural reads were never a re-derivation, so carrying the AST
/// alongside the typed bodies does not reintroduce one.
pub ast: Module,
pub defs: Vec<MirDef>,
/// Typed bodies of the module's non-literal `Def::Const`s. A const
/// is inlined at every `Var` reference site (codegen reads the
/// body from here, keyed by name); literal consts emit a global
/// instead and are not carried. Same role as `defs` for fns — the
/// body is the checker-typed `MTerm` so codegen re-derives no
/// type to lower it.
pub consts: Vec<MirConst>,
}
/// One lowered non-literal const. `body` is the typed `MTerm` codegen
/// inlines at each reference site.
#[derive(Debug, Clone)]
pub struct MirConst {
pub name: String,
pub ty: Type,
pub body: MTerm,
}
/// One lowered top-level fn. `sig` is the declared signature; `body`
/// is the lowered fn body with `ty` on every node.
#[derive(Debug, Clone)]
pub struct MirDef {
pub name: String,
pub sig: Type,
pub params: Vec<String>,
pub body: MTerm,
}
/// One MIR node — structural counterpart of `ast::Term`, plus the
/// `Str` split. `ty` is the checker-proved type at this node.
#[derive(Debug, Clone)]
pub enum MTerm {
Lit { lit: Literal, ty: Type },
/// String literal — the `Str`-representation carrier. `rep` is
/// `Static` for every literal at mir.1; mir.4 flips loop-carried
/// seeds to `Heap`. Split out of `Lit` so the rep annotation has a
/// home (spec §"Str split").
Str { lit: String, rep: StrRep },
Var { name: String, ty: Type },
/// `callee` is `Indirect(<lowered callee>)` at mir.1; mir.2
/// resolves static targets to `Callee::Static`.
App { callee: Callee, args: Vec<MArg>, tail: bool, ty: Type },
Let { name: String, mode: Mode, init: Box<MTerm>, body: Box<MTerm>, ty: Type },
LetRec {
name: String,
sig: Type,
params: Vec<String>,
body: Box<MTerm>,
in_term: Box<MTerm>,
ty: Type,
},
If { cond: Box<MTerm>, then: Box<MTerm>, else_: Box<MTerm>, ty: Type },
Do { op: String, args: Vec<MArg>, tail: bool, ty: Type },
Ctor { type_name: String, ctor: String, args: Vec<MArg>, ty: Type },
Match { scrutinee: Box<MTerm>, arms: Vec<MArm>, ty: Type },
Lam {
params: Vec<String>,
param_tys: Vec<Type>,
ret_ty: Type,
effects: Vec<String>,
body: Box<MTerm>,
ty: Type,
},
Seq { lhs: Box<MTerm>, rhs: Box<MTerm>, ty: Type },
Clone { value: Box<MTerm>, ty: Type },
ReuseAs { source: Box<MTerm>, body: Box<MTerm>, ty: Type },
Loop { binders: Vec<MLoopBinder>, body: Box<MTerm>, ty: Type },
Recur { args: Vec<MArg>, ty: Type },
/// `elem` is `None` at mir.1; mir.5 carries the element type.
New { type_name: String, elem: Option<Type>, args: Vec<MNewArg>, ty: Type },
Intrinsic { ty: Type },
}
/// A match arm. Patterns stay structural (`ast::Pattern`) — they carry
/// no type annotation the boundary re-derives.
#[derive(Debug, Clone)]
pub struct MArm {
pub pat: Pattern,
pub body: MTerm,
}
#[derive(Debug, Clone)]
pub struct MLoopBinder {
pub name: String,
pub ty: Type,
pub init: MTerm,
}
#[derive(Debug, Clone)]
pub enum MNewArg {
Type(Type),
Value(MTerm),
}
/// A call/ctor/recur argument with its mode and consume count. Both
/// fields are mir.1 defaults (`Owned` / `1`); mir.3 fills them.
#[derive(Debug, Clone)]
pub struct MArg {
pub term: MTerm,
pub mode: Mode,
pub consume_count: u32,
}
/// A resolved App callee. `Static`/`Builtin` are filled by mir.2's
/// `lower_to_mir::classify_callee`, which mirrors check's `synth`
/// `Term::Var` ladder; `Indirect` is a genuinely dynamic callee
/// (fn-typed local/param, closure value, or a name shadowed by a
/// local). `sig` is the callee's fn-type — the lossless replacement
/// for the sub-term's `ty()` that codegen's drop path reads for the
/// callee `ret_mode`.
#[derive(Debug, Clone)]
pub enum Callee {
/// A statically-resolved user/prelude fn: codegen routes it to
/// `emit_call(module, fn_name, …)` with `module` pre-resolved
/// (replacing codegen's `type_home_module` ladder).
Static { module: String, fn_name: String, sig: Type },
/// An operator / intrinsic builtin (`+`, `not`, `str_concat`, …):
/// codegen lowers it inline by `name` (opcode selection), never
/// via `emit_call`.
Builtin { name: String, sig: Type },
/// A dynamic callee — lower the boxed term to a fn-pointer.
Indirect(Box<MTerm>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StrRep {
Heap,
Static,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Owned,
Borrow,
}
impl MTerm {
/// The checker-proved type at this node. `Str` is always `Str`.
pub fn ty(&self) -> Type {
match self {
MTerm::Lit { ty, .. }
| MTerm::Var { ty, .. }
| MTerm::App { ty, .. }
| MTerm::Let { ty, .. }
| MTerm::LetRec { ty, .. }
| MTerm::If { ty, .. }
| MTerm::Do { ty, .. }
| MTerm::Ctor { ty, .. }
| MTerm::Match { ty, .. }
| MTerm::Lam { ty, .. }
| MTerm::Seq { ty, .. }
| MTerm::Clone { ty, .. }
| MTerm::ReuseAs { ty, .. }
| MTerm::Loop { ty, .. }
| MTerm::Recur { ty, .. }
| MTerm::New { ty, .. }
| MTerm::Intrinsic { ty } => ty.clone(),
MTerm::Str { .. } => Type::str_(),
}
}
}