diff --git a/docs/plans/2026-05-09-22b2-typecheck-arms.md b/docs/plans/2026-05-09-22b2-typecheck-arms.md new file mode 100644 index 0000000..ab64b5b --- /dev/null +++ b/docs/plans/2026-05-09-22b2-typecheck-arms.md @@ -0,0 +1,1841 @@ +# 22b.2 — Typecheck Arms — Implementation Plan + +> **Parent spec:** `docs/specs/2026-05-09-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" +```