Files
AILang/docs/journals/2026-05-13-iter-bugfix-instance-body-unbound-var.md
T
Brummel 77f584abbb iter bugfix-instance-body-unbound-var: GREEN — walk instance method bodies through check_fn
Fixes the bug RED-pinned in 72f3f65: an unbound identifier inside an
instance method body slipped past `ail check` (false-OK exit 0) and
surfaced only at `ail build` as the degraded internal-error diagnostic
`monomorphise_workspace: unknown identifier: <name>` with no source
location, symbol kind, or "did you mean" candidates.

Root cause: `check_def` in `crates/ailang-check/src/lib.rs:1574-1595`
early-returned `Ok(())` for `Def::Class(_) | Def::Instance(_)` without
ever invoking the body walker. The arm's comment claimed iter 22b.2
landed instance-body typechecking; that wiring was never implemented.
Only the workspace-load coherence checks (Orphan/Duplicate/
MissingMethod) touched instance defs, and they inspect schema only,
not the method-body identifier graph.

Fix: split the combined arm so `Def::Class(_)` stays schema-only at
this layer, while `Def::Instance(inst)` routes through a new helper
`check_instance` that lifts each `InstanceMethod`'s `Term::Lam` body
into a synthetic `FnDef`, applies class-method substitution (class
param -> instance type, via the existing `substitute_rigids` /
`substitute_rigids_in_term` helpers), and hands it to `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 "minimal fix, no surrounding cleanup" constraint: no widening
to Def::Class (still schema-only); no changes to the
monomorphise_workspace diagnostic wording (defence-in-depth
internal-error path stays as backstop); no changes to mono.rs,
codegen, or any other crate.

Tests: 557 baseline + 2 new RED -> 559 green. Both RED pins in
unbound_in_instance_method_pin.rs now PASS. fn-body level
unbound-var path stays green. All 8 carve-out fixtures classify
unchanged.

Known debt (out of scope per carrier; queued in journal):
`check_instance` does not yet cross-check the substituted method
signature against the class's `ClassMethodEntry.method_ty` — a
malformed instance declaring wrong types in its Lam would still
typecheck cleanly. The body-walk catches unbound vars and
effect-mismatches against the Lam's own declared types, but not
class-method-signature mismatch.
2026-05-13 12:09:39 +02:00

4.2 KiB

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

Date: 2026-05-13 Started from: 72f3f6541b 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.rscheck_def arm split, new check_instance helper.

Stats

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