iter mq.2: type-driven dispatch mechanism installed (mechanism-before-exercise)
Installs the dispatch infrastructure for type-driven method resolution without retiring MethodNameCollision yet. The new multi-candidate path is exercised exclusively by 15 unit tests; real workspaces continue producing single-class residuals (candidates: None) and every pre-mq.2 fixture typechecks unchanged. Three new CheckError variants: - AmbiguousMethodResolution (multi-candidate after type-driven filter) - UnknownClass (explicit qualifier names a class not in registry) - NoInstance gains additive candidate_classes field (Vec<String>) Schema/Env additions: - Env.method_to_candidate_classes: BTreeMap<String, BTreeSet<String>> workspace-flat inverse of class_methods, built in build_check_env. - ResidualConstraint.candidates: Option<BTreeSet<String>> — None preserves pre-mq.2 single-class semantics; Some(set) carries the multi-candidate residual that discharge refines. - ResidualConstraint visibility bumped pub(crate) → pub for unit-test crate access. New helpers: - MethodDispatchOutcome enum + pure resolve_method_dispatch implementing the spec's 5-step rule (qualifier → singleton → type-driven filter → constraint-driven filter → Multi for discharge-time refinement). - parse_method_qualifier splits Term::Var.name into (method_name, optional_qualifier_prefix) at the last dot. - RefineOutcome enum + refine_multi_candidate_residual for discharge-time refinement. - resolve_residual_class_for_mono wires the refinement into mono's collect_residuals_ordered residual-to-target mapping. Synth Var-arm class-method branch rewritten via parse_method_qualifier with inner-dot gate (qualifier must be <module>.<Class>; single-dot names fall through to the existing qualified-fn path). Constraint-discharge in check_fn uses expanded (post-superclass- expansion) constraints for the rigid-var path — sounder than raw declared_constraints. Plan-invented format_type_for_display replaced with the existing ailang_core::pretty::type_to_string (one less duplicate). Synth-time declared_constraints: &[] is a deliberate gap documented as known debt — load-bearing only post-mq.3 for the rigid-var fallback (env-plumbing the active fn's constraints into the Var arm is a ~10-line edit slated for mq.3). 9/9 tasks, 539 tests green (was 520 pre-mq.2; +15 mq.2 unit tests + 4 pre-existing). bench/compile_check.py + cross_lang.py clean; bench/check.py 1 regression (latency noise — runtime cannot be touched by a typecheck-side iter).
This commit is contained in:
@@ -75,7 +75,23 @@
|
||||
//! Dual of `missing-constraint`: when the residual is concrete, the
|
||||
//! fn cannot push the obligation to a caller, so an existing
|
||||
//! instance is the only way to discharge it. `ctx`:
|
||||
//! `{"class": "<C>", "method": "<m>", "at_type": "<T>"}`.
|
||||
//! `{"class": "<C>", "method": "<m>", "at_type": "<T>"}`. mq.2 adds
|
||||
//! an optional `candidate_classes` field when the residual originated
|
||||
//! from the multi-candidate dispatch path; absent on single-class
|
||||
//! residuals (back-compat).
|
||||
//! - `ambiguous-method-resolution` (mq.2) — `severity: error`. Emitted
|
||||
//! by the type-driven dispatch resolver (or the discharge-time
|
||||
//! multi-candidate refinement) when a bare-method call site survives
|
||||
//! both type-driven and constraint-driven filtering with more than
|
||||
//! one candidate class. `ctx`: `{"method": "<m>", "at_type": "<T>",
|
||||
//! "candidate_classes": ["<C1>", ...]}`. The author disambiguates
|
||||
//! by writing `<ClassQualifier>.<method> x`.
|
||||
//! - `unknown-class` (mq.2) — `severity: error`. Emitted by the
|
||||
//! type-driven dispatch resolver when an explicit class qualifier
|
||||
//! in a `Term::Var.name` (e.g. `"prelude.Show.show"`) names a
|
||||
//! qualified class that is not in the workspace registry of
|
||||
//! candidate classes for the method. `ctx`:
|
||||
//! `{"name": "<qualified-class>"}`.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
|
||||
+730
-16
@@ -569,6 +569,38 @@ pub enum CheckError {
|
||||
method: String,
|
||||
class: String,
|
||||
at_type: String,
|
||||
/// mq.2: additive candidate-class list. Empty on residuals from
|
||||
/// the single-class dispatch path (pre-mq.2 shape preserved).
|
||||
/// Non-empty when the diagnostic originated from the
|
||||
/// multi-candidate refinement path with zero registry survivors.
|
||||
candidate_classes: Vec<String>,
|
||||
},
|
||||
|
||||
/// mq.2: a monomorphic call site `<method> x` with multiple
|
||||
/// candidate classes (each declaring `<method>` and each having an
|
||||
/// instance for `x`'s concrete type) cannot be resolved unambiguously.
|
||||
/// The LLM-author writes an explicit qualifier
|
||||
/// (`<class-qualifier>.<method> x`) to disambiguate.
|
||||
/// Code: `ambiguous-method-resolution`. `ctx`:
|
||||
/// `{"method": "<m>", "at_type": "<T>", "candidate_classes": ["<C1>", ...]}`.
|
||||
#[error(
|
||||
"method `{method}` at type `{at_type}` is ambiguous: classes \
|
||||
{candidate_classes:?} all declare it and provide an instance. \
|
||||
Disambiguate with `<ClassQualifier>.{method}`."
|
||||
)]
|
||||
AmbiguousMethodResolution {
|
||||
method: String,
|
||||
at_type: String,
|
||||
candidate_classes: Vec<String>,
|
||||
},
|
||||
|
||||
/// mq.2: an explicit class qualifier in `Term::Var.name` names a
|
||||
/// qualified class that is not in the workspace registry of
|
||||
/// candidate classes for the method. Code: `unknown-class`. `ctx`:
|
||||
/// `{"name": "<qualified-class>"}`.
|
||||
#[error("class `{name}` is not declared in any module of this workspace")]
|
||||
UnknownClass {
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Iter 22b.3: an internal invariant in the typechecker / mono pass
|
||||
@@ -617,6 +649,8 @@ impl CheckError {
|
||||
CheckError::ReuseAsNonAllocatingBody { .. } => "reuse-as-non-allocating-body",
|
||||
CheckError::MissingConstraint { .. } => "missing-constraint",
|
||||
CheckError::NoInstance { .. } => "no-instance",
|
||||
CheckError::AmbiguousMethodResolution { .. } => "ambiguous-method-resolution",
|
||||
CheckError::UnknownClass { .. } => "unknown-class",
|
||||
CheckError::Internal(_) => "internal",
|
||||
}
|
||||
}
|
||||
@@ -658,8 +692,26 @@ impl CheckError {
|
||||
CheckError::MissingConstraint { class, method, at_type, .. } => {
|
||||
serde_json::json!({"class": class, "method": method, "at_type": at_type})
|
||||
}
|
||||
CheckError::NoInstance { class, method, at_type, .. } => {
|
||||
serde_json::json!({"class": class, "method": method, "at_type": at_type})
|
||||
CheckError::NoInstance { class, method, at_type, candidate_classes, .. } => {
|
||||
let mut obj = serde_json::json!({
|
||||
"class": class,
|
||||
"method": method,
|
||||
"at_type": at_type,
|
||||
});
|
||||
if !candidate_classes.is_empty() {
|
||||
obj["candidate_classes"] = serde_json::json!(candidate_classes);
|
||||
}
|
||||
obj
|
||||
}
|
||||
CheckError::AmbiguousMethodResolution { method, at_type, candidate_classes } => {
|
||||
serde_json::json!({
|
||||
"method": method,
|
||||
"at_type": at_type,
|
||||
"candidate_classes": candidate_classes,
|
||||
})
|
||||
}
|
||||
CheckError::UnknownClass { name } => {
|
||||
serde_json::json!({ "name": name })
|
||||
}
|
||||
_ => serde_json::Value::Object(serde_json::Map::new()),
|
||||
}
|
||||
@@ -1291,6 +1343,14 @@ pub fn build_check_env(ws: &Workspace) -> Env {
|
||||
for g in mg.values() {
|
||||
for (n, e) in &g.class_methods {
|
||||
env.class_methods.insert(n.clone(), e.clone());
|
||||
// mq.2: build the inverse method → candidate-class set in
|
||||
// the same loop. `e.class_name` already carries the
|
||||
// qualified form (mq.1 invariant), so the set is keyed on
|
||||
// workspace-flat qualified class names directly.
|
||||
env.method_to_candidate_classes
|
||||
.entry(n.clone())
|
||||
.or_default()
|
||||
.insert(e.class_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1657,6 +1717,68 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
|
||||
let expanded = expand_declared_constraints(&declared_constraints, &env.class_superclasses);
|
||||
for r in &residuals {
|
||||
let r_ty = subst.apply(&r.type_);
|
||||
|
||||
// mq.2: multi-candidate residuals refine before the existing
|
||||
// single-class discharge path. `MethodNameCollision` keeps the
|
||||
// candidate set singleton in real workspaces today; this branch
|
||||
// is exercised exclusively by unit tests until mq.3.
|
||||
if r.candidates.is_some() {
|
||||
// Normalize the type before hashing — same contract as the
|
||||
// single-class path below.
|
||||
let r_ty_norm = env
|
||||
.workspace_registry
|
||||
.normalize_type_for_lookup(env.current_module.as_str(), &r_ty);
|
||||
let normalized_residual = ResidualConstraint {
|
||||
class: r.class.clone(),
|
||||
type_: r_ty_norm,
|
||||
method: r.method.clone(),
|
||||
candidates: r.candidates.clone(),
|
||||
};
|
||||
let registry_unit: BTreeMap<(String, String), ()> = env
|
||||
.workspace_registry
|
||||
.entries
|
||||
.keys()
|
||||
.map(|k| (k.clone(), ()))
|
||||
.collect();
|
||||
match refine_multi_candidate_residual(
|
||||
&normalized_residual,
|
||||
&expanded,
|
||||
®istry_unit,
|
||||
) {
|
||||
RefineOutcome::Resolved(_) => {
|
||||
// Discharge succeeded — the resolved class has an
|
||||
// instance for this concrete type (or the rigid
|
||||
// var has a matching declared constraint). Move
|
||||
// on to the next residual; nothing to push.
|
||||
continue;
|
||||
}
|
||||
RefineOutcome::NoInstance { method, at_type, candidate_classes } => {
|
||||
return Err(CheckError::NoInstance {
|
||||
def: f.name.clone(),
|
||||
method,
|
||||
class: r.class.clone(),
|
||||
at_type,
|
||||
candidate_classes,
|
||||
});
|
||||
}
|
||||
RefineOutcome::Ambiguous { method, at_type, candidate_classes } => {
|
||||
return Err(CheckError::AmbiguousMethodResolution {
|
||||
method,
|
||||
at_type,
|
||||
candidate_classes,
|
||||
});
|
||||
}
|
||||
RefineOutcome::MissingConstraint { method, candidate_classes } => {
|
||||
return Err(CheckError::MissingConstraint {
|
||||
def: f.name.clone(),
|
||||
method,
|
||||
class: candidate_classes.first().cloned().unwrap_or_default(),
|
||||
at_type: ailang_core::pretty::type_to_string(&r_ty),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Type::Var { name } = &r_ty {
|
||||
// Skip residuals over metavars (`$mN`) — those mean the
|
||||
// residual is over a value the body never connected to a
|
||||
@@ -1699,6 +1821,7 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
|
||||
method: r.method.clone(),
|
||||
class: r.class.clone(),
|
||||
at_type: ailang_core::pretty::type_to_string(&r_ty),
|
||||
candidate_classes: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1735,8 +1858,14 @@ pub(crate) fn is_fully_concrete(t: &Type) -> bool {
|
||||
/// argument's actual type) or, after `subst.apply`, a rigid var or a
|
||||
/// concrete `Type::Con`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ResidualConstraint {
|
||||
pub struct ResidualConstraint {
|
||||
/// Class name (e.g. `Show`).
|
||||
///
|
||||
/// When [`Self::candidates`] is `None`, this is the resolved class
|
||||
/// (single-class dispatch path). When `Some(set)`, this is a
|
||||
/// tentative value (typically the first set element in BTreeSet
|
||||
/// order); discharge / mono refinement overwrites it with the
|
||||
/// actually-resolved class.
|
||||
pub class: String,
|
||||
/// Class param's instance at this call site. Pre-substitution: the
|
||||
/// fresh metavar inserted at the Var site. Post-substitution: the
|
||||
@@ -1746,6 +1875,243 @@ pub(crate) struct ResidualConstraint {
|
||||
/// diagnostic to point the author at which call site triggered
|
||||
/// the residual.
|
||||
pub method: String,
|
||||
/// mq.2: candidate-class set for the multi-candidate dispatch
|
||||
/// path. `None` ⇒ single-class semantics (pre-mq.2 behaviour);
|
||||
/// the [`Self::class`] field is the resolved class. `Some(set)`
|
||||
/// ⇒ multi-candidate residual; discharge / mono refinement filters
|
||||
/// the set at fn-body-end. With `MethodNameCollision` still active
|
||||
/// (pre-mq.3), real workspaces only ever construct `None`-candidate
|
||||
/// residuals; the `Some`-path is exercised exclusively by unit
|
||||
/// tests until mq.3 retires the workaround.
|
||||
pub candidates: Option<BTreeSet<String>>,
|
||||
}
|
||||
|
||||
/// mq.2: outcome of the dispatch-resolution helper
|
||||
/// [`resolve_method_dispatch`]. The synth Var-arm consumer translates
|
||||
/// this enum into either a singleton [`ResidualConstraint`]
|
||||
/// (`Resolved`) or a multi-candidate residual (`Multi`) for discharge-
|
||||
/// time refinement, or a `CheckError` (`Unknown` / `Ambiguous`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MethodDispatchOutcome {
|
||||
/// Single class resolved unambiguously.
|
||||
Resolved(String),
|
||||
/// Explicit qualifier names a class not in the candidate set.
|
||||
UnknownClass(String),
|
||||
/// Bare-method call site survived both type-driven and
|
||||
/// constraint-driven filters with > 1 candidate.
|
||||
Ambiguous {
|
||||
method: String,
|
||||
at_type: String,
|
||||
candidates: Vec<String>,
|
||||
},
|
||||
/// Bare-method call site with multiple candidates that need
|
||||
/// discharge-time refinement (rigid var type at synth time;
|
||||
/// concrete type only available at discharge).
|
||||
Multi {
|
||||
method: String,
|
||||
candidates: BTreeSet<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// mq.2: resolve a method-call site per the spec's 5-step rule. Pure
|
||||
/// function — no Env mutation, no residual emission.
|
||||
///
|
||||
/// `qualifier_prefix` is the dot-stripped class prefix (e.g.
|
||||
/// `"prelude.Show"` for a `Term::Var.name == "prelude.Show.show"`).
|
||||
/// `None` = bare method form.
|
||||
///
|
||||
/// `concrete_arg_type` is `Some` when the caller has the arg type
|
||||
/// available (e.g. discharge-time refinement); `None` when synth is at
|
||||
/// the Var arm before App-arm unification. When `None`, the helper
|
||||
/// emits `Multi` if the candidate set is non-singleton (no early
|
||||
/// type-driven filter possible). When `Some(t)` with `t` a non-rigid
|
||||
/// concrete type, the type-driven filter applies; when `t` is a rigid
|
||||
/// `Type::Var`, the constraint-driven filter applies.
|
||||
///
|
||||
/// `declared_constraints` are the active forall-bound constraints in
|
||||
/// scope at the call site; consulted for the rigid-var fallback.
|
||||
///
|
||||
/// `registry` is the workspace registry keyed by `(qualified_class,
|
||||
/// type_hash)` for instance-existence checks. Values are unit; only
|
||||
/// key presence matters.
|
||||
pub fn resolve_method_dispatch(
|
||||
method: &str,
|
||||
qualifier_prefix: Option<&str>,
|
||||
candidates: &BTreeSet<String>,
|
||||
concrete_arg_type: Option<&Type>,
|
||||
declared_constraints: &[ailang_core::ast::Constraint],
|
||||
registry: &BTreeMap<(String, String), ()>,
|
||||
) -> MethodDispatchOutcome {
|
||||
// Step 3 (5-step rule): explicit qualifier present.
|
||||
if let Some(q) = qualifier_prefix {
|
||||
if candidates.contains(q) {
|
||||
return MethodDispatchOutcome::Resolved(q.to_string());
|
||||
}
|
||||
return MethodDispatchOutcome::UnknownClass(q.to_string());
|
||||
}
|
||||
|
||||
// Step 4: bare-method form, singleton candidate set — pre-mq.3
|
||||
// path. Returns immediately so the post-mq.3 multi-candidate
|
||||
// logic below stays gated on a non-singleton candidate set.
|
||||
if candidates.len() == 1 {
|
||||
return MethodDispatchOutcome::Resolved(
|
||||
candidates.iter().next().cloned().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Multi-candidate path: type-driven filter first when the arg
|
||||
// type is concrete (non-Var).
|
||||
let concrete = concrete_arg_type
|
||||
.filter(|t| !matches!(t, Type::Var { .. }));
|
||||
if let Some(t) = concrete {
|
||||
let type_h = ailang_core::canonical::type_hash(t);
|
||||
let survivors: BTreeSet<String> = candidates
|
||||
.iter()
|
||||
.filter(|c| registry.contains_key(&((**c).clone(), type_h.clone())))
|
||||
.cloned()
|
||||
.collect();
|
||||
if survivors.len() == 1 {
|
||||
return MethodDispatchOutcome::Resolved(
|
||||
survivors.into_iter().next().unwrap(),
|
||||
);
|
||||
}
|
||||
if survivors.len() > 1 {
|
||||
let mut sorted: Vec<String> = survivors.into_iter().collect();
|
||||
sorted.sort();
|
||||
let at_type = ailang_core::pretty::type_to_string(t);
|
||||
return MethodDispatchOutcome::Ambiguous {
|
||||
method: method.to_string(),
|
||||
at_type,
|
||||
candidates: sorted,
|
||||
};
|
||||
}
|
||||
// Zero survivors after the type-driven filter: fall through
|
||||
// to constraint-driven filter, then `Multi` for discharge-
|
||||
// time refinement (where `NoInstance` with `candidate_classes`
|
||||
// is fired against the full candidate set).
|
||||
}
|
||||
|
||||
// Constraint-driven filter (rigid-var case or no concrete arg).
|
||||
let survivors: BTreeSet<String> = candidates
|
||||
.iter()
|
||||
.filter(|c| {
|
||||
declared_constraints
|
||||
.iter()
|
||||
.any(|dc| &dc.class == *c)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if survivors.len() == 1 {
|
||||
return MethodDispatchOutcome::Resolved(
|
||||
survivors.into_iter().next().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// No filter narrowed; emit `Multi` for discharge-time refinement.
|
||||
MethodDispatchOutcome::Multi {
|
||||
method: method.to_string(),
|
||||
candidates: candidates.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// mq.2: outcome of multi-candidate residual refinement at discharge
|
||||
/// time. Consumed by `check_fn`'s residual loop and by mono's
|
||||
/// residual-to-target mapping.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RefineOutcome {
|
||||
/// Refinement converged on a single class.
|
||||
Resolved(String),
|
||||
/// Concrete `type_` with zero registry survivors — the call site
|
||||
/// must be discharged via an existing instance, but none of the
|
||||
/// candidate classes has one for this type. Surfaces as a
|
||||
/// `CheckError::NoInstance` with the full candidate set populated.
|
||||
NoInstance {
|
||||
method: String,
|
||||
at_type: String,
|
||||
candidate_classes: Vec<String>,
|
||||
},
|
||||
/// Concrete `type_` with multiple registry survivors — every
|
||||
/// surviving candidate has an instance for this type; the author
|
||||
/// must disambiguate explicitly. Surfaces as a
|
||||
/// `CheckError::AmbiguousMethodResolution`.
|
||||
Ambiguous {
|
||||
method: String,
|
||||
at_type: String,
|
||||
candidate_classes: Vec<String>,
|
||||
},
|
||||
/// Rigid-var residual with non-singleton constraint-set survivors.
|
||||
/// Surfaces as a `CheckError::MissingConstraint` (the body must
|
||||
/// declare a constraint to commit to one of the candidates).
|
||||
MissingConstraint {
|
||||
method: String,
|
||||
candidate_classes: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// mq.2: refine a multi-candidate residual at discharge time per the
|
||||
/// spec's 5-step rule (constraint-discharge refinement subsection).
|
||||
/// Caller MUST pass a residual whose `candidates` is `Some(...)`;
|
||||
/// single-candidate residuals (`candidates: None`) bypass this helper
|
||||
/// and discharge directly against the registry as in pre-mq.2.
|
||||
///
|
||||
/// Refinement strategy:
|
||||
/// - Concrete `type_`: filter the candidate set against the registry
|
||||
/// keyed on `(class, type_hash(type_))`. 0 survivors → `NoInstance`;
|
||||
/// 1 → `Resolved`; >1 → `Ambiguous`.
|
||||
/// - Rigid-var `type_`: filter the candidate set against the active
|
||||
/// declared-constraint list. 1 survivor → `Resolved`; otherwise
|
||||
/// → `MissingConstraint`.
|
||||
pub fn refine_multi_candidate_residual(
|
||||
residual: &ResidualConstraint,
|
||||
declared_constraints: &[ailang_core::ast::Constraint],
|
||||
registry: &BTreeMap<(String, String), ()>,
|
||||
) -> RefineOutcome {
|
||||
let candidates = residual
|
||||
.candidates
|
||||
.as_ref()
|
||||
.expect("refine_multi_candidate_residual called on single-candidate residual");
|
||||
|
||||
// Concrete-type path.
|
||||
if !matches!(residual.type_, Type::Var { .. }) {
|
||||
let type_h = ailang_core::canonical::type_hash(&residual.type_);
|
||||
let survivors: Vec<String> = candidates
|
||||
.iter()
|
||||
.filter(|c| registry.contains_key(&((**c).clone(), type_h.clone())))
|
||||
.cloned()
|
||||
.collect();
|
||||
let at_type = ailang_core::pretty::type_to_string(&residual.type_);
|
||||
return match survivors.len() {
|
||||
0 => RefineOutcome::NoInstance {
|
||||
method: residual.method.clone(),
|
||||
at_type,
|
||||
candidate_classes: candidates.iter().cloned().collect(),
|
||||
},
|
||||
1 => RefineOutcome::Resolved(survivors.into_iter().next().unwrap()),
|
||||
_ => {
|
||||
let mut sorted = survivors;
|
||||
sorted.sort();
|
||||
RefineOutcome::Ambiguous {
|
||||
method: residual.method.clone(),
|
||||
at_type,
|
||||
candidate_classes: sorted,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Rigid-var path: filter against declared constraints.
|
||||
let survivors: Vec<String> = candidates
|
||||
.iter()
|
||||
.filter(|c| declared_constraints.iter().any(|dc| &dc.class == *c))
|
||||
.cloned()
|
||||
.collect();
|
||||
match survivors.len() {
|
||||
1 => RefineOutcome::Resolved(survivors.into_iter().next().unwrap()),
|
||||
_ => RefineOutcome::MissingConstraint {
|
||||
method: residual.method.clone(),
|
||||
candidate_classes: candidates.iter().cloned().collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 23.4: per-call-site polymorphic-free-fn observation recorded by
|
||||
@@ -1793,6 +2159,27 @@ pub(crate) fn qualify_class_ref_in_check(class_ref: &str, caller_module: &str) -
|
||||
}
|
||||
}
|
||||
|
||||
/// mq.2: split a `Term::Var.name` into `(method_name,
|
||||
/// optional_qualifier_prefix)` at the last dot.
|
||||
///
|
||||
/// - `"show"` → `("show", None)`
|
||||
/// - `"prelude.Show.show"` → `("show", Some("prelude.Show"))`
|
||||
/// - `"std_list.length"` → `("length", Some("std_list"))`
|
||||
///
|
||||
/// Callers gate on the qualifier shape: a class qualifier is always
|
||||
/// qualified per mq.1's canonical-form rule, so a single-segment
|
||||
/// prefix (no dot) is structurally a module name and the caller falls
|
||||
/// through to the cross-module-fn path. A multi-segment prefix
|
||||
/// (`<module>.<Class>`) signals an explicit class qualifier on a
|
||||
/// class-method call site.
|
||||
pub(crate) fn parse_method_qualifier(name: &str) -> (&str, Option<String>) {
|
||||
if let Some(dot_idx) = name.rfind('.') {
|
||||
(&name[dot_idx + 1..], Some(name[..dot_idx].to_string()))
|
||||
} else {
|
||||
(name, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 22b.2 (Task 9): expand a list of declared class constraints
|
||||
/// with their one-step superclass closure (Decision 11). For every
|
||||
/// declared `(C, t)`, append `(S, t)` if class `C` has a superclass
|
||||
@@ -2025,18 +2412,92 @@ pub(crate) fn synth(
|
||||
.filter(|m| m.contains_key(name))
|
||||
.map(|_| (env.current_module.clone(), name.clone()));
|
||||
(t.clone(), owner)
|
||||
} else if let Some(cm) = env.class_methods.get(name) {
|
||||
// Iter 22b.2 (Task 9): instantiate the class param with
|
||||
// a fresh metavar; the body's unification at the call
|
||||
// site fills it in, and the residual is the class
|
||||
// constraint we owe at this use site.
|
||||
} else if {
|
||||
// mq.2 dispatch entry: the Var arm matches a class
|
||||
// method either bare (`"show"`) or qualified
|
||||
// (`"prelude.Show.show"`). `parse_method_qualifier`
|
||||
// splits at the last dot; a `Some(qualifier)` with no
|
||||
// inner dot is a 1-dot name (`std_list.length`) —
|
||||
// structurally a cross-module-fn call per mq.1's
|
||||
// canonical-form rule (class qualifiers are always
|
||||
// `<module>.<Class>`), so we fall through to the
|
||||
// qualified-fn arm below in that case.
|
||||
let (method_name, qualifier_opt) = parse_method_qualifier(name);
|
||||
let qualifier_is_class_shape = match &qualifier_opt {
|
||||
None => true,
|
||||
Some(q) => q.contains('.'),
|
||||
};
|
||||
qualifier_is_class_shape
|
||||
&& env.method_to_candidate_classes.contains_key(method_name)
|
||||
} {
|
||||
// mq.2: type-driven dispatch entry. The Var arm runs
|
||||
// before App-arm unification, so the arg type is not
|
||||
// available here. The dispatch helper resolves on
|
||||
// (a) explicit class qualifier, (b) singleton candidate
|
||||
// set (today's invariant), or (c) constraint-driven
|
||||
// filter (rigid-var case); otherwise it emits `Multi`
|
||||
// for discharge-time refinement.
|
||||
//
|
||||
// ct.2 Task 1: a class method's declared type lives in
|
||||
// its defining module's bare-local namespace.
|
||||
// qualify_local_types runs symmetrically with the
|
||||
// Term::Var qualified path below so the consumer's
|
||||
// typecheck context sees the method's return /
|
||||
// parameter Type::Cons fully qualified.
|
||||
// With `MethodNameCollision` still active (pre-mq.3),
|
||||
// real workspaces never reach the `Multi` branch — the
|
||||
// candidate set is always singleton. The branch is
|
||||
// installed here for mq.3 to exercise without further
|
||||
// dispatch-side edits.
|
||||
let (method_name, qualifier_opt) = parse_method_qualifier(name);
|
||||
let candidates = env
|
||||
.method_to_candidate_classes
|
||||
.get(method_name)
|
||||
.expect("contains_key invariant: gate above guarantees presence");
|
||||
|
||||
// Build a unit-view of the workspace registry keyed by
|
||||
// (qualified_class, type_hash). Values are unit; only
|
||||
// key presence matters for the type-driven filter.
|
||||
let registry_unit: BTreeMap<(String, String), ()> = env
|
||||
.workspace_registry
|
||||
.entries
|
||||
.keys()
|
||||
.map(|k| (k.clone(), ()))
|
||||
.collect();
|
||||
|
||||
let outcome = resolve_method_dispatch(
|
||||
method_name,
|
||||
qualifier_opt.as_deref(),
|
||||
candidates,
|
||||
/*concrete_arg_type*/ None,
|
||||
/*declared_constraints*/ &[],
|
||||
/*registry*/ ®istry_unit,
|
||||
);
|
||||
|
||||
let (residual_class, residual_candidates) = match outcome {
|
||||
MethodDispatchOutcome::Resolved(class) => (class, None),
|
||||
MethodDispatchOutcome::Multi { candidates: cs, method: _ } => {
|
||||
// Tentative class: first BTreeSet element.
|
||||
// Refinement at discharge / mono time overwrites it.
|
||||
let tentative = cs.iter().next().cloned().unwrap_or_default();
|
||||
(tentative, Some(cs))
|
||||
}
|
||||
MethodDispatchOutcome::UnknownClass(qname) => {
|
||||
return Err(CheckError::UnknownClass { name: qname });
|
||||
}
|
||||
MethodDispatchOutcome::Ambiguous { method, at_type, candidates: cs } => {
|
||||
return Err(CheckError::AmbiguousMethodResolution {
|
||||
method,
|
||||
at_type,
|
||||
candidate_classes: cs,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Look up the `ClassMethodEntry` by method name —
|
||||
// workspace-flat. The `MethodNameCollision` invariant
|
||||
// (pre-mq.3) guarantees the lookup is unambiguous. Post-
|
||||
// mq.3, the class_methods table is keyed by
|
||||
// `(class, method)` and this `.get(method_name)` will
|
||||
// need to be reworked alongside the table.
|
||||
let cm = env.class_methods.get(method_name).expect(
|
||||
"method_to_candidate_classes invariant: method present \
|
||||
implies class_methods entry",
|
||||
);
|
||||
let owner_types = env
|
||||
.module_types
|
||||
.get(&cm.defining_module)
|
||||
@@ -2049,9 +2510,10 @@ pub(crate) fn synth(
|
||||
mapping.insert(cm.class_param.clone(), fresh.clone());
|
||||
let inst_ty = substitute_rigids(&qualified_method_ty, &mapping);
|
||||
residuals.push(ResidualConstraint {
|
||||
class: cm.class_name.clone(),
|
||||
class: residual_class,
|
||||
type_: fresh,
|
||||
method: name.clone(),
|
||||
method: method_name.to_string(),
|
||||
candidates: residual_candidates,
|
||||
});
|
||||
return Ok(inst_ty);
|
||||
} else if let Some((owner_module, raw_ty)) = env
|
||||
@@ -2902,6 +3364,13 @@ pub struct Env {
|
||||
/// method-name-collision check (Iter 22b.2.6), so the merge is
|
||||
/// safe (no overwrites). Read in [`synth`]'s `Term::Var` arm.
|
||||
pub class_methods: BTreeMap<String, ClassMethodEntry>,
|
||||
/// mq.2: workspace-flat inverse of [`Self::class_methods`] — for
|
||||
/// each method name, the set of qualified class names that declare
|
||||
/// it. Pre-mq.3, the `MethodNameCollision` invariant guarantees
|
||||
/// each set is singleton; mq.3 lifts the invariant and cardinality
|
||||
/// > 1 becomes legal. Synth's `Term::Var` arm consults this index
|
||||
/// for type-driven dispatch (spec §Architecture's 5-step rule).
|
||||
pub method_to_candidate_classes: BTreeMap<String, BTreeSet<String>>,
|
||||
/// Iter 22b.2 (Task 9): one-step superclass expansion table.
|
||||
/// Maps each class name to its superclass class name. Absence from
|
||||
/// the map means the class has no superclass — populated only for
|
||||
@@ -5302,4 +5771,249 @@ mod tests {
|
||||
"expected bare `dbl` to resolve through implicit prelude import; got {diags:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// mq.2.1: `AmbiguousMethodResolution` is the new check-time
|
||||
/// diagnostic for multi-candidate residuals that survive type-driven
|
||||
/// filtering at a monomorphic call site. Verifies the variant is
|
||||
/// constructible, its `code()` returns the structured-diagnostic key,
|
||||
/// and the Display surface names the candidate classes.
|
||||
#[test]
|
||||
fn mq2_ambiguous_method_resolution_display_and_code() {
|
||||
let err = CheckError::AmbiguousMethodResolution {
|
||||
method: "show".to_string(),
|
||||
at_type: "Int".to_string(),
|
||||
candidate_classes: vec![
|
||||
"prelude.Show".to_string(),
|
||||
"userlib.Show".to_string(),
|
||||
],
|
||||
};
|
||||
assert_eq!(err.code(), "ambiguous-method-resolution");
|
||||
let rendered = format!("{}", err);
|
||||
assert!(rendered.contains("show"), "Display must echo method, got: {rendered}");
|
||||
assert!(rendered.contains("prelude.Show"), "Display must name candidate, got: {rendered}");
|
||||
assert!(rendered.contains("userlib.Show"), "Display must name candidate, got: {rendered}");
|
||||
}
|
||||
|
||||
/// mq.2.1: `UnknownClass` is the new check-time diagnostic for an
|
||||
/// explicit class qualifier in `Term::Var.name` that names a
|
||||
/// qualified class not in the workspace.
|
||||
#[test]
|
||||
fn mq2_unknown_class_display_and_code() {
|
||||
let err = CheckError::UnknownClass {
|
||||
name: "unknownlib.Show".to_string(),
|
||||
};
|
||||
assert_eq!(err.code(), "unknown-class");
|
||||
let rendered = format!("{}", err);
|
||||
assert!(rendered.contains("unknownlib.Show"), "Display must echo the qualified name, got: {rendered}");
|
||||
}
|
||||
|
||||
/// mq.2.2: `NoInstance` carries an additive `candidate_classes`
|
||||
/// list. When non-empty, the rendered ctx() carries the candidate
|
||||
/// list; when empty, the ctx() shape is unchanged from pre-mq.2
|
||||
/// form (backwards compatible for single-class call sites).
|
||||
#[test]
|
||||
fn mq2_no_instance_with_candidate_classes_renders() {
|
||||
let err = CheckError::NoInstance {
|
||||
def: "f".to_string(),
|
||||
class: "prelude.Show".to_string(),
|
||||
method: "show".to_string(),
|
||||
at_type: "MyType".to_string(),
|
||||
candidate_classes: vec![
|
||||
"prelude.Show".to_string(),
|
||||
"userlib.Show".to_string(),
|
||||
],
|
||||
};
|
||||
let rendered = format!("{}", err);
|
||||
assert!(rendered.contains("show"), "Display must echo method, got: {rendered}");
|
||||
assert!(rendered.contains("MyType"), "Display must echo type, got: {rendered}");
|
||||
let ctx = err.ctx();
|
||||
assert_eq!(ctx["candidate_classes"][0], "prelude.Show");
|
||||
assert_eq!(ctx["candidate_classes"][1], "userlib.Show");
|
||||
}
|
||||
|
||||
/// mq.2.2: empty `candidate_classes` keeps the existing single-class
|
||||
/// ctx() shape — no `candidate_classes` key in the JSON.
|
||||
#[test]
|
||||
fn mq2_no_instance_empty_candidates_back_compat() {
|
||||
let err = CheckError::NoInstance {
|
||||
def: "f".to_string(),
|
||||
class: "prelude.Show".to_string(),
|
||||
method: "show".to_string(),
|
||||
at_type: "MyType".to_string(),
|
||||
candidate_classes: vec![],
|
||||
};
|
||||
let rendered = format!("{}", err);
|
||||
assert!(rendered.contains("prelude.Show"), "Display must echo class, got: {rendered}");
|
||||
let ctx = err.ctx();
|
||||
assert!(ctx.get("candidate_classes").is_none(),
|
||||
"empty candidate_classes must not render in ctx; got: {ctx}");
|
||||
}
|
||||
|
||||
/// mq.2.3: `ResidualConstraint` carries an optional candidate-class
|
||||
/// set for the multi-candidate dispatch path. `None` preserves the
|
||||
/// pre-mq.2 single-class semantics (the `class` field is the
|
||||
/// resolved class). `Some(...)` carries the candidate set; the
|
||||
/// `class` field's content is a tentative value until discharge
|
||||
/// resolves it.
|
||||
#[test]
|
||||
fn mq2_residual_constraint_candidates_field_exists() {
|
||||
let single = ResidualConstraint {
|
||||
class: "prelude.Eq".to_string(),
|
||||
type_: Type::Con { name: "Int".to_string(), args: vec![] },
|
||||
method: "eq".to_string(),
|
||||
candidates: None,
|
||||
};
|
||||
assert!(single.candidates.is_none());
|
||||
|
||||
let mut multi_set = std::collections::BTreeSet::new();
|
||||
multi_set.insert("prelude.Show".to_string());
|
||||
multi_set.insert("userlib.Show".to_string());
|
||||
let multi = ResidualConstraint {
|
||||
class: "prelude.Show".to_string(),
|
||||
type_: Type::Con { name: "Int".to_string(), args: vec![] },
|
||||
method: "show".to_string(),
|
||||
candidates: Some(multi_set.clone()),
|
||||
};
|
||||
assert_eq!(multi.candidates, Some(multi_set));
|
||||
}
|
||||
|
||||
/// mq.2.5: smoke test on `parse_method_qualifier` — the helper
|
||||
/// underpinning the rewritten synth Var-arm. Bare names pass
|
||||
/// through with `None` qualifier; dotted names split at the last
|
||||
/// dot so the prefix is the (possibly-multi-dot) class qualifier.
|
||||
#[test]
|
||||
fn mq2_parse_method_qualifier_smoke() {
|
||||
assert_eq!(parse_method_qualifier("show"), ("show", None));
|
||||
assert_eq!(
|
||||
parse_method_qualifier("prelude.Show.show"),
|
||||
("show", Some("prelude.Show".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_method_qualifier("std_list.length"),
|
||||
("length", Some("std_list".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
/// mq.2.7: a multi-candidate residual with a concrete `type_`
|
||||
/// resolved post-unification to `Int` is filtered against the
|
||||
/// registry. Single survivor → Resolved.
|
||||
#[test]
|
||||
fn mq2_discharge_multi_candidate_concrete_type_single_survivor() {
|
||||
let mut candidates = BTreeSet::new();
|
||||
candidates.insert("prelude.Show".to_string());
|
||||
candidates.insert("userlib.Show".to_string());
|
||||
let residual = ResidualConstraint {
|
||||
class: "prelude.Show".to_string(), // tentative
|
||||
type_: Type::Con { name: "Int".to_string(), args: vec![] },
|
||||
method: "show".to_string(),
|
||||
candidates: Some(candidates),
|
||||
};
|
||||
let mut registry: BTreeMap<(String, String), ()> = BTreeMap::new();
|
||||
let int_h = ailang_core::canonical::type_hash(
|
||||
&Type::Con { name: "Int".to_string(), args: vec![] }
|
||||
);
|
||||
registry.insert(("prelude.Show".to_string(), int_h), ());
|
||||
|
||||
let outcome = refine_multi_candidate_residual(
|
||||
&residual,
|
||||
/*declared_constraints*/ &[],
|
||||
/*registry*/ ®istry,
|
||||
);
|
||||
assert_eq!(outcome, RefineOutcome::Resolved("prelude.Show".to_string()));
|
||||
}
|
||||
|
||||
/// mq.2.7: zero registry survivors → NoInstance with the full
|
||||
/// candidate-class set echoed back to the author.
|
||||
#[test]
|
||||
fn mq2_discharge_multi_candidate_concrete_type_zero_survivors_no_instance() {
|
||||
let mut candidates = BTreeSet::new();
|
||||
candidates.insert("prelude.Show".to_string());
|
||||
candidates.insert("userlib.Show".to_string());
|
||||
let residual = ResidualConstraint {
|
||||
class: "prelude.Show".to_string(),
|
||||
type_: Type::Con { name: "MyType".to_string(), args: vec![] },
|
||||
method: "show".to_string(),
|
||||
candidates: Some(candidates.clone()),
|
||||
};
|
||||
let registry: BTreeMap<(String, String), ()> = BTreeMap::new();
|
||||
let outcome = refine_multi_candidate_residual(&residual, &[], ®istry);
|
||||
match outcome {
|
||||
RefineOutcome::NoInstance { candidate_classes, .. } => {
|
||||
assert_eq!(candidate_classes.len(), 2);
|
||||
}
|
||||
other => panic!("expected NoInstance, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// mq.2.7: multi survivors → AmbiguousMethodResolution.
|
||||
#[test]
|
||||
fn mq2_discharge_multi_candidate_concrete_type_multi_survivors_ambiguous() {
|
||||
let mut candidates = BTreeSet::new();
|
||||
candidates.insert("prelude.Show".to_string());
|
||||
candidates.insert("userlib.Show".to_string());
|
||||
let residual = ResidualConstraint {
|
||||
class: "prelude.Show".to_string(),
|
||||
type_: Type::Con { name: "Int".to_string(), args: vec![] },
|
||||
method: "show".to_string(),
|
||||
candidates: Some(candidates.clone()),
|
||||
};
|
||||
let mut registry: BTreeMap<(String, String), ()> = BTreeMap::new();
|
||||
let int_h = ailang_core::canonical::type_hash(
|
||||
&Type::Con { name: "Int".to_string(), args: vec![] }
|
||||
);
|
||||
registry.insert(("prelude.Show".to_string(), int_h.clone()), ());
|
||||
registry.insert(("userlib.Show".to_string(), int_h), ());
|
||||
let outcome = refine_multi_candidate_residual(&residual, &[], ®istry);
|
||||
match outcome {
|
||||
RefineOutcome::Ambiguous { candidate_classes, .. } => {
|
||||
assert_eq!(candidate_classes.len(), 2);
|
||||
}
|
||||
other => panic!("expected Ambiguous, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// mq.2.4: `Env` carries a workspace-flat
|
||||
/// `method_to_candidate_classes: BTreeMap<String, BTreeSet<String>>`
|
||||
/// inverse map of `class_methods`. Today's `MethodNameCollision`
|
||||
/// invariant guarantees each set is singleton; post-mq.3 the
|
||||
/// cardinality > 1 case becomes legal.
|
||||
#[test]
|
||||
fn mq2_env_method_to_candidate_classes_built() {
|
||||
use ailang_core::ast::{ClassDef, ClassMethod, Module};
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "m".to_string(),
|
||||
imports: vec![],
|
||||
defs: vec![Def::Class(ClassDef {
|
||||
name: "MyShow".to_string(),
|
||||
param: "a".to_string(),
|
||||
superclass: None,
|
||||
methods: vec![ClassMethod {
|
||||
name: "myshow".to_string(),
|
||||
ty: Type::Fn {
|
||||
params: vec![Type::Var { name: "a".to_string() }],
|
||||
param_modes: vec![ParamMode::Borrow],
|
||||
ret: Box::new(Type::Con { name: "Str".to_string(), args: vec![] }),
|
||||
effects: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
default: None,
|
||||
}],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("m".into(), m);
|
||||
let ws = Workspace {
|
||||
entry: "m".into(),
|
||||
modules,
|
||||
root_dir: std::path::PathBuf::from("."),
|
||||
registry: ailang_core::workspace::Registry::default(),
|
||||
};
|
||||
let env = build_check_env(&ws);
|
||||
let candidates = env.method_to_candidate_classes.get("myshow")
|
||||
.expect("method_to_candidate_classes must contain `myshow`");
|
||||
assert!(candidates.contains("m.MyShow"), "candidates: {candidates:?}");
|
||||
assert_eq!(candidates.len(), 1, "MethodNameCollision invariant: singleton");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1108,6 +1108,27 @@ fn pattern_binders(pat: &Pattern) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// mq.2: resolve a residual's class for mono target construction.
|
||||
/// Returns `Some(class)` for a discharge-ready residual (single-class
|
||||
/// or refined-multi-candidate); `None` if the multi-candidate residual
|
||||
/// cannot be refined — in which case typecheck-side already raised a
|
||||
/// `CheckError`, so reaching this branch means the residual slipped
|
||||
/// past discharge and the mono cursor should emit a None-slot
|
||||
/// defensively.
|
||||
pub fn resolve_residual_class_for_mono(
|
||||
r: &crate::ResidualConstraint,
|
||||
registry_unit: &BTreeMap<(String, String), ()>,
|
||||
) -> Option<String> {
|
||||
if r.candidates.is_some() {
|
||||
match crate::refine_multi_candidate_residual(r, &[], registry_unit) {
|
||||
crate::RefineOutcome::Resolved(c) => Some(c),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
Some(r.class.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 22b.3: traversal-ordered residual collection — used by
|
||||
/// the rewrite walker to align cursor positions. Unlike
|
||||
/// [`collect_mono_targets`], this includes non-concrete residuals
|
||||
@@ -1185,10 +1206,33 @@ pub(crate) fn collect_residuals_ordered(
|
||||
let r_ty_norm = env
|
||||
.workspace_registry
|
||||
.normalize_type_for_lookup(module_name, &r_ty);
|
||||
let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty_norm));
|
||||
// mq.2: resolve the residual's class via the multi-
|
||||
// candidate refinement helper. Single-class residuals
|
||||
// (`candidates: None`) flow through `r.class.clone()`
|
||||
// unchanged; multi-candidate residuals filter against
|
||||
// the registry, with the discharge path's CheckError
|
||||
// already raised at typecheck time (so reaching here
|
||||
// with a non-Resolved outcome means the residual was
|
||||
// somehow not gated upstream — emit `None` defensively).
|
||||
let registry_unit: BTreeMap<(String, String), ()> = env
|
||||
.workspace_registry
|
||||
.entries
|
||||
.keys()
|
||||
.map(|k| (k.clone(), ()))
|
||||
.collect();
|
||||
let normalized_residual = crate::ResidualConstraint {
|
||||
class: r.class.clone(),
|
||||
type_: r_ty_norm.clone(),
|
||||
method: r.method.clone(),
|
||||
candidates: r.candidates.clone(),
|
||||
};
|
||||
let resolved_class =
|
||||
resolve_residual_class_for_mono(&normalized_residual, ®istry_unit)?;
|
||||
|
||||
let key = (resolved_class.clone(), ailang_core::canonical::type_hash(&r_ty_norm));
|
||||
let entry = env.workspace_registry.entries.get(&key)?;
|
||||
Some(MonoTarget::ClassMethod {
|
||||
class: r.class.clone(),
|
||||
class: resolved_class,
|
||||
method: r.method.clone(),
|
||||
type_: r_ty,
|
||||
defining_module: entry.defining_module.clone(),
|
||||
@@ -1370,3 +1414,67 @@ fn interleave_slots(
|
||||
Term::Lit { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ailang_core::ast::Type;
|
||||
|
||||
/// mq.2.8: mono's residual-class resolver refines multi-candidate
|
||||
/// residuals via the same logic as discharge. A multi-candidate
|
||||
/// residual with a concrete `type_` and a single registry-survivor
|
||||
/// resolves to that class.
|
||||
#[test]
|
||||
fn mq2_mono_multi_candidate_resolves_to_single_class() {
|
||||
let mut candidates = std::collections::BTreeSet::new();
|
||||
candidates.insert("prelude.Show".to_string());
|
||||
candidates.insert("userlib.Show".to_string());
|
||||
let residual = crate::ResidualConstraint {
|
||||
class: "prelude.Show".to_string(), // tentative
|
||||
type_: Type::Con { name: "Int".to_string(), args: vec![] },
|
||||
method: "show".to_string(),
|
||||
candidates: Some(candidates),
|
||||
};
|
||||
let mut registry_unit: BTreeMap<(String, String), ()> = Default::default();
|
||||
let int_h = ailang_core::canonical::type_hash(
|
||||
&Type::Con { name: "Int".to_string(), args: vec![] }
|
||||
);
|
||||
registry_unit.insert(("prelude.Show".to_string(), int_h), ());
|
||||
|
||||
let resolved = resolve_residual_class_for_mono(&residual, ®istry_unit);
|
||||
assert_eq!(resolved, Some("prelude.Show".to_string()));
|
||||
}
|
||||
|
||||
/// mq.2.8: single-class residual (candidates: None) flows through
|
||||
/// unchanged.
|
||||
#[test]
|
||||
fn mq2_mono_single_class_residual_unchanged() {
|
||||
let residual = crate::ResidualConstraint {
|
||||
class: "prelude.Eq".to_string(),
|
||||
type_: Type::Con { name: "Int".to_string(), args: vec![] },
|
||||
method: "eq".to_string(),
|
||||
candidates: None,
|
||||
};
|
||||
let registry_unit: BTreeMap<(String, String), ()> = Default::default();
|
||||
let resolved = resolve_residual_class_for_mono(&residual, ®istry_unit);
|
||||
assert_eq!(resolved, Some("prelude.Eq".to_string()));
|
||||
}
|
||||
|
||||
/// mq.2.8: multi-candidate residual that cannot refine (zero
|
||||
/// registry survivors) returns None.
|
||||
#[test]
|
||||
fn mq2_mono_multi_candidate_no_survivors_returns_none() {
|
||||
let mut candidates = std::collections::BTreeSet::new();
|
||||
candidates.insert("prelude.Show".to_string());
|
||||
candidates.insert("userlib.Show".to_string());
|
||||
let residual = crate::ResidualConstraint {
|
||||
class: "prelude.Show".to_string(),
|
||||
type_: Type::Con { name: "MyType".to_string(), args: vec![] },
|
||||
method: "show".to_string(),
|
||||
candidates: Some(candidates),
|
||||
};
|
||||
let registry_unit: BTreeMap<(String, String), ()> = Default::default();
|
||||
let resolved = resolve_residual_class_for_mono(&residual, ®istry_unit);
|
||||
assert_eq!(resolved, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//! mq.2.5: pin tests on `resolve_method_dispatch` — the new
|
||||
//! dispatch-resolution helper that synth's `Term::Var` arm consults
|
||||
//! per the spec's 5-step rule.
|
||||
//!
|
||||
//! Six cases:
|
||||
//! 1. Unique candidate.
|
||||
//! 2. Multi + explicit qualifier matching one.
|
||||
//! 3. Multi + explicit qualifier matching none → UnknownClass.
|
||||
//! 4. Multi + type-driven filter narrows to one.
|
||||
//! 5. Multi + constraint-driven filter narrows to one (rigid var case).
|
||||
//! 6. Multi + true ambiguity → AmbiguousMethodResolution.
|
||||
|
||||
use ailang_check::{resolve_method_dispatch, MethodDispatchOutcome};
|
||||
use ailang_core::ast::{Constraint, Type};
|
||||
use ailang_core::canonical;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
fn registry_with(entries: &[(&str, &str)]) -> BTreeMap<(String, String), ()> {
|
||||
// Returns a synthetic registry: keys are (qualified_class, type_hash)
|
||||
// values are unit (instance presence flag).
|
||||
let mut reg = BTreeMap::new();
|
||||
for (cls, ty_name) in entries {
|
||||
let t = Type::Con { name: ty_name.to_string(), args: vec![] };
|
||||
let h = canonical::type_hash(&t);
|
||||
reg.insert((cls.to_string(), h), ());
|
||||
}
|
||||
reg
|
||||
}
|
||||
|
||||
fn candidates_of(classes: &[&str]) -> BTreeSet<String> {
|
||||
classes.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Case 1: unique candidate → returns that class.
|
||||
#[test]
|
||||
fn case1_unique_candidate_resolves() {
|
||||
let candidates = candidates_of(&["prelude.Eq"]);
|
||||
let registry = registry_with(&[("prelude.Eq", "Int")]);
|
||||
let arg_ty = Type::Con { name: "Int".to_string(), args: vec![] };
|
||||
let result = resolve_method_dispatch(
|
||||
/*method*/ "eq",
|
||||
/*qualifier_prefix*/ None,
|
||||
/*candidates*/ &candidates,
|
||||
/*concrete_arg_type*/ Some(&arg_ty),
|
||||
/*declared_constraints*/ &[],
|
||||
/*registry*/ ®istry,
|
||||
);
|
||||
assert_eq!(result, MethodDispatchOutcome::Resolved("prelude.Eq".to_string()));
|
||||
}
|
||||
|
||||
/// Case 2: multi + explicit qualifier matching one → returns that class.
|
||||
#[test]
|
||||
fn case2_qualifier_matches_one() {
|
||||
let candidates = candidates_of(&["prelude.Show", "userlib.Show"]);
|
||||
let registry = registry_with(&[
|
||||
("prelude.Show", "Int"),
|
||||
("userlib.Show", "Int"),
|
||||
]);
|
||||
let arg_ty = Type::Con { name: "Int".to_string(), args: vec![] };
|
||||
let result = resolve_method_dispatch(
|
||||
"show",
|
||||
Some("userlib.Show"),
|
||||
&candidates,
|
||||
Some(&arg_ty),
|
||||
&[],
|
||||
®istry,
|
||||
);
|
||||
assert_eq!(result, MethodDispatchOutcome::Resolved("userlib.Show".to_string()));
|
||||
}
|
||||
|
||||
/// Case 3: multi + explicit qualifier matching none → UnknownClass.
|
||||
#[test]
|
||||
fn case3_qualifier_matches_none_unknown_class() {
|
||||
let candidates = candidates_of(&["prelude.Show", "userlib.Show"]);
|
||||
let registry = registry_with(&[("prelude.Show", "Int")]);
|
||||
let arg_ty = Type::Con { name: "Int".to_string(), args: vec![] };
|
||||
let result = resolve_method_dispatch(
|
||||
"show",
|
||||
Some("nonexistent.Show"),
|
||||
&candidates,
|
||||
Some(&arg_ty),
|
||||
&[],
|
||||
®istry,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
MethodDispatchOutcome::UnknownClass("nonexistent.Show".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Case 4: multi + type-driven filter narrows to one → returns that class.
|
||||
#[test]
|
||||
fn case4_type_driven_filter_narrows_to_one() {
|
||||
let candidates = candidates_of(&["prelude.Show", "userlib.Show"]);
|
||||
let registry = registry_with(&[("prelude.Show", "Int")]); // only prelude.Show has Show Int
|
||||
let arg_ty = Type::Con { name: "Int".to_string(), args: vec![] };
|
||||
let result = resolve_method_dispatch(
|
||||
"show",
|
||||
None,
|
||||
&candidates,
|
||||
Some(&arg_ty),
|
||||
&[],
|
||||
®istry,
|
||||
);
|
||||
assert_eq!(result, MethodDispatchOutcome::Resolved("prelude.Show".to_string()));
|
||||
}
|
||||
|
||||
/// Case 5: multi + constraint-driven filter narrows to one (rigid var) → returns that class.
|
||||
#[test]
|
||||
fn case5_constraint_driven_filter_narrows_to_one() {
|
||||
let candidates = candidates_of(&["prelude.Show", "userlib.Show"]);
|
||||
let registry = registry_with(&[]);
|
||||
let rigid_a = Type::Var { name: "a".to_string() };
|
||||
let declared = vec![Constraint {
|
||||
class: "prelude.Show".to_string(),
|
||||
type_: Type::Var { name: "a".to_string() },
|
||||
}];
|
||||
let result = resolve_method_dispatch(
|
||||
"show",
|
||||
None,
|
||||
&candidates,
|
||||
Some(&rigid_a), // rigid var, can't drive registry lookup
|
||||
&declared,
|
||||
®istry,
|
||||
);
|
||||
assert_eq!(result, MethodDispatchOutcome::Resolved("prelude.Show".to_string()));
|
||||
}
|
||||
|
||||
/// Case 6: multi + true ambiguity → AmbiguousMethodResolution.
|
||||
#[test]
|
||||
fn case6_true_ambiguity() {
|
||||
let candidates = candidates_of(&["prelude.Show", "userlib.Show"]);
|
||||
let registry = registry_with(&[
|
||||
("prelude.Show", "Int"),
|
||||
("userlib.Show", "Int"),
|
||||
]);
|
||||
let arg_ty = Type::Con { name: "Int".to_string(), args: vec![] };
|
||||
let result = resolve_method_dispatch(
|
||||
"show",
|
||||
None,
|
||||
&candidates,
|
||||
Some(&arg_ty),
|
||||
&[],
|
||||
®istry,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
MethodDispatchOutcome::Ambiguous {
|
||||
method: "show".to_string(),
|
||||
at_type: "Int".to_string(),
|
||||
candidates: vec!["prelude.Show".to_string(), "userlib.Show".to_string()],
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user