iter 22b.2.e2e: cross-module class resolution + multi-fn aggregation
Four new E2E tests in typeclass_22b2.rs covering invariants the per-task fixtures (all single-module, single-fn) did not exercise: - cross_module_class_method_resolves_and_fires_no_instance: module B's call to `show` (declared in module A) reaches the no-instance arm, proving Env.class_methods is merged workspace- wide and not restricted to the local module. - cross_module_polymorphic_fn_without_constraint_fires_missing_constraint: same merge guarantees the missing-constraint arm sees the residual produced by a call to A's class method from inside B. - cross_module_instance_satisfies_concrete_call: the workspace registry built at load time over all modules must populate keys visible from B; a concrete call resolves clean cross-module. - two_fns_each_missing_constraint_produce_two_diagnostics: pins check_in_workspace's per-def aggregation — two ill-formed fns produce two diagnostics (one per fn), not one. Six new fixtures (xmod_* trio + two_fns_*).
This commit is contained in:
@@ -258,3 +258,121 @@ fn fn_calling_class_method_at_type_with_instance_typechecks_green() {
|
||||
diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
/// Property protected: class-method symbols declared in module A are
|
||||
/// reachable from a fn in module B that imports A — i.e. the workspace-
|
||||
/// wide merge of `module_globals.class_methods` into `Env.class_methods`
|
||||
/// (per `check_in_workspace`) is what call-site resolution actually
|
||||
/// consults. Without this merge, a `Term::Var { name: "show" }` in B would
|
||||
/// fire `unbound-var`. The discriminator: when B's `main` calls `show 42`
|
||||
/// at a fully-concrete type and A has NO matching instance, the diagnostic
|
||||
/// must be `no-instance` (proving the class method resolved cross-module),
|
||||
/// NOT `unbound-var` (which would mean cross-module class-method lookup
|
||||
/// failed silently). Fixture: A = `test_22b2_xmod_classmod_noinst` declares
|
||||
/// `class Show a`; B = `test_22b2_xmod_no_instance` calls `show 42`.
|
||||
#[test]
|
||||
fn cross_module_class_method_resolves_and_fires_no_instance() {
|
||||
let entry = examples_dir().join("test_22b2_xmod_no_instance.ail.json");
|
||||
let ws = ailang_core::workspace::load_workspace(&entry)
|
||||
.expect("workspace loads");
|
||||
let diagnostics = ailang_check::check_workspace(&ws);
|
||||
let codes: Vec<&str> = diagnostics.iter().map(|d| d.code.as_str()).collect();
|
||||
assert!(
|
||||
codes.contains(&"no-instance"),
|
||||
"expected no-instance (proving cross-module class-method resolution); got: {:?}",
|
||||
codes
|
||||
);
|
||||
assert!(
|
||||
!codes.contains(&"unbound-var"),
|
||||
"must NOT fire unbound-var — class method `show` from module A is in scope in module B; got: {:?}",
|
||||
codes
|
||||
);
|
||||
}
|
||||
|
||||
/// Property protected: a polymorphic fn in module B calling a class method
|
||||
/// declared in module A (without declaring the matching constraint) MUST
|
||||
/// fire `missing-constraint`. This pins the cross-module class-superclass
|
||||
/// expansion path: B's `Forall.constraints` is empty, A's `class Show a`
|
||||
/// is in `Env.class_methods` via the workspace merge, and the residual
|
||||
/// `(Show, a)` collected at the `show x` call site must reach `check_fn`'s
|
||||
/// missing-constraint arm. Without the cross-module merge, the call would
|
||||
/// produce `unbound-var`; with the merge but a broken superclass-table
|
||||
/// merge, the residual could be spuriously satisfied. Fixture: A =
|
||||
/// `test_22b2_xmod_classmod_noinst`; B = `test_22b2_xmod_missing_constraint`
|
||||
/// declares `forall a. (a) -> Str` and calls `show x`.
|
||||
#[test]
|
||||
fn cross_module_polymorphic_fn_without_constraint_fires_missing_constraint() {
|
||||
let entry = examples_dir().join("test_22b2_xmod_missing_constraint.ail.json");
|
||||
let ws = ailang_core::workspace::load_workspace(&entry)
|
||||
.expect("workspace loads");
|
||||
let diagnostics = ailang_check::check_workspace(&ws);
|
||||
assert!(
|
||||
diagnostics.iter().any(|d| d.code == "missing-constraint"),
|
||||
"expected missing-constraint across modules; got: {:?}",
|
||||
diagnostics.iter().map(|d| &d.code).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
/// Property protected: cross-module instance lookup for the no-instance
|
||||
/// arm. Module A declares `class Show a` AND `instance Show Int`; module
|
||||
/// B imports A and calls `show 42`. The call must typecheck clean — the
|
||||
/// workspace registry built at load time over all modules must include A's
|
||||
/// `instance Show Int`, and B's `check_fn` must find it under
|
||||
/// `(class="Show", type_hash(Int))`. Without cross-module registry
|
||||
/// population, B would spuriously fire `no-instance` at a type for which
|
||||
/// an instance demonstrably exists. Fixture: A =
|
||||
/// `test_22b2_xmod_classmod`; B = `test_22b2_xmod_instance_present`.
|
||||
#[test]
|
||||
fn cross_module_instance_satisfies_concrete_call() {
|
||||
let entry = examples_dir().join("test_22b2_xmod_instance_present.ail.json");
|
||||
let ws = ailang_core::workspace::load_workspace(&entry)
|
||||
.expect("workspace loads");
|
||||
let diagnostics = ailang_check::check_workspace(&ws);
|
||||
assert!(
|
||||
diagnostics.is_empty(),
|
||||
"expected clean typecheck — A's instance Show Int must be in the workspace registry visible from B; got: {:?}",
|
||||
diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
/// Property protected: `check_workspace` aggregates diagnostics across
|
||||
/// distinct fns rather than stopping at the first failing fn. When two
|
||||
/// independent polymorphic fns each call `show x` without declaring `Show
|
||||
/// a`, the resulting diagnostic vec must contain TWO `missing-constraint`
|
||||
/// entries — one per fn — with the right `def` field on each. Without
|
||||
/// per-def aggregation in the `for def in &m.defs` loop of
|
||||
/// `check_in_workspace`, an editor consuming the diagnostics list would
|
||||
/// see only the first failure, hiding the second from the author until
|
||||
/// the first is fixed. Fixture has `describe_first` and `describe_second`,
|
||||
/// both ill-formed in the same way; both must surface.
|
||||
#[test]
|
||||
fn two_fns_each_missing_constraint_produce_two_diagnostics() {
|
||||
let entry = examples_dir().join("test_22b2_two_fns_missing_constraint.ail.json");
|
||||
let ws = ailang_core::workspace::load_workspace(&entry)
|
||||
.expect("workspace loads");
|
||||
let diagnostics = ailang_check::check_workspace(&ws);
|
||||
let mc: Vec<&ailang_check::Diagnostic> = diagnostics
|
||||
.iter()
|
||||
.filter(|d| d.code == "missing-constraint")
|
||||
.collect();
|
||||
assert_eq!(
|
||||
mc.len(),
|
||||
2,
|
||||
"expected two missing-constraint diagnostics (one per fn); got: {:?}",
|
||||
diagnostics.iter().map(|d| &d.code).collect::<Vec<_>>()
|
||||
);
|
||||
let defs: std::collections::BTreeSet<&str> = mc
|
||||
.iter()
|
||||
.filter_map(|d| d.def.as_deref())
|
||||
.collect();
|
||||
assert!(
|
||||
defs.contains("describe_first"),
|
||||
"expected diagnostic for `describe_first`; got defs: {:?}",
|
||||
defs
|
||||
);
|
||||
assert!(
|
||||
defs.contains("describe_second"),
|
||||
"expected diagnostic for `describe_second`; got defs: {:?}",
|
||||
defs
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b2_two_fns_missing_constraint",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class",
|
||||
"name": "Show",
|
||||
"param": "a",
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "describe_first",
|
||||
"type": {
|
||||
"k": "forall",
|
||||
"vars": ["a"],
|
||||
"body": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
},
|
||||
"params": ["x"],
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "describe_second",
|
||||
"type": {
|
||||
"k": "forall",
|
||||
"vars": ["b"],
|
||||
"body": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "b" }],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
},
|
||||
"params": ["y"],
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "var", "name": "y" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b2_xmod_classmod",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class",
|
||||
"name": "Show",
|
||||
"param": "a",
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Int" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"body": { "t": "lit", "lit": { "kind": "str", "value": "n" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b2_xmod_classmod_noinst",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class",
|
||||
"name": "Show",
|
||||
"param": "a",
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b2_xmod_instance_present",
|
||||
"imports": [{ "module": "test_22b2_xmod_classmod" }],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
},
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b2_xmod_missing_constraint",
|
||||
"imports": [{ "module": "test_22b2_xmod_classmod_noinst" }],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "describe",
|
||||
"type": {
|
||||
"k": "forall",
|
||||
"vars": ["a"],
|
||||
"body": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
},
|
||||
"params": ["x"],
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b2_xmod_no_instance",
|
||||
"imports": [{ "module": "test_22b2_xmod_classmod_noinst" }],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
},
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user