iter 22b.2.4: constraint-references-unbound-type-var diagnostic

This commit is contained in:
2026-05-09 17:54:20 +02:00
parent 4b84e781d4
commit 257b47625d
3 changed files with 108 additions and 0 deletions
+19
View File
@@ -1068,6 +1068,25 @@ fn workspace_error_to_diagnostic(
"got_type": got_type,
})),
),
W::UnboundConstraintTypeVar {
class,
method,
constraint_class,
var,
} => Some(
ailang_check::Diagnostic::error(
"constraint-references-unbound-type-var",
format!(
"in class `{class}` method `{method}`: constraint `{constraint_class} {var}` references unbound type variable `{var}`"
),
)
.with_ctx(serde_json::json!({
"class": class,
"method": method,
"constraint_class": constraint_class,
"var": var,
})),
),
}
}
+59
View File
@@ -230,6 +230,20 @@ pub enum WorkspaceLoadError {
expected_param: String,
got_type: String,
},
/// Iter 22b.2: class-schema validation. A class method's
/// signature contains a constraint referencing a type variable
/// that is neither bound by the method's `Forall.vars` nor equal
/// to the class's `param`.
#[error(
"in class `{class}` method `{method}`: constraint `{constraint_class} {var}` references unbound type variable `{var}`"
)]
UnboundConstraintTypeVar {
class: String,
method: String,
constraint_class: String,
var: String,
},
}
/// Hash over the canonical bytes of a complete module.
@@ -450,6 +464,25 @@ fn validate_classdefs(
});
}
}
for method in &c.methods {
if let Type::Forall { vars, constraints, .. } = &method.ty {
let mut bound: BTreeSet<&str> =
vars.iter().map(String::as_str).collect();
bound.insert(c.param.as_str());
for constr in constraints {
if let Type::Var { name } = &constr.type_ {
if !bound.contains(name.as_str()) {
return Err(WorkspaceLoadError::UnboundConstraintTypeVar {
class: c.name.clone(),
method: method.name.clone(),
constraint_class: constr.class.clone(),
var: name.clone(),
});
}
}
}
}
}
}
}
}
@@ -856,4 +889,30 @@ mod tests {
other => panic!("expected InvalidSuperclassParam, got {other:?}"),
}
}
/// Iter 22b.2: a class method's `Type::Forall` whose `constraints`
/// reference a type variable that is neither bound by `Forall.vars`
/// nor equal to the class's `param` must fire
/// `UnboundConstraintTypeVar`. The fixture's `class Foo a` declares
/// method `foo` with `forall a. (Bar z) => a -> Unit`; `z` is
/// bound nowhere, so the constraint is unsatisfiable by
/// construction.
#[test]
fn constraint_with_unbound_var_fires_unbound_constraint_type_var() {
let entry = std::path::PathBuf::from(
"../../examples/test_22b2_unbound_constraint_var.ail.json",
);
let err = load_workspace(&entry)
.expect_err("must fire constraint-references-unbound-type-var");
match err {
WorkspaceLoadError::UnboundConstraintTypeVar {
class, method, var, ..
} => {
assert_eq!(class, "Foo");
assert_eq!(method, "foo");
assert_eq!(var, "z");
}
other => panic!("expected UnboundConstraintTypeVar, got {other:?}"),
}
}
}
@@ -0,0 +1,30 @@
{
"schema": "ailang/v0",
"name": "test_22b2_unbound_constraint_var",
"imports": [],
"defs": [
{
"kind": "class",
"name": "Foo",
"param": "a",
"methods": [
{
"name": "foo",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [
{ "class": "Bar", "type": { "k": "var", "name": "z" } }
],
"body": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"ret": { "k": "con", "name": "Unit" },
"effects": []
}
}
}
]
}
]
}