diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index 3e73f15..909d9a6 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -111,6 +111,54 @@ fn mono_symbol_primitive_types_use_surface_form() { assert_eq!(ailang_check::mono::mono_symbol("foo", &unit_ty), "foo#Unit"); } +/// Property: collect_mono_targets returns exactly the set of +/// fully-concrete `(class, method, type)` triples observed at +/// class-method call sites in a fn body, with the registry's +/// `defining_module` attached to each. The fixture's `main` calls +/// `show 42` once with `Int`, and the workspace declares +/// `instance Show Int` in the same module — the expected output is +/// a single MonoTarget naming `Show`, `show`, `Int`, and that +/// module. +#[test] +fn collect_mono_targets_single_concrete_call_site() { + use ailang_check::mono::collect_mono_targets; + + let entry = examples_dir().join("test_22b2_instance_present.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); + + // Build the workspace-wide env (class_methods, registry, ...) + // exactly as `check_in_workspace` does. The mono pass exposes + // this via `build_workspace_env`. + let env = ailang_check::mono::build_workspace_env(&ws); + + // The fixture has one user fn — `main`. Locate it. + let main_module = ws.modules.get("test_22b2_instance_present").expect("module"); + let main_fn = main_module + .defs + .iter() + .find_map(|d| match d { + ailang_core::ast::Def::Fn(f) if f.name == "main" => Some(f), + _ => None, + }) + .expect("main fn"); + + let targets = collect_mono_targets(main_fn, "test_22b2_instance_present", &env) + .expect("collect"); + + assert_eq!(targets.len(), 1, "one target expected: {:?}", targets); + let t = &targets[0]; + assert_eq!(t.class, "Show"); + assert_eq!(t.method, "show"); + assert!( + matches!(&t.type_, ailang_core::ast::Type::Con { name, args } if name == "Int" && args.is_empty()), + "type must be Int, got {:?}", + t.type_ + ); + assert_eq!(t.defining_module, "test_22b2_instance_present"); +} + #[test] fn mono_symbol_compound_type_uses_hash_suffix() { // Compound types fall through to canonical type_hash with an diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 985b3f2..cd9ac3e 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -123,7 +123,7 @@ fn instantiate(forall_vars: &[String], body: &Type, counter: &mut u32) -> (Vec) -> Type { +pub(crate) fn substitute_rigids(t: &Type, mapping: &BTreeMap) -> Type { match t { Type::Var { name } => mapping.get(name).cloned().unwrap_or_else(|| t.clone()), Type::Con { name, args } => Type::Con { @@ -1436,7 +1436,7 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> { /// the no-instance lookup — anything carrying a metavar / rigid-var /// component is either covered by the missing-constraint check or /// genuinely under-specified at the call site. -fn is_fully_concrete(t: &Type) -> bool { +pub(crate) fn is_fully_concrete(t: &Type) -> bool { match t { Type::Con { args, .. } => args.iter().all(is_fully_concrete), Type::Var { .. } => false, diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index abaaec0..b3dddf3 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -51,9 +51,11 @@ //! the implementation so the constraint is visible to anyone //! extending the skeleton. -use ailang_core::ast::{Def, Type}; +use ailang_core::ast::{Def, FnDef as AstFnDef, Type}; use ailang_core::workspace::Workspace; use crate::Result; +use indexmap::IndexMap; +use std::collections::BTreeSet; /// Iter 22b.3: workspace-wide monomorphisation pass entry. See the /// module-level doc for the architecture and contract. @@ -129,3 +131,172 @@ fn primitive_surface_name(ty: &Type) -> Option<&'static str> { _ => None, } } + +/// Iter 22b.3: one synthesisation request — a fully-concrete +/// `(class, method, type)` triple observed at a class-method call +/// site, plus the registry's `defining_module` for the matching +/// `InstanceDef` (the synthesised fn lives there). Equality of +/// targets is compared via [`mono_target_key`] — `(class, +/// method, type-hash)`. +#[derive(Debug, Clone)] +pub struct MonoTarget { + pub class: String, + pub method: String, + pub type_: Type, + pub defining_module: String, +} + +/// Iter 22b.3: dedup key for a [`MonoTarget`] — the same triple +/// `Registry::entries` uses for instance lookup, plus the method +/// name. Two targets with the same key produce the same +/// synthesised symbol and the same body. Consumed by the workspace +/// fixpoint (Task 5) and the rewrite walker (Task 6); declared +/// here in Task 3 so the dedup contract lives next to `MonoTarget`. +#[allow(dead_code)] +pub(crate) fn mono_target_key(t: &MonoTarget) -> (String, String, String) { + ( + t.class.clone(), + t.method.clone(), + ailang_core::canonical::type_hash(&t.type_), + ) +} + +/// Iter 22b.3: build the workspace-wide [`crate::Env`] used by +/// the mono pass to re-run [`crate::synth`] on each fn body. +/// Mirrors `check_in_workspace`'s setup: builtins + workspace +/// registry + per-module globals + class-method table + +/// superclass map. Cross-module type defs and consts are NOT +/// installed here — `synth` for mono only needs to (a) recognise +/// class-method names and (b) unify call-site arg types with +/// declared param types. Module-resident fns are still needed +/// for non-class-method calls inside instance bodies. +pub fn build_workspace_env(ws: &Workspace) -> crate::Env { + let mut env = crate::Env::default(); + crate::builtins::install(&mut env); + env.workspace_registry = ws.registry.clone(); + + // Per-module globals + class method table + superclass map. + // The construction order matches `check_in_workspace` so the + // env shape is interchangeable. `build_module_globals` may + // surface duplicate-def errors for malformed workspaces; the + // mono pass's pre-condition is that typecheck has already + // succeeded, so any error here means a caller violated that + // contract — fall back to an empty per-module table to keep + // this builder infallible. + let module_globals_index = crate::build_module_globals(ws) + .unwrap_or_default(); + env.module_globals = module_globals_index + .iter() + .map(|(mname, g)| (mname.clone(), g.fns.clone())) + .collect(); + env.module_types = ws + .modules + .iter() + .map(|(mname, m)| { + let tys: IndexMap = m + .defs + .iter() + .filter_map(|d| match d { + Def::Type(td) => Some((td.name.clone(), td.clone())), + _ => None, + }) + .collect(); + (mname.clone(), tys) + }) + .collect(); + + // Workspace-wide class-method table: merge per module's + // ModuleGlobals.class_methods. + for (_mname, g) in &module_globals_index { + for (name, entry) in &g.class_methods { + env.class_methods.insert(name.clone(), entry.clone()); + } + } + // Superclass map: walk every module's class defs. + for m in ws.modules.values() { + for d in &m.defs { + if let Def::Class(c) = d { + if let Some(sc) = &c.superclass { + env.class_superclasses.insert(c.name.clone(), sc.class.clone()); + } + } + } + } + env +} + +/// Iter 22b.3: re-run [`crate::synth`] on `f`'s body to recover +/// the per-fn residual class constraints. Filter to fully- +/// concrete residuals (the only ones eligible for monomorphisation), +/// look up each `(class, type-hash)` in the registry to recover +/// the `defining_module`, and return the list. Var-shaped or +/// metavar-shaped residuals are silently skipped — the +/// 22b.2 typecheck pass has already fired +/// `MissingConstraint`/`NoInstance` for any that should not exist +/// at this point. +pub fn collect_mono_targets( + f: &AstFnDef, + module_name: &str, + env: &crate::Env, +) -> Result> { + // Build the per-def env exactly as `check_fn` does — install + // rigid vars, set the current module, etc. This mirrors + // `crate::check_fn` minus the diagnostic emission. + let (rigids, inner_ty): (Vec, Type) = match &f.ty { + 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()), + }; + + let mut env = env.clone(); + for v in &rigids { + env.rigid_vars.insert(v.clone()); + } + env.current_module = module_name.to_string(); + + 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 effects: BTreeSet = BTreeSet::new(); + let mut subst = crate::Subst::default(); + let mut counter: u32 = 0; + let mut residuals: Vec = Vec::new(); + crate::synth( + &f.body, + &env, + &mut locals, + &mut effects, + &f.name, + &mut subst, + &mut counter, + &mut residuals, + )?; + + // Filter residuals to fully-concrete ones; look up + // defining_module via the registry. + let mut out: Vec = Vec::new(); + for r in residuals { + let r_ty = subst.apply(&r.type_); + if !crate::is_fully_concrete(&r_ty) { + continue; + } + let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty)); + let entry = match env.workspace_registry.entries.get(&key) { + Some(e) => e, + None => continue, // no-instance — Task 10 of 22b.2 fires; skip silently here. + }; + out.push(MonoTarget { + class: r.class.clone(), + method: r.method.clone(), + type_: r_ty, + defining_module: entry.defining_module.clone(), + }); + } + Ok(out) +}