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
+56
View File
@@ -181,4 +181,60 @@ mod tests {
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
}
/// Iter 22b.1 regression: adding `Def::Class` and `Def::Instance`
/// must NOT change canonical-JSON hashes of any pre-22b def. The
/// new variants are alternatives, not field additions — pre-22b
/// fixtures simply do not produce a `Class` / `Instance` `Def`,
/// and the existing `Fn` / `Const` / `Type` arms are unchanged.
/// This re-asserts the same on-disk hashes the 13a / 19b
/// regressions pinned, after the 22b.1 schema bump.
#[test]
fn iter22b1_schema_extension_preserves_pre_22b_hashes() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples = manifest_dir.join("../../examples");
let sum_src = std::fs::read(examples.join("sum.ail.json"))
.expect("examples/sum.ail.json present");
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
let list_src = std::fs::read(examples.join("list.ail.json"))
.expect("examples/list.ail.json present");
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
let int_list_def = list_mod
.defs
.iter()
.find(|d| d.name() == "IntList")
.unwrap();
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
}
/// Iter 22b.1: a [`crate::ast::ClassDef`] with no doc, no
/// superclass, and an empty methods list serialises to a stable
/// canonical form. Two construction paths (default-elided
/// optionals vs. JSON without those keys at all) hash
/// bit-identically. If FAIL: a `skip_serializing_if` is missing
/// on `superclass` or `doc`, or the parser produces different
/// canonical bytes from the same logical value.
#[test]
fn iter22b1_classdef_empty_optionals_hash_stable() {
let bare = Def::Class(crate::ast::ClassDef {
name: "Empty".into(),
param: "a".into(),
superclass: None,
methods: vec![],
doc: None,
});
let json = r#"{"kind":"class","name":"Empty","param":"a","methods":[]}"#;
let parsed: Def = serde_json::from_str(json).unwrap();
assert_eq!(
def_hash(&bare),
def_hash(&parsed),
"ClassDef with elided optionals must hash identically to construction with explicit None"
);
}
}