# 22b.2 — Typecheck Arms — Implementation Plan > **Parent spec:** `docs/specs/0002-22-typeclasses.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Land the eight 22b.2 diagnostics (three class-schema, three workspace-load coherence, two per-FnDef) and activate the `constraints` slot on `Type::Forall` so polymorphic FnDefs can declare class constraints. After 22b.2, the typechecker rejects every kind of ill-formed class / instance / constraint shape that 22b.3's monomorphisation pass would otherwise have to defend against. **Architecture:** Two extension points. (1) `crates/ailang-core/src/ workspace.rs` — `validate_classdefs` runs before `build_registry` and covers the three class-schema diagnostics; `build_registry` itself gains three more checks (`overriding-non-existent-method`, `method-name-collision`, `missing-superclass-instance`). (2) `crates/ailang-check/src/lib.rs` — `build_module_globals` registers class methods alongside top-level fns, and `check_fn` collects residual constraints at class-method call sites, comparing them against `FnDef.ty`'s `Forall.constraints` (with superclass expansion) and resolving fully-concrete residuals against the registry. **Tech Stack:** rust + serde (schema), `thiserror` for error variants, existing `Diagnostic` shape in `crates/ailang-check/src/diagnostic.rs`, canonical-JSON hashing via `crates/ailang-core/src/canonical.rs`. **Files this plan creates or modifies:** - Modify: `crates/ailang-core/src/ast.rs` — add `Constraint` struct, add `constraints: Vec` field to `Type::Forall` - Modify: `crates/ailang-core/src/workspace.rs` — add 7 `WorkspaceLoadError` variants, add `validate_classdefs` fn, extend `build_registry` with three new checks - Modify: `crates/ail/src/main.rs` — extend the `WorkspaceLoadError → Diagnostic` mapping with 7 new arms - Modify: `crates/ailang-check/src/lib.rs` — extend `build_module_globals` to register class methods, add `MissingConstraint` and `NoInstance` to `CheckError`, extend `check_fn` with residual-constraint collection and resolution - Modify: `crates/ailang-check/src/diagnostic.rs` — register the two new check-side codes in the in-source diagnostic-code registry comment - Create: `examples/test_22b2_kind_mismatch.ail.json` - Create: `examples/test_22b2_invalid_superclass_param.ail.json` - Create: `examples/test_22b2_unbound_constraint_var.ail.json` - Create: `examples/test_22b2_overriding_nonexistent.ail.json` - Create: `examples/test_22b2_method_name_collision_class_class.ail.json` - Create: `examples/test_22b2_method_name_collision_class_fn.ail.json` - Create: `examples/test_22b2_missing_superclass_instance.ail.json` - Create: `examples/test_22b2_missing_constraint.ail.json` - Create: `examples/test_22b2_no_instance.ail.json` - Create: `examples/test_22b2_constraint_declared.ail.json` (positive — should typecheck green) --- ## Task 1: Schema — `Constraint` struct + `Type::Forall.constraints` **Files:** - Modify: `crates/ailang-core/src/ast.rs` (after `InstanceMethod`, before `ConstDef`; in `enum Type`, the `Forall` variant) - Test: `crates/ailang-core/src/hash.rs` (existing `#[cfg(test)] mod tests`) - [ ] **Step 1: Write the hash-stability RED test** In `crates/ailang-core/src/hash.rs`'s test module add: ```rust #[test] fn forall_without_constraints_hashes_bit_identical_to_pre_22b2() { use crate::ast::Type; let t = Type::Forall { vars: vec!["a".into()], body: Box::new(Type::fn_implicit( vec![Type::Var { name: "a".into() }], Type::Var { name: "a".into() }, vec![], )), }; let bytes = crate::canonical::to_bytes(&t); let s = std::str::from_utf8(&bytes).unwrap(); assert!( !s.contains("constraints"), "Type::Forall serialised must omit `constraints` when empty; got: {s}" ); } ``` - [ ] **Step 2: Run RED test, expect compile failure** Run: `cargo test --workspace -p ailang-core forall_without_constraints_hashes_bit_identical_to_pre_22b2` Expected: FAIL — compile error, the test references the existing struct shape; this passes shape-wise but will fail-as-FAIL once `constraints` is a field if `skip_serializing_if` is wrong. - [ ] **Step 3: Add `Constraint` struct** In `crates/ailang-core/src/ast.rs`, after `InstanceMethod`: ```rust /// Iter 22b.2: a class constraint on a polymorphic function (Decision /// 11). `(class, type)` pair where `class` is a class name and `type` /// is a `Type` expression — typically a single `Type::Var` (e.g. /// `(Eq, a)` for `Eq a => ...`). Concrete-type constraints are legal /// schema-wise but fired as `no-instance` at typecheck time if no /// matching registry entry exists. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Constraint { /// Class name. pub class: String, /// Type the class is applied to. #[serde(rename = "type")] pub type_: Type, } ``` - [ ] **Step 4: Add `constraints` field to `Type::Forall`** In `crates/ailang-core/src/ast.rs`, change the `Forall` variant: ```rust Forall { vars: Vec, /// Iter 22b.2: class constraints quantified together with /// `vars`. Empty for pre-22b.2 polymorphic types; serialised /// with `skip_serializing_if = "Vec::is_empty"` so existing /// canonical-JSON bytes stay bit-identical. #[serde(default, skip_serializing_if = "Vec::is_empty")] constraints: Vec, body: Box, }, ``` - [ ] **Step 5: Fix construction sites** Run: `cargo build --workspace 2>&1 | grep -E "error\[" | head` Expected: a finite list of `Type::Forall { vars, body }` construction sites missing the new field. Add `constraints: vec![]` at each. Match arms `Type::Forall { vars, body }` need `..` or `constraints: _`. - [ ] **Step 6: Run hash-stability test, expect pass** Run: `cargo test --workspace -p ailang-core forall_without_constraints_hashes_bit_identical_to_pre_22b2` Expected: PASS — `constraints` is not serialised when empty. - [ ] **Step 7: Run the full unit-test suite to confirm no schema regressions** Run: `cargo test --workspace` Expected: PASS (everything green; no fixture hash drifts). - [ ] **Step 8: Commit** ```bash git add crates/ailang-core/src/ast.rs crates/ailang-core/src/hash.rs git commit -m "iter 22b.2.1: add Constraint struct, Type::Forall.constraints" ``` --- ## Task 2: Class-schema validation entry point + `kind-mismatch` **Files:** - Modify: `crates/ailang-core/src/workspace.rs` (new fn `validate_classdefs`, new error variant `KindMismatch`; call site in `load_workspace` between `visit` loop and `build_registry`) - Modify: `crates/ail/src/main.rs` (add `KindMismatch` arm in the `WorkspaceLoadError → Diagnostic` matcher near line 1015) - Create: `examples/test_22b2_kind_mismatch.ail.json` - [ ] **Step 1: Write the fixture (RED data)** `examples/test_22b2_kind_mismatch.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_kind_mismatch", "imports": [], "defs": [ { "kind": "class", "name": "Functor", "param": "f", "methods": [ { "name": "fmap", "type": { "k": "fn", "params": [ { "k": "fn", "params": [{ "k": "var", "name": "a" }], "ret": { "k": "var", "name": "b" }, "effects": [] }, { "k": "con", "name": "f", "args": [{ "k": "var", "name": "a" }] } ], "ret": { "k": "con", "name": "f", "args": [{ "k": "var", "name": "b" }] }, "effects": [] } } ] } ] } ``` The class param `f` appears as `Type::Con { name: "f", args: [a] }` — applied position. Decision 11 axis 5 forbids this. - [ ] **Step 2: Write the Rust RED test** In `crates/ailang-core/src/workspace.rs`'s `#[cfg(test)] mod tests` (near the existing `missing_method_fires` test ~line 695): ```rust #[test] fn class_param_in_applied_position_fires_kind_mismatch() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_kind_mismatch.ail.json", ); let err = load_workspace(&entry) .expect_err("must fire kind-mismatch"); match err { WorkspaceLoadError::KindMismatch { class, param, method, .. } => { assert_eq!(class, "Functor"); assert_eq!(param, "f"); assert_eq!(method, "fmap"); } other => panic!("expected KindMismatch, got {other:?}"), } } ``` - [ ] **Step 3: Run RED test, expect compile error** Run: `cargo test -p ailang-core class_param_in_applied_position_fires_kind_mismatch` Expected: compile error — `KindMismatch` variant does not exist. - [ ] **Step 4: Add `WorkspaceLoadError::KindMismatch` variant** In `crates/ailang-core/src/workspace.rs`'s `enum WorkspaceLoadError`, after `MissingMethod`: ```rust /// Iter 22b.2: class-schema validation. The class parameter /// appears in applied position (e.g. as the head of a /// `Type::Con { name == param, args.len() > 0 }`) inside a method /// signature. Decision 11 axis 5 forbids HKTs — class params are /// kind `*` only. #[error( "kind mismatch in class `{class}`: parameter `{param}` is used in applied position \ inside method `{method}` (Decision 11 axis 5: class params are kind `*` only)" )] KindMismatch { class: String, param: String, method: String, defining_module: String, }, ``` - [ ] **Step 5: Add `validate_classdefs` and the kind-mismatch check** In `crates/ailang-core/src/workspace.rs`, after `build_registry`: ```rust /// Iter 22b.2: class-schema validation. Runs before `build_registry`. /// Three diagnostics fire from here: `kind-mismatch`, /// `invalid-superclass-param`, `constraint-references-unbound-type-var`. fn validate_classdefs( modules: &BTreeMap, ) -> Result<(), WorkspaceLoadError> { for (mod_name, m) in modules { for def in &m.defs { if let Def::Class(c) = def { for method in &c.methods { walk_kind_mismatch(&method.ty, &c.param) .map_err(|()| WorkspaceLoadError::KindMismatch { class: c.name.clone(), param: c.param.clone(), method: method.name.clone(), defining_module: mod_name.clone(), })?; } } } } Ok(()) } /// Walks a `Type` looking for any `Type::Con { name == param, args /// non-empty }`. The class param is kind `*`; appearing as a /// constructor with arguments is a kind-mismatch. fn walk_kind_mismatch(t: &Type, param: &str) -> Result<(), ()> { match t { Type::Con { name, args } => { if name == param && !args.is_empty() { return Err(()); } for a in args { walk_kind_mismatch(a, param)?; } Ok(()) } Type::Fn { params, ret, .. } => { for p in params { walk_kind_mismatch(p, param)?; } walk_kind_mismatch(ret, param) } Type::Forall { body, .. } => walk_kind_mismatch(body, param), Type::Var { .. } => Ok(()), } } ``` In `load_workspace`, before `build_registry(&modules)`: ```rust validate_classdefs(&modules)?; let registry = build_registry(&modules)?; ``` - [ ] **Step 6: Add CLI mapping** In `crates/ail/src/main.rs`, in the `WorkspaceLoadError → Diagnostic` matcher (immediately after the `MissingMethod` arm, ~line 1031), add: ```rust W::KindMismatch { class, param, method, defining_module, } => Some( ailang_check::Diagnostic::error( "kind-mismatch", format!( "kind mismatch in class `{class}`: parameter `{param}` is used in applied position \ inside method `{method}` (Decision 11 axis 5: class params are kind `*` only)" ), ) .with_ctx(serde_json::json!({ "class": class, "param": param, "method": method, "defining_module": defining_module, })), ), ``` - [ ] **Step 7: Run RED test, expect pass** Run: `cargo test -p ailang-core class_param_in_applied_position_fires_kind_mismatch` Expected: PASS. - [ ] **Step 8: Run full check suite** Run: `cargo test --workspace` Expected: PASS. - [ ] **Step 9: Commit** ```bash git add crates/ailang-core/src/workspace.rs crates/ail/src/main.rs \ examples/test_22b2_kind_mismatch.ail.json git commit -m "iter 22b.2.2: kind-mismatch class-schema diagnostic" ``` --- ## Task 3: Class-schema validation — `invalid-superclass-param` **Files:** - Modify: `crates/ailang-core/src/workspace.rs` (new error variant `InvalidSuperclassParam`; extend `validate_classdefs`) - Modify: `crates/ail/src/main.rs` (add CLI mapping arm) - Create: `examples/test_22b2_invalid_superclass_param.ail.json` - [ ] **Step 1: Write the fixture** `examples/test_22b2_invalid_superclass_param.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_invalid_superclass_param", "imports": [], "defs": [ { "kind": "class", "name": "Eq", "param": "a", "methods": [ { "name": "eq", "type": { "k": "fn", "params": [ { "k": "var", "name": "a" }, { "k": "var", "name": "a" } ], "ret": { "k": "con", "name": "Bool" }, "effects": [] } } ] }, { "kind": "class", "name": "Ord", "param": "a", "superclass": { "class": "Eq", "type": "b" }, "methods": [ { "name": "lt", "type": { "k": "fn", "params": [ { "k": "var", "name": "a" }, { "k": "var", "name": "a" } ], "ret": { "k": "con", "name": "Bool" }, "effects": [] } } ] } ] } ``` `Ord`'s superclass `Eq` is applied to `b`, but `Ord`'s own param is `a`. Decision 11 axis 1: superclass `type` must equal `param`. - [ ] **Step 2: Write the Rust RED test** In `crates/ailang-core/src/workspace.rs`: ```rust #[test] fn superclass_with_wrong_param_fires_invalid_superclass_param() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_invalid_superclass_param.ail.json", ); let err = load_workspace(&entry) .expect_err("must fire invalid-superclass-param"); match err { WorkspaceLoadError::InvalidSuperclassParam { class, superclass, expected_param, got_type, } => { assert_eq!(class, "Ord"); assert_eq!(superclass, "Eq"); assert_eq!(expected_param, "a"); assert_eq!(got_type, "b"); } other => panic!("expected InvalidSuperclassParam, got {other:?}"), } } ``` - [ ] **Step 3: Run RED test, expect compile error** Run: `cargo test -p ailang-core superclass_with_wrong_param_fires_invalid_superclass_param` Expected: compile error. - [ ] **Step 4: Add `WorkspaceLoadError::InvalidSuperclassParam`** In the enum, after `KindMismatch`: ```rust /// Iter 22b.2: class-schema validation. A class's `superclass.type` /// does not equal its own `param`. Decision 11 single-superclass /// model requires the superclass to be applied to the same param /// (e.g. `class Ord a extends Eq a`, not `extends Eq b`). #[error( "class `{class}` declares superclass `{superclass} {got_type}`, but its own parameter is `{expected_param}` \ — superclass `type` must equal class `param`" )] InvalidSuperclassParam { class: String, superclass: String, expected_param: String, got_type: String, }, ``` - [ ] **Step 5: Extend `validate_classdefs`** Inside the `Def::Class(c)` arm, after the kind-mismatch loop: ```rust if let Some(sc) = &c.superclass { if sc.type_ != c.param { return Err(WorkspaceLoadError::InvalidSuperclassParam { class: c.name.clone(), superclass: sc.class.clone(), expected_param: c.param.clone(), got_type: sc.type_.clone(), }); } } ``` - [ ] **Step 6: Add CLI mapping** In `crates/ail/src/main.rs`, after the `KindMismatch` arm: ```rust W::InvalidSuperclassParam { class, superclass, expected_param, got_type, } => Some( ailang_check::Diagnostic::error( "invalid-superclass-param", format!( "class `{class}` declares superclass `{superclass} {got_type}`, but its own parameter is `{expected_param}` — superclass `type` must equal class `param`" ), ) .with_ctx(serde_json::json!({ "class": class, "superclass": superclass, "expected_param": expected_param, "got_type": got_type, })), ), ``` - [ ] **Step 7: Run RED test, expect pass** Run: `cargo test -p ailang-core superclass_with_wrong_param_fires_invalid_superclass_param` Expected: PASS. - [ ] **Step 8: Run full suite** Run: `cargo test --workspace` Expected: PASS. - [ ] **Step 9: Commit** ```bash git add crates/ailang-core/src/workspace.rs crates/ail/src/main.rs \ examples/test_22b2_invalid_superclass_param.ail.json git commit -m "iter 22b.2.3: invalid-superclass-param class-schema diagnostic" ``` --- ## Task 4: Class-schema validation — `constraint-references-unbound-type-var` **Files:** - Modify: `crates/ailang-core/src/workspace.rs` (new error variant `UnboundConstraintTypeVar`; extend `validate_classdefs`) - Modify: `crates/ail/src/main.rs` (CLI mapping) - Create: `examples/test_22b2_unbound_constraint_var.ail.json` This check applies once `Type::Forall.constraints` (Task 1) is wired in. It guards against constraints that reference a type variable not quantified by the enclosing `Forall` AND not equal to the class's `param`. - [ ] **Step 1: Write the fixture** `examples/test_22b2_unbound_constraint_var.ail.json`: ```json { "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": [] } } } ] } ] } ``` The constraint `(Bar, z)` references `z`, which is neither in `vars` nor the class's `param`. - [ ] **Step 2: Write the Rust RED test** ```rust #[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:?}"), } } ``` - [ ] **Step 3: Run RED test, expect compile error** Run: `cargo test -p ailang-core constraint_with_unbound_var_fires_unbound_constraint_type_var` Expected: compile error. - [ ] **Step 4: Add error variant** ```rust /// 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, }, ``` - [ ] **Step 5: Extend `validate_classdefs`** Add after the superclass check in the `Def::Class(c)` arm: ```rust 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(), }); } } } } } ``` - [ ] **Step 6: Add CLI mapping** ```rust 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, })), ), ``` - [ ] **Step 7: Run RED test, expect pass** Run: `cargo test -p ailang-core constraint_with_unbound_var_fires_unbound_constraint_type_var` Expected: PASS. - [ ] **Step 8: Run full suite** Run: `cargo test --workspace` Expected: PASS. - [ ] **Step 9: Commit** ```bash git add crates/ailang-core/src/workspace.rs crates/ail/src/main.rs \ examples/test_22b2_unbound_constraint_var.ail.json git commit -m "iter 22b.2.4: constraint-references-unbound-type-var diagnostic" ``` --- ## Task 5: Workspace coherence — `overriding-non-existent-method` **Files:** - Modify: `crates/ailang-core/src/workspace.rs` (new error variant; extend `build_registry`'s instance loop) - Modify: `crates/ail/src/main.rs` (CLI mapping) - Create: `examples/test_22b2_overriding_nonexistent.ail.json` - [ ] **Step 1: Write the fixture** `examples/test_22b2_overriding_nonexistent.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_overriding_nonexistent", "imports": [], "defs": [ { "kind": "class", "name": "Eq", "param": "a", "methods": [ { "name": "eq", "type": { "k": "fn", "params": [{ "k": "var", "name": "a" }, { "k": "var", "name": "a" }], "ret": { "k": "con", "name": "Bool" }, "effects": [] } } ] }, { "kind": "instance", "class": "Eq", "type": { "k": "con", "name": "Int" }, "methods": [ { "name": "eq", "body": { "t": "lit", "lit": { "kind": "bool", "value": true } } }, { "name": "ne", "body": { "t": "lit", "lit": { "kind": "bool", "value": false } } } ] } ] } ``` `Eq` does not declare `ne`; the `Int` instance tries to override it. - [ ] **Step 2: Write the Rust RED test** ```rust #[test] fn instance_overriding_nonexistent_method_fires() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_overriding_nonexistent.ail.json", ); let err = load_workspace(&entry) .expect_err("must fire overriding-non-existent-method"); match err { WorkspaceLoadError::OverridingNonExistentMethod { class, type_repr, method, } => { assert_eq!(class, "Eq"); assert_eq!(type_repr, "Int"); assert_eq!(method, "ne"); } other => panic!("expected OverridingNonExistentMethod, got {other:?}"), } } ``` - [ ] **Step 3: Run RED test, expect compile error** Run: `cargo test -p ailang-core instance_overriding_nonexistent_method_fires` Expected: compile error. - [ ] **Step 4: Add `WorkspaceLoadError::OverridingNonExistentMethod`** ```rust /// Iter 22b.2: an instance specifies a body for a method name /// that the corresponding class does not declare. Symmetric to /// `MissingMethod` but in the opposite direction. #[error( "instance `{class} {type_repr}` provides body for method `{method}`, but class `{class}` does not declare it" )] OverridingNonExistentMethod { class: String, type_repr: String, method: String, }, ``` - [ ] **Step 5: Extend `build_registry`'s instance loop** In `crates/ailang-core/src/workspace.rs`, inside the existing `if let Some(class_def) = class_by_name.get(&inst.class)` block in `build_registry`, after the existing missing-method loop, add: ```rust let declared: BTreeSet<&str> = class_def.methods.iter().map(|m| m.name.as_str()).collect(); for inst_method in &inst.methods { if !declared.contains(inst_method.name.as_str()) { return Err(WorkspaceLoadError::OverridingNonExistentMethod { class: inst.class.clone(), type_repr: type_repr.clone(), method: inst_method.name.clone(), }); } } ``` - [ ] **Step 6: Add CLI mapping** In `crates/ail/src/main.rs`: ```rust W::OverridingNonExistentMethod { class, type_repr, method, } => Some( ailang_check::Diagnostic::error( "overriding-non-existent-method", format!( "instance `{class} {type_repr}` provides body for method `{method}`, but class `{class}` does not declare it" ), ) .with_ctx(serde_json::json!({ "class": class, "type": type_repr, "method": method, })), ), ``` - [ ] **Step 7: Run RED test, expect pass** Run: `cargo test -p ailang-core instance_overriding_nonexistent_method_fires` Expected: PASS. - [ ] **Step 8: Run full suite** Run: `cargo test --workspace` Expected: PASS. - [ ] **Step 9: Commit** ```bash git add crates/ailang-core/src/workspace.rs crates/ail/src/main.rs \ examples/test_22b2_overriding_nonexistent.ail.json git commit -m "iter 22b.2.5: overriding-non-existent-method diagnostic" ``` --- ## Task 6: Workspace coherence — `method-name-collision` **Files:** - Modify: `crates/ailang-core/src/workspace.rs` (new error variant with two flavours via context, extend `build_registry`) - Modify: `crates/ail/src/main.rs` (CLI mapping) - Create: `examples/test_22b2_method_name_collision_class_class.ail.json` - Create: `examples/test_22b2_method_name_collision_class_fn.ail.json` Two collision shapes share one diagnostic code: class-method ↔ class-method, and class-method ↔ top-level-fn. - [ ] **Step 1: Write the class-class fixture** `examples/test_22b2_method_name_collision_class_class.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_method_name_collision_class_class", "imports": [], "defs": [ { "kind": "class", "name": "A", "param": "a", "methods": [{ "name": "foo", "type": { "k": "fn", "params": [{ "k": "var", "name": "a" }], "ret": { "k": "con", "name": "Unit" }, "effects": [] } }] }, { "kind": "class", "name": "B", "param": "b", "methods": [{ "name": "foo", "type": { "k": "fn", "params": [{ "k": "var", "name": "b" }], "ret": { "k": "con", "name": "Unit" }, "effects": [] } }] } ] } ``` - [ ] **Step 2: Write the class-fn fixture** `examples/test_22b2_method_name_collision_class_fn.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_method_name_collision_class_fn", "imports": [], "defs": [ { "kind": "class", "name": "Greet", "param": "a", "methods": [{ "name": "greet", "type": { "k": "fn", "params": [{ "k": "var", "name": "a" }], "ret": { "k": "con", "name": "Str" }, "effects": [] } }] }, { "kind": "fn", "name": "greet", "type": { "k": "fn", "params": [{ "k": "con", "name": "Int" }], "ret": { "k": "con", "name": "Str" }, "effects": [] }, "params": ["x"], "body": { "t": "lit", "lit": { "kind": "string", "value": "hi" } } } ] } ``` - [ ] **Step 3: Write Rust RED tests (one per fixture)** ```rust #[test] fn class_class_method_name_collision_fires() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_method_name_collision_class_class.ail.json", ); let err = load_workspace(&entry) .expect_err("must fire method-name-collision"); match err { WorkspaceLoadError::MethodNameCollision { method, kind, .. } => { assert_eq!(method, "foo"); assert_eq!(kind, "class-class"); } other => panic!("expected MethodNameCollision, got {other:?}"), } } #[test] fn class_fn_method_name_collision_fires() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_method_name_collision_class_fn.ail.json", ); let err = load_workspace(&entry) .expect_err("must fire method-name-collision"); match err { WorkspaceLoadError::MethodNameCollision { method, kind, .. } => { assert_eq!(method, "greet"); assert_eq!(kind, "class-fn"); } other => panic!("expected MethodNameCollision, got {other:?}"), } } ``` - [ ] **Step 4: Run RED tests, expect compile error** Run: `cargo test -p ailang-core method_name_collision` Expected: compile error. - [ ] **Step 5: Add `WorkspaceLoadError::MethodNameCollision` variant** ```rust /// Iter 22b.2: a class-method name collides with another /// class-method or with a top-level fn. The Prelude reserves /// names `show`, `eq`, `ne`, `lt`, `le`, `gt`, `ge` once 22b.4 /// lands. `kind` is `"class-class"` or `"class-fn"`. #[error( "method name `{method}` collides ({kind}): defined in `{first_origin}` and `{second_origin}`" )] MethodNameCollision { method: String, kind: &'static str, first_origin: String, second_origin: String, }, ``` - [ ] **Step 6: Extend `build_registry`'s pass-1** Right after the existing pass-1 loop that builds `class_def_module`, extend it to track method origins (replace the existing pass-1 loop or augment alongside): ```rust let mut method_origins: BTreeMap = BTreeMap::new(); for (mod_name, m) in modules { for def in &m.defs { match def { Def::Class(c) => { for method in &c.methods { let origin = format!("class {} (in {mod_name})", c.name); if let Some(prior) = method_origins.get(&method.name) { return Err(WorkspaceLoadError::MethodNameCollision { method: method.name.clone(), kind: if prior.starts_with("class ") { "class-class" } else { "class-fn" }, first_origin: prior.clone(), second_origin: origin, }); } method_origins.insert(method.name.clone(), origin); } } Def::Fn(f) => { let origin = format!("fn {} (in {mod_name})", f.name); if let Some(prior) = method_origins.get(&f.name) { return Err(WorkspaceLoadError::MethodNameCollision { method: f.name.clone(), kind: if prior.starts_with("class ") { "class-fn" } else { "class-fn" }, first_origin: prior.clone(), second_origin: origin, }); } method_origins.insert(f.name.clone(), origin); } _ => {} } } } ``` (The existing class-name and type-name maps stay in their own pass-1 loop; this is a new collision pass added before instance processing.) - [ ] **Step 7: Add CLI mapping** ```rust W::MethodNameCollision { method, kind, first_origin, second_origin, } => Some( ailang_check::Diagnostic::error( "method-name-collision", format!( "method name `{method}` collides ({kind}): defined in `{first_origin}` and `{second_origin}`" ), ) .with_ctx(serde_json::json!({ "method": method, "kind": kind, "first_origin": first_origin, "second_origin": second_origin, })), ), ``` - [ ] **Step 8: Run both RED tests, expect pass** Run: `cargo test -p ailang-core method_name_collision` Expected: PASS (two tests). - [ ] **Step 9: Run full suite** Run: `cargo test --workspace` Expected: PASS. - [ ] **Step 10: Commit** ```bash git add crates/ailang-core/src/workspace.rs crates/ail/src/main.rs \ examples/test_22b2_method_name_collision_class_class.ail.json \ examples/test_22b2_method_name_collision_class_fn.ail.json git commit -m "iter 22b.2.6: method-name-collision diagnostic" ``` --- ## Task 7: Workspace coherence — `missing-superclass-instance` **Files:** - Modify: `crates/ailang-core/src/workspace.rs` (new error variant; add a post-loop superclass check at the end of `build_registry`) - Modify: `crates/ail/src/main.rs` (CLI mapping) - Create: `examples/test_22b2_missing_superclass_instance.ail.json` - [ ] **Step 1: Write the fixture** `examples/test_22b2_missing_superclass_instance.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_missing_superclass_instance", "imports": [], "defs": [ { "kind": "class", "name": "Eq", "param": "a", "methods": [{ "name": "eq", "type": { "k": "fn", "params": [{ "k": "var", "name": "a" }, { "k": "var", "name": "a" }], "ret": { "k": "con", "name": "Bool" }, "effects": [] } }] }, { "kind": "class", "name": "Ord", "param": "a", "superclass": { "class": "Eq", "type": "a" }, "methods": [{ "name": "lt", "type": { "k": "fn", "params": [{ "k": "var", "name": "a" }, { "k": "var", "name": "a" }], "ret": { "k": "con", "name": "Bool" }, "effects": [] } }] }, { "kind": "instance", "class": "Ord", "type": { "k": "con", "name": "Int" }, "methods": [{ "name": "lt", "body": { "t": "lit", "lit": { "kind": "bool", "value": true } } }] } ] } ``` `instance Ord Int` is declared, but no `instance Eq Int` — superclass violation. - [ ] **Step 2: Write the Rust RED test** ```rust #[test] fn instance_without_superclass_instance_fires() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_missing_superclass_instance.ail.json", ); let err = load_workspace(&entry) .expect_err("must fire missing-superclass-instance"); match err { WorkspaceLoadError::MissingSuperclassInstance { class, superclass, type_repr, } => { assert_eq!(class, "Ord"); assert_eq!(superclass, "Eq"); assert_eq!(type_repr, "Int"); } other => panic!("expected MissingSuperclassInstance, got {other:?}"), } } ``` - [ ] **Step 3: Run RED test, expect compile error** Run: `cargo test -p ailang-core instance_without_superclass_instance_fires` Expected: compile error. - [ ] **Step 4: Add `WorkspaceLoadError::MissingSuperclassInstance`** ```rust /// Iter 22b.2: an instance `C T` was declared, but `C`'s /// superclass `S` does not have an instance for the same type /// `T`. Decision 11 single-superclass model requires `instance S /// T` to exist whenever `instance C T` exists. #[error( "instance `{class} {type_repr}` requires superclass instance `{superclass} {type_repr}`, but none was found" )] MissingSuperclassInstance { class: String, superclass: String, type_repr: String, }, ``` - [ ] **Step 5: Add post-loop superclass check in `build_registry`** After the main `for (mod_name, m) in modules` instance-loop in `build_registry`, before `Ok(Registry { entries })`, add: ```rust // Superclass-instance completeness: for every `(class, type-hash)` // entry, walk the class's superclass chain and require an entry // for each step with the same type-hash. for (key, entry) in entries.iter() { let (class_name, type_hash) = key; let mut current = class_by_name.get(class_name.as_str()).copied(); while let Some(c) = current { if let Some(sc) = &c.superclass { let sc_key = (sc.class.clone(), type_hash.clone()); if !entries.contains_key(&sc_key) { return Err(WorkspaceLoadError::MissingSuperclassInstance { class: class_name.clone(), superclass: sc.class.clone(), type_repr: type_head_name(&entry.instance.type_), }); } current = class_by_name.get(sc.class.as_str()).copied(); } else { break; } } } ``` - [ ] **Step 6: Add CLI mapping** ```rust W::MissingSuperclassInstance { class, superclass, type_repr, } => Some( ailang_check::Diagnostic::error( "missing-superclass-instance", format!( "instance `{class} {type_repr}` requires superclass instance `{superclass} {type_repr}`, but none was found" ), ) .with_ctx(serde_json::json!({ "class": class, "superclass": superclass, "type": type_repr, })), ), ``` - [ ] **Step 7: Run RED test, expect pass** Run: `cargo test -p ailang-core instance_without_superclass_instance_fires` Expected: PASS. - [ ] **Step 8: Run full suite** Run: `cargo test --workspace` Expected: PASS. - [ ] **Step 9: Commit** ```bash git add crates/ailang-core/src/workspace.rs crates/ail/src/main.rs \ examples/test_22b2_missing_superclass_instance.ail.json git commit -m "iter 22b.2.7: missing-superclass-instance diagnostic" ``` --- ## Task 8: Pass-1 — register class methods in module globals **Files:** - Modify: `crates/ailang-check/src/lib.rs` (extend `build_module_globals` to register class methods; track class membership for residual generation in Task 9) This is infrastructure for Tasks 9 and 10. Class methods must be resolvable as global names so that `Term::Var { name: "show" }` does not fire `unbound-var`. Resolution must record which class the method belongs to and at which type-var position the class param appears, so Task 9 can build the residual constraint at the call site. - [ ] **Step 1: Read the current `build_module_globals` shape** Read `crates/ailang-check/src/lib.rs:843` (search for `fn build_module_globals`). Note the existing `ModuleGlobals` / `Globals` struct shape; the class-method-membership extension MUST preserve its existing field names so other call sites stay untouched. - [ ] **Step 2: Create the fixture** `examples/test_22b2_class_method_lookup.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_class_method_lookup", "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": "string", "value": "n" } } }] } ] } ``` - [ ] **Step 3: Write the RED test** Create `crates/ail/tests/typeclass_22b2.rs` (the same file Tasks 9 and 10 will extend): ```rust #[test] fn class_method_is_in_module_globals() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_class_method_lookup.ail.json", ); let ws = ailang_core::workspace::load_workspace(&entry) .expect("workspace must load"); let globals = ailang_check::build_module_globals(&ws) .expect("globals build"); let mod_globals = globals.get("test_22b2_class_method_lookup") .expect("module globals present"); assert!( mod_globals.has_class_method("show"), "class method `show` must appear in module globals" ); assert_eq!( mod_globals.class_method_class("show"), Some("Show"), "class method `show` must remember its class is `Show`" ); } ``` `build_module_globals` is currently `fn`-private (file-local). Promote it to `pub(crate)` or `pub` for the test, OR reach the same data through `check_workspace`'s public surface — the implementer chooses based on what's least invasive. The helper names `has_class_method` / `class_method_class` are suggested; existing `Globals`-struct naming wins if it conflicts. - [ ] **Step 4: Run RED test, expect failure** Run: `cargo test -p ail class_method_is_in_module_globals` Expected: FAIL — class methods are not registered yet (or compile error if helper accessors don't exist yet). - [ ] **Step 5: Extend `build_module_globals` and `Globals` shape** Add a field `class_methods: BTreeMap` (or match existing naming) where `ClassMethodEntry` carries: - `class_name: String` - `class_param: String` - `method_ty: Type` (the method's declared signature) - `defining_module: String` Populate from `Def::Class` in the same loop that handles `Def::Fn`. Add `has_class_method(&str) -> bool` and `class_method_class(&str) -> Option<&str>` helpers. - [ ] **Step 6: Run RED test, expect pass** Run: `cargo test -p ail class_method_is_in_module_globals` Expected: PASS. - [ ] **Step 7: Run full suite** Run: `cargo test --workspace` Expected: PASS — no regressions to existing typecheck tests; class methods are now resolved as globals but no FnDef body invokes them yet, so no behaviour changes elsewhere. - [ ] **Step 8: Commit** ```bash git add crates/ailang-check/src/lib.rs \ crates/ail/tests/typeclass_22b2.rs \ examples/test_22b2_class_method_lookup.ail.json git commit -m "iter 22b.2.8: register class methods in module globals" ``` --- ## Task 9: Per-FnDef typecheck — `missing-constraint` diagnostic **Files:** - Modify: `crates/ailang-check/src/lib.rs` (extend `check_fn` to collect residual constraints at class-method call sites; add `CheckError::MissingConstraint`; extend code mapping) - Modify: `crates/ailang-check/src/diagnostic.rs` (extend the in-source code registry comment) - Create: `examples/test_22b2_missing_constraint.ail.json` (negative) - Create: `examples/test_22b2_constraint_declared.ail.json` (positive) - [ ] **Step 1: Write the negative fixture** `examples/test_22b2_missing_constraint.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_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", "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" }] } } ] } ``` `describe` calls `show x` but its `Forall` declares no `Show a` constraint. - [ ] **Step 2: Write the positive fixture** `examples/test_22b2_constraint_declared.ail.json`: same as above but `describe`'s `forall` carries `"constraints": [{"class": "Show", "type": {"k":"var","name":"a"}}]`. Plus an `instance Show Int` so Task 10's no-instance does not fire transitively. (Or: the call is on a polymorphic value, so the residual stays unresolved — typecheck green is enough.) ```json { "schema": "ailang/v0", "name": "test_22b2_constraint_declared", "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", "type": { "k": "forall", "vars": ["a"], "constraints": [ { "class": "Show", "type": { "k": "var", "name": "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" }] } } ] } ``` - [ ] **Step 3: Write Rust RED tests (one negative, one positive)** Create `crates/ail/tests/typeclass_22b2.rs` (next to existing `crates/ail/tests/ir_snapshot.rs`): ```rust #[test] fn fn_calling_class_method_without_declared_constraint_fires_missing_constraint() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_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; got: {:?}", diagnostics.iter().map(|d| &d.code).collect::>() ); } #[test] fn fn_with_correct_constraint_typechecks_green() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_constraint_declared.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; got: {:?}", diagnostics ); } ``` The public entry point is `ailang_check::check_workspace(&Workspace) -> Vec` (`crates/ailang-check/src/lib.rs:625`). Land both new tests in a new file `crates/ail/tests/typeclass_22b2.rs` so they sit next to existing e2e tests like `ir_snapshot.rs`. - [ ] **Step 4: Run RED tests, expect failure** Run: `cargo test -p ail fn_calling_class_method` Expected: FAIL on the negative test — currently the typechecker either emits `unbound-var` (before Task 8) or no diagnostic at all (after Task 8 but before residual generation). - [ ] **Step 5: Add `CheckError::MissingConstraint`** In `crates/ailang-check/src/lib.rs`'s `enum CheckError` (near `UnknownIdent` or per-existing-grouping): ```rust #[error( "fn `{def}` calls class method `{method}` (class `{class}`) at type `{at_type}`, \ but its declared constraints do not include `{class} {at_type}`" )] MissingConstraint { def: String, method: String, class: String, at_type: String, }, ``` Add to `code_of()`: ```rust CheckError::MissingConstraint { .. } => "missing-constraint", ``` Add `ctx()` arm with class/method/at_type fields. - [ ] **Step 6a: Extend `check_fn` to detect class-method calls** When the typechecker resolves `Term::Var { name }` against the module-globals lookup and the entry is a class-method entry, take the call's argument type at the class-param position (substituting the class param in the method's declared signature with the inferred type) and collect a `Constraint { class, type_: inferred_type }` into a per-FnDef `residuals: Vec` accumulator threaded through the body check. - [ ] **Step 6b: Build the expanded declared-constraints set** After the body finishes, take `FnDef.ty`'s `Forall.constraints` (or empty if the type is not `Forall`) and expand: for every declared `(C, t)`, walk `C`'s superclass chain (max one step per class per Decision 11) and append every `(S, t)`. The expanded set is what counts as "satisfied" for residual matching. - [ ] **Step 6c: Compare residuals; emit `MissingConstraint` for var-shaped uncovered** For each residual whose `type_` is `Type::Var { name }`: if no expanded declared constraint matches `(class, type_)`, emit `CheckError::MissingConstraint`. Concrete-type residuals are deferred to Task 10. - [ ] **Step 7: Register the new code in `diagnostic.rs`** Add `missing-constraint` to the in-source kebab-case-code registry comment in `crates/ailang-check/src/diagnostic.rs` (the same comment block where existing codes like `duplicate-type` are listed), with a one-line description: ``` - `missing-constraint` — fn body calls a class method but its declared constraints do not cover the inferred class-param type. ``` - [ ] **Step 8: Run RED tests, expect pass** Run: `cargo test -p ail fn_calling_class_method` Run: `cargo test -p ail fn_with_correct_constraint` Expected: both PASS. - [ ] **Step 9: Run full suite** Run: `cargo test --workspace` Expected: PASS. - [ ] **Step 10: Commit** ```bash git add crates/ailang-check/src/lib.rs crates/ailang-check/src/diagnostic.rs \ examples/test_22b2_missing_constraint.ail.json \ examples/test_22b2_constraint_declared.ail.json \ crates/ail/tests/typeclass_22b2.rs git commit -m "iter 22b.2.9: missing-constraint per-fn diagnostic" ``` --- ## Task 10: Per-FnDef typecheck — `no-instance` diagnostic **Files:** - Modify: `crates/ailang-check/src/lib.rs` (handle the concrete-residual path: look up the registry, emit `NoInstance` if absent) - Modify: `crates/ailang-check/src/diagnostic.rs` (registry comment) - Create: `examples/test_22b2_no_instance.ail.json` - [ ] **Step 1: Write the fixture** `examples/test_22b2_no_instance.ail.json`: ```json { "schema": "ailang/v0", "name": "test_22b2_no_instance", "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": "main", "type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, "effects": [] }, "params": [], "body": { "t": "seq", "lhs": { "t": "app", "fn": { "t": "var", "name": "show" }, "args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }] }, "rhs": { "t": "lit", "lit": { "kind": "unit" } } } } ] } ``` Calls `show 42` with no `instance Show Int` declared. - [ ] **Step 2: Write the Rust RED test** In `crates/ail/tests/typeclass_22b2.rs` (the file created in Task 9): ```rust #[test] fn fn_calling_class_method_at_type_without_instance_fires_no_instance() { let entry = std::path::PathBuf::from( "../../examples/test_22b2_no_instance.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 == "no-instance"), "expected no-instance; got: {:?}", diagnostics.iter().map(|d| &d.code).collect::>() ); } ``` - [ ] **Step 3: Run RED test, expect failure** Run: `cargo test -p ail fn_calling_class_method_at_type_without_instance` Expected: FAIL. - [ ] **Step 4: Add `CheckError::NoInstance`** ```rust #[error( "fn `{def}` calls `{method}` requiring `instance {class} {at_type}`, but no such instance exists in the workspace" )] NoInstance { def: String, method: String, class: String, at_type: String, }, ``` Add to `code_of()`: ```rust CheckError::NoInstance { .. } => "no-instance", ``` - [ ] **Step 5: Handle the concrete-residual path in `check_fn`** In the residual-handling loop (Task 9 step 6): when the residual's `type_` is a fully-concrete `Type` (no `Type::Var`), compute its canonical type-hash and look up `(class, type-hash)` in `workspace.registry.entries`. If absent, emit `NoInstance`. The existing concrete-vs-var branching from Task 9 hooks here. - [ ] **Step 6: Register the new code in `diagnostic.rs`** Add to the kebab-case-code registry comment: ``` - `no-instance` — class-method call with a fully-concrete class-param type that has no matching workspace registry entry. ``` - [ ] **Step 7: Run RED test, expect pass** Run: `cargo test -p ail fn_calling_class_method_at_type_without_instance` Expected: PASS. - [ ] **Step 8: Run the full suite + bench gates** ```bash cargo test --workspace python3 bench/check.py python3 bench/compile_check.py python3 bench/cross_lang.py ``` Expected: all green. Bench gates verify no regressions on pre-22b fixtures (which never invoke classes, so the check-fn extension is a no-op for them). - [ ] **Step 9: Commit** ```bash git add crates/ailang-check/src/lib.rs crates/ailang-check/src/diagnostic.rs \ examples/test_22b2_no_instance.ail.json \ crates/ail/tests/typeclass_22b2.rs git commit -m "iter 22b.2.10: no-instance per-fn diagnostic" ``` --- ## Final iter step: JOURNAL entry After all 10 tasks land green, append a JOURNAL entry under `docs/JOURNAL.md` summarising: - 8 new diagnostics (3 class-schema, 3 workspace-coherence, 2 per-fn) wired through workspace-load and check-time - `Type::Forall.constraints` slot active; pre-22b.2 fixtures hash bit-identical via `skip_serializing_if` - `build_module_globals` now resolves class methods as global names - All bench gates green; all `test_22b1_*` and `test_22b2_*` fixtures green / fire their expected diagnostics - 22b.3 next: monomorphisation pass (synthesise FnDefs from `(method, type-hash)` pairs, rewrite calls), with a synthetic class+instance fixture for end-to-end mono validation before the Prelude lands in 22b.4 Commit: ```bash git add docs/JOURNAL.md git commit -m "iter 22b.2: typecheck arms — 8 diagnostics shipped" ```