iter 22b.3.4: synthesise_mono_fn — instance body + class default fallback
This commit is contained in:
@@ -183,3 +183,127 @@ fn mono_symbol_compound_type_uses_hash_suffix() {
|
|||||||
let again = ailang_check::mono::mono_symbol("show", &pair_ty);
|
let again = ailang_check::mono::mono_symbol("show", &pair_ty);
|
||||||
assert_eq!(name, again, "mono_symbol must be deterministic");
|
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!(¶ms[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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -487,6 +487,13 @@ pub enum CheckError {
|
|||||||
class: String,
|
class: String,
|
||||||
at_type: 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>;
|
pub(crate) type Result<T> = std::result::Result<T, CheckError>;
|
||||||
@@ -527,6 +534,7 @@ impl CheckError {
|
|||||||
CheckError::ReuseAsNonAllocatingBody { .. } => "reuse-as-non-allocating-body",
|
CheckError::ReuseAsNonAllocatingBody { .. } => "reuse-as-non-allocating-body",
|
||||||
CheckError::MissingConstraint { .. } => "missing-constraint",
|
CheckError::MissingConstraint { .. } => "missing-constraint",
|
||||||
CheckError::NoInstance { .. } => "no-instance",
|
CheckError::NoInstance { .. } => "no-instance",
|
||||||
|
CheckError::Internal(_) => "internal",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,11 +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, FnDef as AstFnDef, Type};
|
use ailang_core::ast::{ClassDef, Def, FnDef as AstFnDef, InstanceDef, Term, Type};
|
||||||
use ailang_core::workspace::Workspace;
|
use ailang_core::workspace::Workspace;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::{BTreeMap, 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.
|
||||||
@@ -287,3 +287,100 @@ pub fn collect_mono_targets(
|
|||||||
}
|
}
|
||||||
Ok(out)
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user