iter ct.2.1: qualify_local_types on class-method channel + Forall constraints recursion
This commit is contained in:
@@ -1806,10 +1806,24 @@ pub(crate) fn synth(
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
let owner_types = env
|
||||
.module_types
|
||||
.get(&cm.defining_module)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let qualified_method_ty =
|
||||
qualify_local_types(&cm.method_ty, &cm.defining_module, &owner_types);
|
||||
let fresh = Subst::fresh(counter);
|
||||
let mut mapping: BTreeMap<String, Type> = BTreeMap::new();
|
||||
mapping.insert(cm.class_param.clone(), fresh.clone());
|
||||
let inst_ty = substitute_rigids(&cm.method_ty, &mapping);
|
||||
let inst_ty = substitute_rigids(&qualified_method_ty, &mapping);
|
||||
residuals.push(ResidualConstraint {
|
||||
class: cm.class_name.clone(),
|
||||
type_: fresh,
|
||||
@@ -2417,7 +2431,13 @@ fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap<Stri
|
||||
},
|
||||
Type::Forall { vars, constraints, body } => Type::Forall {
|
||||
vars: vars.clone(),
|
||||
constraints: constraints.clone(),
|
||||
constraints: constraints
|
||||
.iter()
|
||||
.map(|c| Constraint {
|
||||
class: c.class.clone(),
|
||||
type_: qualify_local_types(&c.type_, owner_module, local_types),
|
||||
})
|
||||
.collect(),
|
||||
body: Box::new(qualify_local_types(body, owner_module, local_types)),
|
||||
},
|
||||
Type::Var { .. } => t.clone(),
|
||||
@@ -3969,6 +3989,213 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// ct.2 Task 1: a class method whose declared return type is a
|
||||
/// bare local Type::Con in the defining module (e.g.
|
||||
/// `compare : a -> a -> Ordering` declared inside `prelude`) must
|
||||
/// be presented to a consumer module's typecheck context with the
|
||||
/// return type qualified (`prelude.Ordering`). Without this, the
|
||||
/// consumer's `Pattern::Ctor LT` against the scrutinee no longer
|
||||
/// resolves once the post-ct.1 canonical form removes the
|
||||
/// imports-fallback that previously papered over the mismatch.
|
||||
#[test]
|
||||
fn ct2_class_method_cross_module_qualifies_return_type() {
|
||||
// Defining module declares `Ordering` locally and a class
|
||||
// `Ord` with a method `compare : a -> a -> Ordering`. The
|
||||
// `Ordering` Type::Con is bare-local in the class method's ty,
|
||||
// which is canonical post-ct.1 (bare = local to defining module).
|
||||
let prelude = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "p".into(),
|
||||
imports: vec![],
|
||||
defs: vec![
|
||||
Def::Type(TypeDef {
|
||||
name: "Ordering".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![
|
||||
Ctor { name: "LT".into(), fields: vec![] },
|
||||
Ctor { name: "EQ".into(), fields: vec![] },
|
||||
Ctor { name: "GT".into(), fields: vec![] },
|
||||
],
|
||||
doc: None,
|
||||
drop_iterative: false,
|
||||
}),
|
||||
Def::Class(ClassDef {
|
||||
name: "Ord".into(),
|
||||
param: "a".into(),
|
||||
superclass: None,
|
||||
methods: vec![ClassMethod {
|
||||
name: "compare".into(),
|
||||
ty: Type::Forall {
|
||||
vars: vec!["a".into()],
|
||||
constraints: vec![],
|
||||
body: Box::new(Type::Fn {
|
||||
params: vec![
|
||||
Type::Var { name: "a".into() },
|
||||
Type::Var { name: "a".into() },
|
||||
],
|
||||
ret: Box::new(Type::Con {
|
||||
name: "Ordering".into(),
|
||||
args: vec![],
|
||||
}),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
}),
|
||||
},
|
||||
default: None,
|
||||
}],
|
||||
doc: None,
|
||||
}),
|
||||
Def::Instance(InstanceDef {
|
||||
class: "Ord".into(),
|
||||
type_: Type::int(),
|
||||
methods: vec![InstanceMethod {
|
||||
name: "compare".into(),
|
||||
body: Term::Lam {
|
||||
params: vec!["x".into(), "y".into()],
|
||||
param_tys: vec![Type::int(), Type::int()],
|
||||
ret_ty: Box::new(Type::Con {
|
||||
name: "Ordering".into(),
|
||||
args: vec![],
|
||||
}),
|
||||
effects: vec![],
|
||||
body: Box::new(Term::Ctor {
|
||||
type_name: "Ordering".into(),
|
||||
ctor: "EQ".into(),
|
||||
args: vec![],
|
||||
}),
|
||||
},
|
||||
}],
|
||||
doc: None,
|
||||
}),
|
||||
],
|
||||
};
|
||||
// Consumer module imports prelude and calls `compare` (bare).
|
||||
// The return type seen by the consumer must be `p.Ordering`,
|
||||
// not bare `Ordering` — otherwise the match arm below fails to
|
||||
// typecheck because the Pattern::Ctor lookup is type-driven and
|
||||
// would not find `LT` in any TypeDef named bare `Ordering` in
|
||||
// the consumer's local types.
|
||||
let consumer = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "u".into(),
|
||||
imports: vec![Import { module: "p".into(), alias: None }],
|
||||
defs: vec![fn_def(
|
||||
"use_compare",
|
||||
Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
vec![],
|
||||
Term::Match {
|
||||
scrutinee: Box::new(Term::App {
|
||||
callee: Box::new(Term::Var { name: "compare".into() }),
|
||||
args: vec![
|
||||
Term::Lit { lit: Literal::Int { value: 1 } },
|
||||
Term::Lit { lit: Literal::Int { value: 2 } },
|
||||
],
|
||||
tail: false,
|
||||
}),
|
||||
arms: vec![
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "LT".into(),
|
||||
fields: vec![],
|
||||
},
|
||||
body: Term::Lit { lit: Literal::Int { value: 1 } },
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "EQ".into(),
|
||||
fields: vec![],
|
||||
},
|
||||
body: Term::Lit { lit: Literal::Int { value: 2 } },
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "GT".into(),
|
||||
fields: vec![],
|
||||
},
|
||||
body: Term::Lit { lit: Literal::Int { value: 3 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
)],
|
||||
};
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("p".into(), prelude);
|
||||
modules.insert("u".into(), consumer);
|
||||
let ws = Workspace {
|
||||
entry: "u".into(),
|
||||
modules,
|
||||
root_dir: std::path::PathBuf::from("."),
|
||||
registry: ailang_core::workspace::Registry::default(),
|
||||
};
|
||||
let diags = check_workspace(&ws);
|
||||
assert!(
|
||||
diags.is_empty(),
|
||||
"consumer module must typecheck cleanly when calling a class \
|
||||
method whose return type is bare-local in its defining \
|
||||
module; the class-method channel must apply \
|
||||
qualify_local_types just like the qualified Term::Var path \
|
||||
does. Got diagnostics: {:?}",
|
||||
diags
|
||||
);
|
||||
}
|
||||
|
||||
/// ct.2 Task 1: `qualify_local_types`'s `Type::Forall` arm must
|
||||
/// recurse into `constraints[].type_`. A class method whose
|
||||
/// declared type is `Forall<a>{Ord a}. Fn[..]` carries the
|
||||
/// constraint type `Type::Var { name: "a" }` — fine — but a
|
||||
/// hypothetical `Forall<a>{Show p.Foo}. Fn[..]` would carry a
|
||||
/// bare-local `Foo` if the defining module is `p`, and the
|
||||
/// consumer must see `p.Foo` after the cross-module qualifier
|
||||
/// runs. Symmetric to the ct.1.5a-followup fix on
|
||||
/// `normalize_type_for_registry`.
|
||||
#[test]
|
||||
fn ct2_qualify_local_types_recurses_into_forall_constraints() {
|
||||
use crate::qualify_local_types;
|
||||
let mut local_types: IndexMap<String, TypeDef> = IndexMap::new();
|
||||
local_types.insert("Foo".into(), TypeDef {
|
||||
name: "Foo".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![Ctor { name: "MkFoo".into(), fields: vec![] }],
|
||||
doc: None,
|
||||
drop_iterative: false,
|
||||
});
|
||||
let input = Type::Forall {
|
||||
vars: vec!["a".into()],
|
||||
constraints: vec![Constraint {
|
||||
class: "Show".into(),
|
||||
type_: Type::Con { name: "Foo".into(), args: vec![] },
|
||||
}],
|
||||
body: Box::new(Type::Fn {
|
||||
params: vec![Type::Var { name: "a".into() }],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
}),
|
||||
};
|
||||
let out = qualify_local_types(&input, "p", &local_types);
|
||||
match out {
|
||||
Type::Forall { constraints, .. } => {
|
||||
assert_eq!(constraints.len(), 1, "constraint count preserved");
|
||||
match &constraints[0].type_ {
|
||||
Type::Con { name, .. } => assert_eq!(
|
||||
name, "p.Foo",
|
||||
"constraint Type::Con must be qualified by qualify_local_types"
|
||||
),
|
||||
other => panic!("expected Type::Con, got {:?}", other),
|
||||
}
|
||||
}
|
||||
other => panic!("expected Type::Forall, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 14e: a `Term::App { tail: true, .. }` that genuinely sits
|
||||
/// in tail position (as the rhs of a `Seq` that is the body of a
|
||||
/// `Match` arm that is the body of the fn) must pass.
|
||||
|
||||
Reference in New Issue
Block a user