diff --git a/crates/ail/tests/typeclass_22b2.rs b/crates/ail/tests/typeclass_22b2.rs index a283fcd..94a3459 100644 --- a/crates/ail/tests/typeclass_22b2.rs +++ b/crates/ail/tests/typeclass_22b2.rs @@ -235,3 +235,26 @@ fn fn_calling_class_method_at_type_without_instance_fires_no_instance() { diagnostics.iter().map(|d| &d.code).collect::>() ); } + +/// Property protected: the positive counterpart to `no-instance` — when a +/// monomorphic fn calls a class method at a fully-concrete type AND a +/// matching `instance C T` exists in the workspace, the call typechecks +/// green. The concrete-residual path must look up `(class, type_hash(T))` +/// in `env.workspace_registry` and find a hit, so no diagnostic fires. +/// This guards against registry-keying or threading bugs that would +/// otherwise sneak past the negative-only test: a registry that is empty +/// at typecheck time, or keyed under the wrong shape, would silently fire +/// `no-instance` even when the instance is present. Fixture: `main` calls +/// `show 42` with `instance Show Int` declared in the same workspace. +#[test] +fn fn_calling_class_method_at_type_with_instance_typechecks_green() { + let entry = examples_dir().join("test_22b2_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 no diagnostics — instance Show Int is present, registry lookup must hit; got: {:?}", + diagnostics + ); +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 6280d3a..ce5c3a3 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1177,12 +1177,11 @@ fn check_in_workspace( env.class_superclasses = class_superclasses; // Iter 22b.2 (Task 10): thread the workspace-load-time instance // registry into the env so `check_fn`'s concrete-residual path can - // look up `(class, type_hash)` keys for the no-instance check. + // look up `(class, type_hash)` keys for the no-instance check. The + // registry is the only field of `ws` consumed at typecheck time; + // cross-module name resolution uses `module_globals` / `module_types` + // built earlier in this function. env.workspace_registry = ws.registry.clone(); - // Workspace isn't otherwise needed in the env; cross-module lookup - // uses only `module_globals` / `module_types`. The ws reference is - // preserved in case cross-module ADTs are added later. - let _ = ws; for def in &m.defs { if let Err(e) = check_def(def, &env) { diff --git a/examples/test_22b2_instance_present.ail.json b/examples/test_22b2_instance_present.ail.json new file mode 100644 index 0000000..9090b86 --- /dev/null +++ b/examples/test_22b2_instance_present.ail.json @@ -0,0 +1,40 @@ +{ + "schema": "ailang/v0", + "name": "test_22b2_instance_present", + "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" } } + }] + }, + { + "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 } }] + } + } + ] +}