# iter bugfix-instance-body-unbound-var — instance method bodies now walk through check_fn

**Date:** 2026-05-13
**Started from:** 72f3f6541b3e41f2d4a7706dcad5bd784ec07d81
**Status:** DONE
**Tasks completed:** 1 of 1

## Summary

Fixes the bug surfaced by the 2026-05-13 form-a fieldtest: an unbound
identifier inside an `instance` method body slipped past `ail check`
and surfaced only at `ail build` as the degraded internal-error
diagnostic `monomorphise_workspace: unknown identifier: ...`. Root
cause: `check_def`'s `Def::Class(_) | Def::Instance(_)` arm at
`crates/ailang-check/src/lib.rs:1574-1595` early-returned `Ok(())`
without ever invoking the body walker — the comment claimed "instance-
body typechecking (with class-method substitution) land[s] in 22b.2",
but that wiring was never implemented. Workspace-load coherence checks
(`build_registry`'s Orphan/Duplicate/MissingMethod) only inspect
instance schema, not method-body identifier graphs.

Fix is the minimum-scope change at that call site: split the combined
arm so `Def::Class(_)` continues to be schema-only, while
`Def::Instance(inst)` calls a new helper `check_instance` that, for
each `InstanceMethod`, lifts the body `Term::Lam` into a synthetic
`FnDef` (class-method-substituted via `substitute_rigids` /
`substitute_rigids_in_term` against the class's `param` name as
declared in `env.class_methods[(qualified_class, method)]`) and
hands it to the existing `check_fn`. The reuse of `check_fn` is what
gets the body walked through the same identifier-resolution path as
fn bodies, so an unbound name fires `[unbound-var]` at `ail check`
with exit 1 and the standard structured diagnostic.

Per the carrier's "minimal fix, no surrounding cleanup" constraint:
no widening to `Def::Class` (still schema-only); no changes to the
`monomorphise_workspace` diagnostic wording (the defence-in-depth
internal-error path stays as a backstop); no changes to mono.rs,
codegen, or any other crate.

## Per-task notes

- iter bugfix-instance-body-unbound-var.1: split the
  `Def::Class(_) | Def::Instance(_)` arm into `Def::Class(_) =>
  Ok(())` and `Def::Instance(inst) => check_instance(inst, env,
  out_warnings)`. New free fn `check_instance` reads
  `env.class_methods[(qualify_class_ref_in_check(&inst.class,
  &env.current_module), im.name)]` to recover the class's `param`
  name, builds a `{class_param: inst.type_}` substitution, applies
  it to the Lam's `param_tys` / `ret_ty` and to the Lam's body
  Term (via the existing `substitute_rigids` /
  `substitute_rigids_in_term` helpers), then constructs a synthetic
  `FnDef` (`name = "<class>::<method>"`, `ty =
  Type::fn_implicit(subst_param_tys, subst_ret_ty,
  lam.effects)`, params from Lam, body = substituted Lam body)
  and calls `check_fn` on it.

  First-attempt sketch wrapped the Lam's `param_tys` / `ret_ty`
  unsubstituted on the assumption that the on-disk canonical form
  was post-substitution — the `codegen_import_map_fallback_pin`
  regression (and a sweep of the prelude `(typed x a) (typed y a)`
  shape) immediately disproved that. The substitution step was
  added at the second iteration of the implementer phase, before
  reaching the spec / quality phases.

  Non-Lam method bodies are silently skipped — current schema
  doesn't admit them and the pre-fix behaviour was also silent.

## Concerns

- None.

## Known debt

- `check_instance` does **not** verify that the substituted method
  signature matches the corresponding `ClassMethodEntry.method_ty`
  (with `class_param ↦ inst.type_`). The body-walk catches unbound
  vars and effect-mismatches against the Lam's own declared types,
  but a malformed instance whose Lam declares
  `(typed x (con Int)) (typed y (con Int)) -> Bool` for an
  `instance Eq Bool` body would currently typecheck cleanly. The
  workspace-load `MissingMethod` / coherence checks only inspect
  the (class, type) pair, not the method's declared type. Out of
  scope per the carrier's "minimal fix" constraint; queued as a
  follow-up roadmap candidate ("instance-method declared-type
  vs class-method-type cross-check").

## Files touched

- `crates/ailang-check/src/lib.rs` — `check_def` arm split,
  new `check_instance` helper.

## Stats

bench/orchestrator-stats/2026-05-13-iter-bugfix-instance-body-unbound-var.json
