iter 22b.3.3: collect_mono_targets — synth-replay residual gathering

This commit is contained in:
2026-05-09 20:31:12 +02:00
parent c77fd1b85e
commit 89311ee20b
3 changed files with 222 additions and 3 deletions
+48
View File
@@ -111,6 +111,54 @@ fn mono_symbol_primitive_types_use_surface_form() {
assert_eq!(ailang_check::mono::mono_symbol("foo", &unit_ty), "foo#Unit"); 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] #[test]
fn mono_symbol_compound_type_uses_hash_suffix() { fn mono_symbol_compound_type_uses_hash_suffix() {
// Compound types fall through to canonical type_hash with an // Compound types fall through to canonical type_hash with an
+2 -2
View File
@@ -123,7 +123,7 @@ fn instantiate(forall_vars: &[String], body: &Type, counter: &mut u32) -> (Vec<T
/// Substitutes named rigid vars throughout a type (used by /// Substitutes named rigid vars throughout a type (used by
/// `instantiate`). Unlike `Subst::apply`, this targets vars by name — /// `instantiate`). Unlike `Subst::apply`, this targets vars by name —
/// it has no notion of metavar ids. /// it has no notion of metavar ids.
fn substitute_rigids(t: &Type, mapping: &BTreeMap<String, Type>) -> Type { pub(crate) fn substitute_rigids(t: &Type, mapping: &BTreeMap<String, Type>) -> Type {
match t { match t {
Type::Var { name } => mapping.get(name).cloned().unwrap_or_else(|| t.clone()), Type::Var { name } => mapping.get(name).cloned().unwrap_or_else(|| t.clone()),
Type::Con { name, args } => Type::Con { 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 /// the no-instance lookup — anything carrying a metavar / rigid-var
/// component is either covered by the missing-constraint check or /// component is either covered by the missing-constraint check or
/// genuinely under-specified at the call site. /// 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 { match t {
Type::Con { args, .. } => args.iter().all(is_fully_concrete), Type::Con { args, .. } => args.iter().all(is_fully_concrete),
Type::Var { .. } => false, Type::Var { .. } => false,
+172 -1
View File
@@ -51,9 +51,11 @@
//! the implementation so the constraint is visible to anyone //! the implementation so the constraint is visible to anyone
//! extending the skeleton. //! extending the skeleton.
use ailang_core::ast::{Def, Type}; use ailang_core::ast::{Def, FnDef as AstFnDef, Type};
use ailang_core::workspace::Workspace; use ailang_core::workspace::Workspace;
use crate::Result; use crate::Result;
use indexmap::IndexMap;
use std::collections::BTreeSet;
/// Iter 22b.3: workspace-wide monomorphisation pass entry. See the /// Iter 22b.3: workspace-wide monomorphisation pass entry. See the
/// module-level doc for the architecture and contract. /// module-level doc for the architecture and contract.
@@ -129,3 +131,172 @@ fn primitive_surface_name(ty: &Type) -> Option<&'static str> {
_ => None, _ => 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<String, ailang_core::ast::TypeDef> = 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<Vec<MonoTarget>> {
// 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<String>, 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>, Type, Vec<String>) = 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<String, Type> = IndexMap::new();
for (n, t) in f.params.iter().zip(param_tys.iter()) {
locals.insert(n.clone(), t.clone());
}
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();
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<MonoTarget> = 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)
}