feat(check): mir.1a — typed-MIR types + post-mono lower_to_mir + elaborate_workspace (additive)

First half of spec iteration mir.1 (docs/specs/0060-typed-mir.md,
plan docs/plans/0114-mir.1a-mir-infrastructure.md). Purely additive: a
new leaf crate plus a post-mono lowering walk plus the front-end entry.
Nothing consumes MIR yet — codegen and the CLI are untouched — so the
whole existing suite stays green (102 test-result-ok lines, 0 failed;
the one ignored is the #49 pin, which lifts at mir.4). mir.1b flips
codegen to consume MIR and deletes the synth_with_extras mirror.

What lands:
- `ailang-mir` (new leaf crate, depends only on ailang-core): MTerm /
  MArg / MArm / MLoopBinder / MNewArg / Callee / Mode / StrRep / MirDef
  / MirModule / MirWorkspace, structurally complete over all 17 Term
  variants plus the dedicated Str split (MTerm::Str{lit,rep}). Every
  later-iteration annotation field exists now at a mir.1 default
  (App.callee = Indirect, MArg.mode/consume_count = Owned/1, Str.rep =
  Static, New.elem = None); the struct shape will not change across
  mir.2–mir.5, only the values. MTerm::ty() reads the carried type.
- `ailang-check::lower_to_mir`: the post-mono walk. Carries no second
  type engine — it calls the canonical `synth` per node with fresh
  throwaway inference scaffolding (subst/counter/residuals/…), applies
  the substitution, and threads only the lexical locals/loop_stack,
  maintained by the same push/pop rules synth uses (Let, Loop, Lam,
  LetRec, and Match via synth's own type_check_pattern). This is the
  "one engine, not two" property the milestone buys, on the producer
  side.
- `ailang-check::elaborate_workspace`: the single front-end entry —
  check (diagnostics gate) → desugar+lift → monomorphise → lower_to_mir
  — transcribed from the CLI build path. check_workspace stays for the
  diagnostics-only callers.
- Three ty-fill pins (crates/ailang-check/tests/lower_to_mir_ty.rs)
  load the #51/#53/#49 witnesses as fixtures and elaborate the whole
  workspace, so lower_to_mir is exercised over the full prelude+kernel,
  not just the tiny witness. Two new witness fixtures
  (examples/new_{rawbuf_size_only,counter_user_adt}.ail); #49 reuses the
  existing loop_recur_str_binder_no_leak_pin.ail.

Deviations from the plan, all forced by ground truth and verified
against the live code before commit:
- FnDef.export is Option<String>, not bool — dropped from MirDef (the
  plan's note allowed this fallback). Codegen reads FnDef.export
  directly, unaffected.
- fn_param_types did not exist — lifted the param-type extraction into
  a shared pub(crate) fn and rewired both mono.rs sites
  (collect_mono_targets, collect_rewrite_targets) to it, so the
  param-type source cannot drift between mono and lower_to_mir. The
  helper unwraps a top-level Forall then reads Type::Fn.params —
  identical to the inner_ty extraction it replaces; the early-return
  non-fn guard is preserved at both sites. Behaviour-preserving (whole
  suite green; this is the only edit to existing mono behaviour).
- lower_module mirrors two more mono.rs scaffolding rules the plan
  under-transcribed: skip intrinsic-bodied fns (they are
  signature-only; lowering them would hit synth's Term::Intrinsic
  guard) and seed env.current_module + env.globals + env.imports per
  module so synth's Var lookup resolves post-mono specialisations
  (e.g. compare__Int) in the prelude.
- The #53 pin asserts the lowered (new Counter 42) init carries ty
  Counter, not that it is an MTerm::New: a monomorphic `new` over a
  user ADT is desugared to a dotted call `(app Counter.new 42)` before
  lower_to_mir runs (desugar.rs:1078-1098), so it lowers to MTerm::App.
  MTerm::New survives only for a polymorphic `new` carrying a written
  NewArg::Type (the #51 RawBuf path, covered by the rawbuf pin). The
  pin's intent — ty-fill correctness — is preserved; the structural
  variant is mir.2/mir.5 territory.
- ailang-mir uses workspace-inherited Cargo fields for consistency with
  the existing crates (added it to [workspace.dependencies]).

refs #49
This commit is contained in:
2026-05-31 14:04:03 +02:00
parent 438a009b83
commit 135f4ceed7
11 changed files with 712 additions and 11 deletions
+1
View File
@@ -6,6 +6,7 @@ license.workspace = true
[dependencies]
ailang-core.workspace = true
ailang-mir.workspace = true
ailang-surface.workspace = true
thiserror.workspace = true
indexmap.workspace = true
+75 -1
View File
@@ -372,6 +372,7 @@ fn unify(a: &Type, b: &Type, subst: &mut Subst) -> Result<()> {
pub mod builtins;
pub mod diagnostic;
pub mod lift;
pub mod lower_to_mir;
pub mod mono;
pub use diagnostic::{Diagnostic, Severity};
@@ -1288,6 +1289,60 @@ pub struct CheckedModule {
pub symbols: IndexMap<String, (Type, String)>,
}
/// A `Severity::Error` diagnostic for an internal compiler error in the
/// elaborate path — "check passed but a later phase (lift / mono /
/// lower_to_mir) failed". This is the mir.1a stand-in for the dedicated
/// `MirLoweringError` the typed-mir spec names (arrives in mir.1b); such
/// a failure on an already-typechecked workspace is an
/// internal-compiler-error class, surfaced as a diagnostic rather than
/// a panic.
fn internal_diag(message: String) -> diagnostic::Diagnostic {
diagnostic::Diagnostic::error("internal-compiler-error", message)
}
/// The build-path front end: typecheck (diagnostics gate) → desugar +
/// lift → monomorphise → lower to MIR. Returns a `MirWorkspace` on
/// success, or the check diagnostics on a type error. `check_workspace`
/// stays the entry for diagnostics-only callers (`ail check`); this is
/// what the build / emit-ir paths call.
pub fn elaborate_workspace(
ws: &ailang_core::workspace::Workspace,
) -> std::result::Result<ailang_mir::MirWorkspace, Vec<diagnostic::Diagnostic>> {
// 1. diagnostics gate — bail with the diagnostics on any error.
let diags = check_workspace(ws);
if diags.iter().any(|d| d.severity == diagnostic::Severity::Error) {
return Err(diags);
}
// 2. desugar + lift + monomorphise — the exact sequence the CLI
// runs at main.rs build path. A lift/mono Err on a checked
// workspace is an internal compiler error → surface as a
// diagnostic, do not panic.
let mut lifted_modules = std::collections::BTreeMap::new();
for (mname, m) in &ws.modules {
let desugared = ailang_core::desugar::desugar_module(m);
let lifted = lift_letrecs(&desugared)
.map_err(|e| vec![internal_diag(format!("lift_letrecs in `{mname}`: {e}"))])?;
lifted_modules.insert(mname.clone(), lifted);
}
let lifted = ailang_core::workspace::Workspace {
entry: ws.entry.clone(),
modules: lifted_modules,
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
let mono = monomorphise_workspace(&lifted)
.map_err(|e| vec![internal_diag(format!("monomorphise_workspace: {e}"))])?;
// 3. lower each post-mono module to MIR.
let env = build_check_env(&mono);
let mut modules = std::collections::BTreeMap::new();
for (name, m) in &mono.modules {
let mir = lower_to_mir::lower_module(m, &env)
.map_err(|e| vec![internal_diag(format!("lower_to_mir in `{name}`: {e}"))])?;
modules.insert(name.clone(), mir);
}
Ok(ailang_mir::MirWorkspace { entry: mono.entry.clone(), modules })
}
/// Single-error entry point. Typechecks `m` standalone (trivial
/// workspace), returning a [`CheckedModule`] with the symbol-to-hash
/// table on success or the **first** [`CheckError`] on failure.
@@ -4818,7 +4873,26 @@ pub(crate) fn qualify_workspace_term(
/// Checks a pattern against an expected type and returns the bindings
/// introduced by the pattern.
fn type_check_pattern(
/// Parameter types of a top-level fn signature, in declaration order.
/// Unwraps a top-level `Forall` (polymorphic def) to its `Type::Fn`
/// body, then returns the `params`. A non-fn signature (or a fn with
/// no `Type::Fn` after unwrapping) yields an empty list. This is the
/// shared source `mono.rs` reads at the `param_tys` binding sites
/// (`collect_mono_targets` / `collect_rewrite_targets`) and that
/// `lower_to_mir::lower_module` seeds per-fn `locals` from — lifted to
/// a `pub(crate)` helper so both sites agree by construction.
pub(crate) fn fn_param_types(ty: &Type) -> Vec<Type> {
let inner = match ty {
Type::Forall { body, .. } => body.as_ref(),
other => other,
};
match inner {
Type::Fn { params, .. } => params.clone(),
_ => Vec::new(),
}
}
pub(crate) fn type_check_pattern(
p: &Pattern,
expected: &Type,
env: &Env,
+335
View File
@@ -0,0 +1,335 @@
//! Post-monomorphisation lowering from the typechecked `ast::Term`
//! to typed `MTerm`. Runs after `monomorphise_workspace`. Carries no
//! second type engine: each node's type comes from a fresh-scaffolding
//! call to the canonical `crate::synth` (lib.rs:3223). The only state
//! threaded across nodes is the lexical `locals` / `loop_stack`,
//! maintained exactly as `synth` maintains it (e.g. the `Term::Let`
//! push/pop at lib.rs:3703-3715).
use ailang_core::ast::{Literal, Module, Term, Type};
use ailang_mir::{
Callee, MArg, MArm, MLoopBinder, MNewArg, MTerm, MirDef, MirModule, Mode, StrRep,
};
use indexmap::IndexMap;
use std::collections::BTreeSet;
use crate::{Env, Result};
/// Per-walk scaffolding for the canonical `synth` re-entry. `env` and
/// `in_def` are fixed per fn; `locals` / `loop_stack` are mutated as
/// the walk descends and restored on the way up, mirroring `synth`.
struct Ctx<'a> {
env: &'a Env,
in_def: &'a str,
locals: IndexMap<String, Type>,
loop_stack: Vec<Vec<(String, Type)>>,
}
impl<'a> Ctx<'a> {
/// The canonical type of `t` in the current lexical scope, with no
/// side effects on real check state. Fresh inference scaffolding
/// per call; the result has its substitution applied so downstream
/// reads see a resolved type.
fn synth_pure(&mut self, t: &Term) -> Result<Type> {
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = crate::Subst::default();
let mut counter: u32 = 0;
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
let ty = crate::synth(
t,
self.env,
&mut self.locals,
&mut self.loop_stack,
&mut effects,
self.in_def,
&mut subst,
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
)?;
Ok(subst.apply(&ty))
}
}
/// Default arg wrapper — mode/consume_count are mir.1 placeholders
/// (mir.3 fills them).
fn arg(term: MTerm) -> MArg {
MArg { term, mode: Mode::Owned, consume_count: 1 }
}
/// Lower one term to `MTerm`, filling `ty` from `synth_pure`.
fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
let ty = ctx.synth_pure(t)?;
Ok(match t {
// ---- String literal: the rep carrier (Static at mir.1) ----
Term::Lit { lit: Literal::Str { value } } => {
MTerm::Str { lit: value.clone(), rep: StrRep::Static }
}
Term::Lit { lit } => MTerm::Lit { lit: lit.clone(), ty },
Term::Var { name } => MTerm::Var { name: name.clone(), ty },
// callee is Indirect at mir.1 (mir.2 resolves Static).
Term::App { callee, args, tail } => {
let m_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 }
}
// scope-affecting: insert binder type, lower body, restore —
// exactly synth's Term::Let arm (lib.rs:3703-3715).
Term::Let { name, value, body } => {
let m_init = lower_term(ctx, value)?;
let v_ty = ctx.synth_pure(value)?;
let prev = ctx.locals.insert(name.clone(), v_ty);
let m_body = lower_term(ctx, body)?;
match prev {
Some(p) => {
ctx.locals.insert(name.clone(), p);
}
None => {
ctx.locals.shift_remove(name);
}
}
MTerm::Let {
name: name.clone(),
mode: Mode::Owned,
init: Box::new(m_init),
body: Box::new(m_body),
ty,
}
}
Term::If { cond, then, else_ } => MTerm::If {
cond: Box::new(lower_term(ctx, cond)?),
then: Box::new(lower_term(ctx, then)?),
else_: Box::new(lower_term(ctx, else_)?),
ty,
},
Term::Do { op, args, tail } => {
let m_args = args
.iter()
.map(|a| Ok(arg(lower_term(ctx, a)?)))
.collect::<Result<Vec<_>>>()?;
MTerm::Do { op: op.clone(), args: m_args, tail: *tail, ty }
}
Term::Ctor { type_name, ctor, args } => {
let m_args = args
.iter()
.map(|a| Ok(arg(lower_term(ctx, a)?)))
.collect::<Result<Vec<_>>>()?;
MTerm::Ctor {
type_name: type_name.clone(),
ctor: ctor.clone(),
args: m_args,
ty,
}
}
// scope-affecting: each arm binds its pattern vars. Mirror
// synth's Match arm exactly — including its own helper
// `type_check_pattern`, which resolves the binder types from
// the scrutinee's ADT. Push, lower the arm body, restore
// (innermost-first), per arm.
Term::Match { scrutinee, arms } => {
let m_scrut = Box::new(lower_term(ctx, scrutinee)?);
let s_ty = ctx.synth_pure(scrutinee)?;
let mut m_arms = Vec::with_capacity(arms.len());
for a in arms {
let bindings = crate::type_check_pattern(&a.pat, &s_ty, ctx.env)?;
let mut pushed = Vec::new();
for (n, t) in &bindings {
let prev = ctx.locals.insert(n.clone(), t.clone());
pushed.push((n.clone(), prev));
}
let m_body = lower_term(ctx, &a.body)?;
for (n, prev) in pushed.into_iter().rev() {
match prev {
Some(p) => {
ctx.locals.insert(n, p);
}
None => {
ctx.locals.shift_remove(&n);
}
}
}
m_arms.push(MArm { pat: a.pat.clone(), body: m_body });
}
MTerm::Match { scrutinee: m_scrut, arms: m_arms, ty }
}
// scope-affecting: params enter scope for the body. Mirror
// synth's Lam arm: insert each param:param_ty into locals for
// the body, restore after.
Term::Lam { params, param_tys, ret_ty, effects, body } => {
let saved = ctx.locals.clone();
for (p, pty) in params.iter().zip(param_tys.iter()) {
ctx.locals.insert(p.clone(), pty.clone());
}
let m_body = Box::new(lower_term(ctx, body)?);
ctx.locals = saved;
MTerm::Lam {
params: params.clone(),
param_tys: param_tys.clone(),
ret_ty: (**ret_ty).clone(),
effects: effects.clone(),
body: m_body,
ty,
}
}
Term::Seq { lhs, rhs } => MTerm::Seq {
lhs: Box::new(lower_term(ctx, lhs)?),
rhs: Box::new(lower_term(ctx, rhs)?),
ty,
},
Term::Clone { value } => MTerm::Clone {
value: Box::new(lower_term(ctx, value)?),
ty,
},
Term::ReuseAs { source, body } => MTerm::ReuseAs {
source: Box::new(lower_term(ctx, source)?),
body: Box::new(lower_term(ctx, body)?),
ty,
},
// scope-affecting: binders enter scope and a loop frame is
// pushed for the body so an inner Recur resolves. Mirror
// synth's Loop arm: push the binder frame onto loop_stack and
// the binder types onto locals before lowering the body,
// pop/restore after.
Term::Loop { binders, body } => {
let mut m_binders = Vec::with_capacity(binders.len());
for b in binders {
m_binders.push(MLoopBinder {
name: b.name.clone(),
ty: b.ty.clone(),
init: lower_term(ctx, &b.init)?,
});
}
let saved_locals = ctx.locals.clone();
let frame: Vec<(String, Type)> =
binders.iter().map(|b| (b.name.clone(), b.ty.clone())).collect();
for b in binders {
ctx.locals.insert(b.name.clone(), b.ty.clone());
}
ctx.loop_stack.push(frame);
let m_body = Box::new(lower_term(ctx, body)?);
ctx.loop_stack.pop();
ctx.locals = saved_locals;
MTerm::Loop { binders: m_binders, body: m_body, ty }
}
Term::Recur { args } => {
let m_args = args
.iter()
.map(|a| Ok(arg(lower_term(ctx, a)?)))
.collect::<Result<Vec<_>>>()?;
MTerm::Recur { args: m_args, ty }
}
// elem is None at mir.1 (mir.5 carries the element type).
Term::New { type_name, args } => {
let m_args = args
.iter()
.map(|a| {
Ok(match a {
ailang_core::ast::NewArg::Type(t) => MNewArg::Type(t.clone()),
ailang_core::ast::NewArg::Value(v) => {
MNewArg::Value(lower_term(ctx, v)?)
}
})
})
.collect::<Result<Vec<_>>>()?;
MTerm::New { type_name: type_name.clone(), elem: None, args: m_args, ty }
}
// scope-affecting: synth's LetRec arm binds `name: ty` for both
// `body` and `in_term`. Mirror it.
Term::LetRec { name, ty: rec_ty, params, body, in_term } => {
let saved = ctx.locals.clone();
ctx.locals.insert(name.clone(), rec_ty.clone());
let m_body = Box::new(lower_term(ctx, body)?);
let m_in = Box::new(lower_term(ctx, in_term)?);
ctx.locals = saved;
MTerm::LetRec {
name: name.clone(),
sig: rec_ty.clone(),
params: params.clone(),
body: m_body,
in_term: m_in,
ty,
}
}
Term::Intrinsic => MTerm::Intrinsic { ty },
})
}
/// Lower one post-mono module to MIR. `env` is the workspace check
/// 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> {
let mut env = env.clone();
env.current_module = module.name.clone();
// Seed `env.globals` from the current module's fns so `synth`'s
// `Term::Var` lookup resolves bare same-module references —
// including the monomorphic specialisations mono appended (e.g.
// `compare__Int`). Mirrors `mono::collect_mono_targets`
// (mono.rs:769-781); per-module because top-level fn names are
// only per-module-unique.
if let Some(g) = env.module_globals.get(&module.name).cloned() {
for (n, t) in g {
env.globals.insert(n, t);
}
}
// Seed `env.imports` from the current module's import list so
// `synth`'s qualified-var path resolves `Mod.fn` references.
// Mirrors mono.rs:782-787.
if let Some(im) = env.module_imports.get(&module.name).cloned() {
env.imports = im;
}
let mut defs = Vec::new();
for def in &module.defs {
let ailang_core::ast::Def::Fn(f) = def else { continue };
// An `(intrinsic)` body is signature-only — codegen supplies it
// via the intercept registry, never walking a body. Skip the
// lower (which would hit synth's `Term::Intrinsic` unreachable
// guard), exactly as `mono::collect_mono_targets` skips its
// synth re-entry. No `MirDef` is produced for such a fn.
if crate::is_intrinsic_body(f) {
continue;
}
// param types from the fn signature (same source mono uses).
let param_tys = crate::fn_param_types(&f.ty);
let mut locals: IndexMap<String, Type> = IndexMap::new();
for (n, t) in f.params.iter().zip(param_tys.iter()) {
locals.insert(n.clone(), t.clone());
}
let mut ctx = Ctx {
env: &env,
in_def: &f.name,
locals,
loop_stack: Vec::new(),
};
let body = lower_term(&mut ctx, &f.body)?;
defs.push(MirDef {
name: f.name.clone(),
sig: f.ty.clone(),
params: f.params.clone(),
body,
});
}
Ok(MirModule { name: module.name.clone(), defs })
}
+17 -10
View File
@@ -751,12 +751,15 @@ pub fn collect_mono_targets(
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
other => (vec![], other.clone()),
};
let (param_tys, _ret_ty, _eff): (Vec<Type>, Type, Vec<String>) = match &inner_ty {
Type::Fn { params, ret, effects, .. } => {
(params.clone(), (**ret).clone(), effects.clone())
}
_ => return Ok(Vec::new()),
};
// Non-fn signature carries no mono slots — keep the early-return
// guard; the param-type extraction itself goes through the shared
// `crate::fn_param_types` helper so this site and
// `lower_to_mir::lower_module` cannot drift. (`ret` / `effects`
// were extracted here only to be discarded.)
if !matches!(&inner_ty, Type::Fn { .. }) {
return Ok(Vec::new());
}
let param_tys: Vec<Type> = crate::fn_param_types(&f.ty);
let mut env = env.clone();
for v in &rigids {
@@ -1460,10 +1463,14 @@ pub(crate) fn collect_residuals_ordered(
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
other => (vec![], other.clone()),
};
let param_tys: Vec<Type> = match &inner_ty {
Type::Fn { params, .. } => params.clone(),
_ => return Ok(Vec::new()),
};
// Non-fn signature carries no mono slots — keep the early-return
// guard; the param-type extraction itself goes through the shared
// `crate::fn_param_types` helper so this site and
// `lower_to_mir::lower_module` cannot drift.
if !matches!(&inner_ty, Type::Fn { .. }) {
return Ok(Vec::new());
}
let param_tys: Vec<Type> = crate::fn_param_types(&f.ty);
let mut env = env.clone();
for v in &rigids {
@@ -0,0 +1,99 @@
//! mir.1a: `lower_to_mir` fills `ty` on every node from the canonical
//! `synth`, and routes string literals to `MTerm::Str { rep: Static }`.
//! Loads the boundary witnesses as workspace fixtures (prelude +
//! kernel injected by `load_workspace`) and elaborates the whole
//! workspace, so the walk is exercised over the full prelude. These
//! pins protect the producer; codegen consumption arrives in mir.1b.
use ailang_check::elaborate_workspace;
use ailang_mir::{MTerm, MirWorkspace, StrRep};
use ailang_surface::load_workspace;
use std::path::{Path, PathBuf};
/// `<repo>/examples` — resolved from this crate's manifest dir,
/// cwd-independent (same pattern as `method_collision_pin.rs`).
fn examples_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent().expect("crates/ailang-check has parent crates/")
.parent().expect("crates/ has parent repo root")
.join("examples")
}
/// Load a witness fixture (prelude injected) and elaborate it to MIR.
fn elaborate_fixture(module: &str) -> MirWorkspace {
let entry = examples_dir().join(format!("{module}.ail"));
let ws = load_workspace(&entry).expect("fixture loads (prelude injected)");
elaborate_workspace(&ws).expect("witness elaborates to MIR")
}
/// A def's lowered body in the elaborated workspace.
fn body<'a>(mir: &'a MirWorkspace, module: &str, def: &str) -> &'a MTerm {
&mir.modules[module]
.defs
.iter()
.find(|d| d.name == def)
.expect("def present")
.body
}
#[test]
fn str_literal_lowers_to_static_str_node() {
// #49 witness: the loop seed "x" is a Str literal → MTerm::Str,
// rep = Static at mir.1 (mir.4 flips loop seeds to Heap).
let m = "loop_recur_str_binder_no_leak_pin";
let mir = elaborate_fixture(m);
assert!(
find_static_str_seed(body(&mir, m, "main")),
"loop seed \"x\" must lower to MTerm::Str {{ rep: Static }}"
);
}
#[test]
fn new_over_user_adt_carries_node_types() {
// #53 witness: every node has a filled `ty`; the lowered
// `(new Counter 42)` node's type is the user ADT `Counter`.
//
// A *monomorphic* `new` (no leading type-arg) is desugared to a
// type-scoped call `(app Counter.new 42)` before lower_to_mir runs
// (desugar.rs:1083-1098) — so the let-init lowers to `MTerm::App`,
// not `MTerm::New`. `MTerm::New` survives only for a *polymorphic*
// `new` carrying a written `NewArg::Type` (the #51 RawBuf case).
// What this pin protects is the ty-fill: the lowered init node
// carries the checker-proved result type `Counter`.
let m = "new_counter_user_adt";
let mir = elaborate_fixture(m);
let MTerm::Let { init, .. } = body(&mir, m, "main") else {
panic!("main body is a let");
};
let ty_str = ailang_core::pretty::type_to_string(&init.ty());
assert!(
ty_str.contains("Counter"),
"lowered (new Counter 42) init node ty is Counter, got {ty_str}"
);
}
#[test]
fn rawbuf_size_only_elaborates() {
// #51 witness: a RawBuf read only for size — must elaborate clean
// (the element type carried only by the author's annotation does
// not block lowering).
let mir = elaborate_fixture("new_rawbuf_size_only");
assert!(mir.modules.contains_key("new_rawbuf_size_only"));
}
/// Recursively search for a `MTerm::Str { rep: Static }` reachable
/// from a loop binder init (the seed).
fn find_static_str_seed(t: &MTerm) -> bool {
match t {
MTerm::Loop { binders, body, .. } => {
binders.iter().any(|b| matches!(
&b.init,
MTerm::Str { rep: StrRep::Static, .. }
)) || find_static_str_seed(body)
}
MTerm::Let { init, body, .. } => {
find_static_str_seed(init) || find_static_str_seed(body)
}
_ => false,
}
}
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "ailang-mir"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ailang-core.workspace = true
+154
View File
@@ -0,0 +1,154 @@
//! 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, 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,
pub defs: Vec<MirDef>,
}
/// 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,
}
#[derive(Debug, Clone)]
pub enum Callee {
Static { module: String, fn_name: String },
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_(),
}
}
}