iter 22b.3.4: synthesise_mono_fn — instance body + class default fallback

This commit is contained in:
2026-05-09 20:41:45 +02:00
parent 9f86c072dc
commit d6084a71fb
3 changed files with 232 additions and 3 deletions
+124
View File
@@ -183,3 +183,127 @@ fn mono_symbol_compound_type_uses_hash_suffix() {
let again = ailang_check::mono::mono_symbol("show", &pair_ty);
assert_eq!(name, again, "mono_symbol must be deterministic");
}
/// Property: synthesise_mono_fn copies the matching instance method's
/// body verbatim and produces a fully-concrete `FnDef` whose name uses
/// the `mono_symbol` form, whose `ty` is the class method's declared
/// type with the class param substituted to the target type, and whose
/// `params` / `body` come from the instance's `Term::Lam`.
#[test]
fn synthesise_mono_fn_uses_instance_body_lam() {
use ailang_check::mono::{synthesise_mono_fn, MonoTarget};
use ailang_core::ast::{ClassDef, ClassMethod, InstanceDef, InstanceMethod, Term};
let class_def = ClassDef {
name: "Foo".into(),
param: "a".into(),
superclass: None,
methods: vec![ClassMethod {
name: "bar".into(),
ty: Type::Fn {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
},
default: None,
}],
doc: None,
};
let instance = InstanceDef {
class: "Foo".into(),
type_: Type::int(),
methods: vec![InstanceMethod {
name: "bar".into(),
body: Term::Lam {
params: vec!["i".into()],
param_tys: vec![Type::Var { name: "a".into() }],
ret_ty: Box::new(Type::int()),
effects: vec![],
body: Box::new(Term::Var { name: "i".into() }),
},
}],
doc: None,
};
let target = MonoTarget {
class: "Foo".into(),
method: "bar".into(),
type_: Type::int(),
defining_module: "m".into(),
};
let f = synthesise_mono_fn(&target, &class_def, &instance).expect("synth");
assert_eq!(f.name, "bar#Int");
assert!(
matches!(
&f.ty,
Type::Fn { params, ret, .. }
if params.len() == 1
&& matches!(&params[0], Type::Con { name, args } if name == "Int" && args.is_empty())
&& matches!(ret.as_ref(), Type::Con { name, args } if name == "Int" && args.is_empty())
),
"ty = {:?}",
f.ty
);
assert_eq!(f.params, vec!["i".to_string()]);
assert!(
matches!(&f.body, Term::Var { name } if name == "i"),
"body = {:?}",
f.body
);
}
/// Property: when the instance omits a method that has a class-level
/// `default` body, synthesise_mono_fn falls back to that default and
/// uses its params/body verbatim — registry-build's `MissingMethod`
/// has already enforced that one of the two is present.
#[test]
fn synthesise_mono_fn_falls_back_to_class_default() {
use ailang_check::mono::{synthesise_mono_fn, MonoTarget};
use ailang_core::ast::{ClassDef, ClassMethod, InstanceDef, Literal, Term};
let class_def = ClassDef {
name: "Greet".into(),
param: "a".into(),
superclass: None,
methods: vec![ClassMethod {
name: "hello".into(),
ty: Type::Fn {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
},
default: Some(Term::Lam {
params: vec!["_x".into()],
param_tys: vec![Type::Var { name: "a".into() }],
ret_ty: Box::new(Type::str_()),
effects: vec![],
body: Box::new(Term::Lit {
lit: Literal::Str { value: "hi".into() },
}),
}),
}],
doc: None,
};
// Instance carries no `hello` body — must fall back to default.
let instance = InstanceDef {
class: "Greet".into(),
type_: Type::int(),
methods: vec![],
doc: None,
};
let target = MonoTarget {
class: "Greet".into(),
method: "hello".into(),
type_: Type::int(),
defining_module: "m".into(),
};
let f = synthesise_mono_fn(&target, &class_def, &instance).expect("synth");
assert_eq!(f.name, "hello#Int");
assert_eq!(f.params, vec!["_x".to_string()]);
assert!(matches!(&f.body, Term::Lit { .. }), "body = {:?}", f.body);
}
+9 -1
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
/// `instantiate`). Unlike `Subst::apply`, this targets vars by name —
/// 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 {
Type::Var { name } => mapping.get(name).cloned().unwrap_or_else(|| t.clone()),
Type::Con { name, args } => Type::Con {
@@ -487,6 +487,13 @@ pub enum CheckError {
class: String,
at_type: String,
},
/// Iter 22b.3: an internal invariant in the typechecker / mono pass
/// was violated — surfaced as an error so callers can propagate
/// rather than abort, but in well-formed inputs (typecheck has
/// succeeded) it must never fire. Code: `internal`.
#[error("internal: {0}")]
Internal(String),
}
pub(crate) type Result<T> = std::result::Result<T, CheckError>;
@@ -527,6 +534,7 @@ impl CheckError {
CheckError::ReuseAsNonAllocatingBody { .. } => "reuse-as-non-allocating-body",
CheckError::MissingConstraint { .. } => "missing-constraint",
CheckError::NoInstance { .. } => "no-instance",
CheckError::Internal(_) => "internal",
}
}
+99 -2
View File
@@ -51,11 +51,11 @@
//! the implementation so the constraint is visible to anyone
//! extending the skeleton.
use ailang_core::ast::{Def, FnDef as AstFnDef, Type};
use ailang_core::ast::{ClassDef, Def, FnDef as AstFnDef, InstanceDef, Term, Type};
use ailang_core::workspace::Workspace;
use crate::Result;
use indexmap::IndexMap;
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
/// Iter 22b.3: workspace-wide monomorphisation pass entry. See the
/// module-level doc for the architecture and contract.
@@ -287,3 +287,100 @@ pub fn collect_mono_targets(
}
Ok(out)
}
/// Iter 22b.3: produce a `FnDef` for a single (target, class,
/// instance) triple. The synthesised fn:
///
/// 1. has name [`mono_symbol`]`(target.method, target.type_)`,
/// 2. has type = the class's method type with the class param
/// substituted to `target.type_` (rigid-var substitution via
/// [`crate::substitute_rigids`]); the result is a plain
/// `Type::Fn` with no `Forall`,
/// 3. has params + body taken from the instance's matching
/// `InstanceMethod.body` if present and Lam-shaped; from the
/// class's `default` body if the instance omits the method;
/// or directly from the body if the method has no params
/// (zero-arg method types).
///
/// Errors (all [`crate::CheckError::Internal`] — caller-contract
/// violations that cannot occur after typecheck has succeeded):
///
/// - The instance omits the method AND the class has no `default`
/// — unreachable in practice because
/// `workspace::build_registry`'s `MissingMethod` check fires at
/// load.
/// - The class itself has no method by that name — also caught at
/// load.
/// - The method type is `(args) -> ret` with `args` non-empty but
/// the resolved body is not [`Term::Lam`] — schema-shape
/// mismatch enforced by 22b.2's instance-method check.
pub fn synthesise_mono_fn(
target: &MonoTarget,
class_def: &ClassDef,
instance: &InstanceDef,
) -> Result<AstFnDef> {
// Locate the class method declaration.
let class_method = class_def
.methods
.iter()
.find(|m| m.name == target.method)
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"synthesise_mono_fn: class `{}` has no method `{}`",
class_def.name, target.method
))
})?;
// Build the substitution `param := target.type_` and apply it
// to the method type. The result is a concrete `Type::Fn` (no
// `Forall`).
let mut mapping: BTreeMap<String, Type> = BTreeMap::new();
mapping.insert(class_def.param.clone(), target.type_.clone());
let concrete_method_ty = crate::substitute_rigids(&class_method.ty, &mapping);
// Resolve the body — instance override first, then class default.
let body_term: Term = match instance.methods.iter().find(|im| im.name == target.method) {
Some(im) => im.body.clone(),
None => class_method.default.clone().ok_or_else(|| {
crate::CheckError::Internal(format!(
"synthesise_mono_fn: instance `{} {}` omits method `{}` and class has no \
default (registry-build should have rejected this)",
target.class,
ailang_core::pretty::type_to_string(&target.type_),
target.method,
))
})?,
};
// Decide the (params, body) shape based on whether the method
// type takes positional args.
let method_has_params = matches!(
&class_method.ty,
Type::Fn { params, .. } if !params.is_empty()
);
let (params, body): (Vec<String>, Term) = if method_has_params {
// Method has positional params — body must be a Lam.
match body_term {
Term::Lam { params, body, .. } => (params, *body),
other => {
return Err(crate::CheckError::Internal(format!(
"synthesise_mono_fn: method `{}` has positional params but body is not \
a Lam: {:?}",
target.method, other
)));
}
}
} else {
// Zero-arg method — body is the value directly.
(Vec::new(), body_term)
};
Ok(AstFnDef {
name: mono_symbol(&target.method, &target.type_),
ty: concrete_method_ty,
params,
body,
doc: None,
suppress: Vec::new(),
})
}