From 135f4ceed7e1bb747c21f784449f711255da882b Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 31 May 2026 14:04:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(check):=20mir.1a=20=E2=80=94=20typed-MIR?= =?UTF-8?q?=20types=20+=20post-mono=20lower=5Fto=5Fmir=20+=20elaborate=5Fw?= =?UTF-8?q?orkspace=20(additive)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, 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 --- Cargo.lock | 8 + Cargo.toml | 2 + crates/ailang-check/Cargo.toml | 1 + crates/ailang-check/src/lib.rs | 76 ++++- crates/ailang-check/src/lower_to_mir.rs | 335 +++++++++++++++++++ crates/ailang-check/src/mono.rs | 27 +- crates/ailang-check/tests/lower_to_mir_ty.rs | 99 ++++++ crates/ailang-mir/Cargo.toml | 8 + crates/ailang-mir/src/lib.rs | 154 +++++++++ examples/new_counter_user_adt.ail | 6 + examples/new_rawbuf_size_only.ail | 7 + 11 files changed, 712 insertions(+), 11 deletions(-) create mode 100644 crates/ailang-check/src/lower_to_mir.rs create mode 100644 crates/ailang-check/tests/lower_to_mir_ty.rs create mode 100644 crates/ailang-mir/Cargo.toml create mode 100644 crates/ailang-mir/src/lib.rs create mode 100644 examples/new_counter_user_adt.ail create mode 100644 examples/new_rawbuf_size_only.ail diff --git a/Cargo.lock b/Cargo.lock index d16a435..cba0509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ name = "ailang-check" version = "0.0.1" dependencies = [ "ailang-core", + "ailang-mir", "ailang-surface", "indexmap", "serde", @@ -58,6 +59,13 @@ dependencies = [ name = "ailang-kernel" version = "0.0.1" +[[package]] +name = "ailang-mir" +version = "0.0.1" +dependencies = [ + "ailang-core", +] + [[package]] name = "ailang-prose" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 4d30772..dab9976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "crates/ailang-core", + "crates/ailang-mir", "crates/ailang-check", "crates/ailang-codegen", "crates/ailang-kernel", @@ -32,6 +33,7 @@ indexmap = { version = "2", features = ["serde"] } tempfile = "3" ailang-core = { path = "crates/ailang-core" } +ailang-mir = { path = "crates/ailang-mir" } ailang-check = { path = "crates/ailang-check" } ailang-codegen = { path = "crates/ailang-codegen" } ailang-kernel = { path = "crates/ailang-kernel" } diff --git a/crates/ailang-check/Cargo.toml b/crates/ailang-check/Cargo.toml index 9be58f6..def29dd 100644 --- a/crates/ailang-check/Cargo.toml +++ b/crates/ailang-check/Cargo.toml @@ -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 diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 8b8d5c6..0819691 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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, } +/// 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> { + // 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 { + 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, diff --git a/crates/ailang-check/src/lower_to_mir.rs b/crates/ailang-check/src/lower_to_mir.rs new file mode 100644 index 0000000..76c2ae4 --- /dev/null +++ b/crates/ailang-check/src/lower_to_mir.rs @@ -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, + loop_stack: Vec>, +} + +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 { + let mut effects: BTreeSet = BTreeSet::new(); + let mut subst = crate::Subst::default(); + let mut counter: u32 = 0; + let mut residuals: Vec = Vec::new(); + let mut free_fn_calls: Vec = Vec::new(); + let mut warnings: Vec = 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 { + 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::>>()?; + 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::>>()?; + 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::>>()?; + 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::>>()?; + 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::>>()?; + 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 { + 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 = 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 }) +} diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 96ff174..2087bc6 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -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, Vec) = 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 = 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 = 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 = crate::fn_param_types(&f.ty); let mut env = env.clone(); for v in &rigids { diff --git a/crates/ailang-check/tests/lower_to_mir_ty.rs b/crates/ailang-check/tests/lower_to_mir_ty.rs new file mode 100644 index 0000000..1f60831 --- /dev/null +++ b/crates/ailang-check/tests/lower_to_mir_ty.rs @@ -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}; + +/// `/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, + } +} diff --git a/crates/ailang-mir/Cargo.toml b/crates/ailang-mir/Cargo.toml new file mode 100644 index 0000000..7b824ce --- /dev/null +++ b/crates/ailang-mir/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ailang-mir" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +ailang-core.workspace = true diff --git a/crates/ailang-mir/src/lib.rs b/crates/ailang-mir/src/lib.rs new file mode 100644 index 0000000..807e311 --- /dev/null +++ b/crates/ailang-mir/src/lib.rs @@ -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, +} + +#[derive(Debug, Clone)] +pub struct MirModule { + pub name: String, + pub defs: Vec, +} + +/// 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, + 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()` at mir.1; mir.2 + /// resolves static targets to `Callee::Static`. + App { callee: Callee, args: Vec, tail: bool, ty: Type }, + Let { name: String, mode: Mode, init: Box, body: Box, ty: Type }, + LetRec { + name: String, + sig: Type, + params: Vec, + body: Box, + in_term: Box, + ty: Type, + }, + If { cond: Box, then: Box, else_: Box, ty: Type }, + Do { op: String, args: Vec, tail: bool, ty: Type }, + Ctor { type_name: String, ctor: String, args: Vec, ty: Type }, + Match { scrutinee: Box, arms: Vec, ty: Type }, + Lam { + params: Vec, + param_tys: Vec, + ret_ty: Type, + effects: Vec, + body: Box, + ty: Type, + }, + Seq { lhs: Box, rhs: Box, ty: Type }, + Clone { value: Box, ty: Type }, + ReuseAs { source: Box, body: Box, ty: Type }, + Loop { binders: Vec, body: Box, ty: Type }, + Recur { args: Vec, ty: Type }, + /// `elem` is `None` at mir.1; mir.5 carries the element type. + New { type_name: String, elem: Option, args: Vec, 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), +} + +#[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_(), + } + } +} diff --git a/examples/new_counter_user_adt.ail b/examples/new_counter_user_adt.ail new file mode 100644 index 0000000..5013529 --- /dev/null +++ b/examples/new_counter_user_adt.ail @@ -0,0 +1,6 @@ +(module new_counter_user_adt + (data Counter (ctor MkCounter (con Int))) + (fn new (type (fn-type (params (con Int)) (ret (con Counter)))) (params n) + (body (term-ctor Counter MkCounter n))) + (fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) + (body (let c (new Counter 42) (do io/print_str "ok\n"))))) diff --git a/examples/new_rawbuf_size_only.ail b/examples/new_rawbuf_size_only.ail new file mode 100644 index 0000000..8c3b6de --- /dev/null +++ b/examples/new_rawbuf_size_only.ail @@ -0,0 +1,7 @@ +(module new_rawbuf_size_only + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (let b (new RawBuf (con Int) 3) + (app print (app RawBuf.size b))))))