# 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<Constraint>` 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<String>,
        /// 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<Constraint>,
        body: Box<Type>,
    },
```

- [ ] **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<String, Module>,
) -> 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<String, String> = 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<String, ClassMethodEntry>` (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::<Vec<_>>()
    );
}

#[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<Diagnostic>` (`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<Constraint>` 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::<Vec<_>>()
    );
}
```

- [ ] **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"
```
