iter 22b.3.1: mono pass skeleton — identity for class-free workspaces

This commit is contained in:
2026-05-09 20:12:36 +02:00
parent f7791e6cbd
commit 209074ee5d
4 changed files with 127 additions and 0 deletions
+7
View File
@@ -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)?;
+49
View File
@@ -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 `<repo>/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::<Vec<_>>(),
before.modules.keys().collect::<Vec<_>>(),
"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
);
}
}
+2
View File
@@ -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.
///
+69
View File
@@ -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<Workspace> {
// 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(_)))
})
}