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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "ailang-mir"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ailang-core.workspace = true
|
||||
@@ -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_(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user