Files
AILang/docs/plans/0002-22b2-typecheck-arms.md
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

56 KiB

22b.2 — Typecheck Arms — Implementation Plan

Parent spec: docs/specs/0002-22-typeclasses.md

For agentic workers: REQUIRED SUB-SKILL: use skills/implement to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Land the eight 22b.2 diagnostics (three class-schema, three workspace-load coherence, two per-FnDef) and activate the constraints slot on Type::Forall so polymorphic FnDefs can declare class constraints. After 22b.2, the typechecker rejects every kind of ill-formed class / instance / constraint shape that 22b.3's monomorphisation pass would otherwise have to defend against.

Architecture: Two extension points. (1) crates/ailang-core/src/ workspace.rsvalidate_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.rsbuild_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:

#[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:

/// 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:

    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
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:

{
  "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):

#[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:

    /// 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:

/// 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):

    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:

        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
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:

{
  "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:

#[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:

    /// 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:

                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:

        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
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:

{
  "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
#[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
    /// 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:

                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
        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
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:

{
  "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
#[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
    /// 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:

                    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:

        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
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:

{
  "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:

{
  "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)
#[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
    /// 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):

    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
        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
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:

{
  "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
#[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
    /// 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:

    // 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
        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
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:

{
  "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):

#[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
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:

{
  "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.)

{
  "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):

#[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):

    #[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():

            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
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:

{
  "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):

#[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
    #[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():

            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
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
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:

git add docs/JOURNAL.md
git commit -m "iter 22b.2: typecheck arms — 8 diagnostics shipped"