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.
56 KiB
22b.2 — Typecheck Arms — Implementation Plan
Parent spec:
docs/specs/0002-22-typeclasses.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto 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— addConstraintstruct, addconstraints: Vec<Constraint>field toType::Forall - Modify:
crates/ailang-core/src/workspace.rs— add 7WorkspaceLoadErrorvariants, addvalidate_classdefsfn, extendbuild_registrywith three new checks - Modify:
crates/ail/src/main.rs— extend theWorkspaceLoadError → Diagnosticmapping with 7 new arms - Modify:
crates/ailang-check/src/lib.rs— extendbuild_module_globalsto register class methods, addMissingConstraintandNoInstancetoCheckError, extendcheck_fnwith 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(afterInstanceMethod, beforeConstDef; inenum Type, theForallvariant) -
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
Constraintstruct
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
constraintsfield toType::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 fnvalidate_classdefs, new error variantKindMismatch; call site inload_workspacebetweenvisitloop andbuild_registry) -
Modify:
crates/ail/src/main.rs(addKindMismatcharm in theWorkspaceLoadError → Diagnosticmatcher 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::KindMismatchvariant
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_classdefsand 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 variantInvalidSuperclassParam; extendvalidate_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 variantUnboundConstraintTypeVar; extendvalidate_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; extendbuild_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, extendbuild_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::MethodNameCollisionvariant
/// 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 ofbuild_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(extendbuild_module_globalsto 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_globalsshape
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_globalsandGlobalsshape
Add a field class_methods: BTreeMap<String, ClassMethodEntry> (or
match existing naming) where ClassMethodEntry carries:
class_name: Stringclass_param: Stringmethod_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(extendcheck_fnto collect residual constraints at class-method call sites; addCheckError::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_fnto 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
MissingConstraintfor 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, emitNoInstanceif 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.constraintsslot active; pre-22b.2 fixtures hash bit-identical viaskip_serializing_ifbuild_module_globalsnow resolves class methods as global names- All bench gates green; all
test_22b1_*andtest_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"