diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index cbd2c2c..99c9411 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -2032,6 +2032,13 @@ fn build_to( // load-time remains valid. registry: ws.registry.clone(), }; + // Iter 22b.3: monomorphisation pass. After `lift_letrecs` and + // before codegen, every (class-method, concrete-type) call-site + // pair becomes a synthesised top-level fn; user call sites are + // rewritten to target those names. Class-free workspaces flow + // through bit-identically; see `ailang_check::monomorphise_workspace`. + let ws = ailang_check::monomorphise_workspace(&ws) + .map_err(|e| anyhow::anyhow!("monomorphise_workspace: {e}"))?; let ir = ailang_codegen::lower_workspace_with_alloc(&ws, alloc)?; let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id())); std::fs::create_dir_all(&tmpdir)?; diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs new file mode 100644 index 0000000..d5c0927 --- /dev/null +++ b/crates/ail/tests/typeclass_22b3.rs @@ -0,0 +1,49 @@ +//! Iter 22b.3: monomorphisation pass tests. +//! +//! Co-located with `typeclass_22b2.rs` so the typeclass-feature +//! coverage is browsable in one directory. Tests use the same +//! `examples_dir()` pattern — `cargo test`-cwd is the crate dir, +//! the fixtures live in `/examples/`. + +use std::path::PathBuf; + +fn examples_dir() -> PathBuf { + // crates/ail/ → repo root → examples/ + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.pop(); + d.pop(); + d.join("examples") +} + +#[test] +fn monomorphise_workspace_is_identity_on_class_free_workspace() { + // Pick any class-free fixture; `examples/hello.ail.json` is + // the canonical class-free smoke test. + let entry = examples_dir().join("hello.ail.json"); + let ws = ailang_core::load_workspace(&entry).expect("load"); + let diags = ailang_check::check_workspace(&ws); + assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags); + + let ws = ailang_check::monomorphise_workspace(&ws) + .expect("mono on class-free workspace"); + + // Module set unchanged. + let before = ailang_core::load_workspace(&examples_dir().join("hello.ail.json")) + .expect("re-load"); + assert_eq!( + ws.modules.keys().collect::>(), + before.modules.keys().collect::>(), + "module set must be identical" + ); + // Per-module def list unchanged (compare by canonical-JSON hash). + for (mname, m) in &ws.modules { + let before_m = &before.modules[mname]; + let h_after = ailang_core::module_hash(m); + let h_before = ailang_core::module_hash(before_m); + assert_eq!( + h_after, h_before, + "class-free module `{}` must hash-identical through mono", + mname + ); + } +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index ce5c3a3..669ba31 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -251,9 +251,11 @@ fn unify(a: &Type, b: &Type, subst: &mut Subst) -> Result<()> { pub mod builtins; pub mod diagnostic; pub mod lift; +mod mono; pub use diagnostic::{Diagnostic, Severity}; pub use lift::lift_letrecs; +pub use mono::monomorphise_workspace; /// Internal error type produced by the typechecker. /// diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs new file mode 100644 index 0000000..d4602ff --- /dev/null +++ b/crates/ailang-check/src/mono.rs @@ -0,0 +1,69 @@ +//! Iter 22b.3: workspace monomorphisation pass. +//! +//! Slots into the build pipeline after [`crate::lift_letrecs`] and +//! before `ailang_codegen::lower_workspace_with_alloc`. The pass +//! consumes the workspace's typeclass [`Registry`] and the per- +//! module class-method tables (already populated by +//! [`crate::check_in_workspace`]) and produces a workspace with: +//! +//! 1. Synthesised [`Def::Fn`] entries — one per unique +//! `(class, method, type-hash)` triple observed at any +//! class-method call site — appended to the [`Registry`]'s +//! `defining_module` for that instance. +//! 2. Rewritten call sites — every `Term::Var { name }` whose +//! `name` resolves through [`Env::class_methods`] is replaced +//! by the corresponding mono-symbol name (qualified with the +//! instance's `defining_module` iff that module differs from +//! the calling module). +//! +//! Pre-existing `Def::Class` and `Def::Instance` entries are +//! preserved verbatim — codegen ignores them, but downstream +//! tooling (e.g. `ail describe`) may still consult them. +//! +//! Class-free workspaces are byte-identical through the pass: +//! the early-out check at the top of [`monomorphise_workspace`] +//! returns the input clone untouched. +//! +//! ## Symbol-hashing invariant +//! +//! Synthesised FnDefs are post-typecheck artefacts. They do NOT +//! enter `CheckedModule.symbols` (built from the original module +//! at typecheck time and used by `ail diff` / `ail manifest`). +//! Same convention as [`crate::lift_letrecs`]. + +use ailang_core::ast::Def; +use ailang_core::workspace::Workspace; +use crate::Result; + +/// Iter 22b.3: workspace-wide monomorphisation pass entry. See the +/// module-level doc for the architecture and contract. +/// +/// Pre-condition: `ws` has been typechecked (`check_workspace(ws)` +/// returned no errors) and lifted (`lift_letrecs` per module). The +/// pass does not perform new type checking — it queries types via +/// `synth` on already-typechecked bodies. +pub fn monomorphise_workspace(ws: &Workspace) -> Result { + // Fast path — no class / instance defs anywhere → nothing to do. + // The pass also has no targets when there are class defs but no + // instance defs (no callable methods at concrete types), but + // the body walks would still be a wasted traversal; the + // class-free check is the cheap way to opt out. + if !workspace_has_typeclasses(ws) { + return Ok(ws.clone()); + } + + // Skeleton in Task 1: even with classes present, return the + // workspace clone unchanged. Subsequent tasks fill in collection, + // synthesis, and rewriting. + Ok(ws.clone()) +} + +/// Iter 22b.3: returns `true` iff any module in `ws` declares at +/// least one [`Def::Class`] or [`Def::Instance`]. Cheap workspace +/// scan; the early-out keeps class-free workspaces byte-identical +/// through the pass. +fn workspace_has_typeclasses(ws: &Workspace) -> bool { + ws.modules.values().any(|m| { + m.defs.iter().any(|d| matches!(d, Def::Class(_) | Def::Instance(_))) + }) +}