iter 22b.1.2: workspace registry skeleton + coherence checks

Adds the workspace-global typeclass instance registry per Decision 11
"Resolution and monomorphisation". Built at the end of load_workspace
after the DFS over imports completes; keyed by (class-name, type-hash)
where type-hash uses the new canonical::type_hash (16-hex prefix,
parallel to def_hash and module_hash).

Three coherence checks fire from build_registry, each surfacing as a
distinct WorkspaceLoadError variant:

- OrphanInstance: `instance C T` not in C's or T's defining module.
- DuplicateInstance: two entries share the same (class, type-hash) key.
- MissingMethod: instance omits a required (non-default) method.

The CLI's workspace_error_to_diagnostic is extended with the three new
codes (orphan-instance / duplicate-instance / missing-method).

All existing Workspace { ... } construction sites get the new
`registry` field, defaulting to Registry::default() at synthetic /
test-only paths and threading through ws.registry.clone() at codepath
sites that already hold a real workspace.

Includes hash-stability regression tests (iter22b1_schema_extension_*
and iter22b1_classdef_empty_optionals_hash_stable) and the empty-
registry positive test against examples/sum.ail.json. Test count:
288 → 291 (3 new tests, all pass).
This commit is contained in:
2026-05-09 12:37:00 +02:00
parent f25d7b6cd6
commit eb4db9dafc
8 changed files with 442 additions and 2 deletions
+20
View File
@@ -598,6 +598,13 @@ pub fn check_module(m: &Module) -> Vec<Diagnostic> {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
// Iter 22b.1: single-module check_module entry point does not
// build a registry. The registry is workspace-load-time
// metadata; check_module is invoked from in-memory paths
// (tests, single-file CLI) where no workspace DFS happened.
// Once 22b.2 typecheck arms read the registry, those paths
// will need to populate it from the in-memory module too.
registry: ailang_core::workspace::Registry::default(),
};
check_workspace(&ws)
}
@@ -628,6 +635,11 @@ pub fn check_workspace(ws: &Workspace) -> Vec<Diagnostic> {
.map(|(k, m)| (k.clone(), ailang_core::desugar::desugar_module(m)))
.collect(),
root_dir: ws.root_dir.clone(),
// Iter 22b.1: pass the registry through unchanged. The desugar
// pass does not touch class/instance defs (see desugar.rs:
// 22b.1 passthrough), so the registry built at load time
// remains valid against the desugared modules.
registry: ws.registry.clone(),
};
let ws = &ws_owned;
// Pass 1: build per-module top-level symbol table — without checking
@@ -730,6 +742,11 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
// Iter 22b.1: the legacy single-module `check` entry point
// builds an empty registry. See `check_module` for the same
// pattern; once 22b.2 typecheck arms read the registry, both
// paths must populate it from the in-memory module.
registry: ailang_core::workspace::Registry::default(),
};
let module_globals = build_module_globals(&ws)?;
// `check` keeps single-error semantics for callers (snapshot tests,
@@ -2881,6 +2898,7 @@ mod tests {
entry: consumer.name,
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
}
}
@@ -3056,6 +3074,7 @@ mod tests {
entry: "use_both".into(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
assert!(
@@ -3162,6 +3181,7 @@ mod tests {
entry: "uses_lst".into(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "expected green; got {diags:?}");
+6
View File
@@ -112,6 +112,7 @@ fn invalid_def_name_with_dot_is_reported() {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
@@ -184,6 +185,7 @@ fn body_errors_accumulate_across_defs() {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
@@ -288,6 +290,7 @@ fn use_after_consume_on_own_param_is_reported() {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
@@ -389,6 +392,7 @@ fn consume_while_borrowed_in_sibling_arg_is_reported() {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
@@ -527,6 +531,7 @@ fn reuse_as_happy_path_in_map_inc_is_linearity_clean() {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
assert!(
@@ -638,6 +643,7 @@ fn reuse_as_shape_mismatch_is_reported_on_cons_to_nil() {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
let shape_diags: Vec<&ailang_check::Diagnostic> = diags