Wado

WEP: Diagnostic Reason Chains for Type and Trait Errors

Context

Wado source — including this compiler — is increasingly written and repaired by AI coding agents. The error messages the compiler emits are therefore read by agents as much as by humans, and the two audiences have different needs.

Krishnamurthi and Flatt, "Type-Error Ablation and AI Coding Agents" (arXiv:2606.01522, 2026), ran a controlled experiment on an ML-style language and found:

An inventory of Wado's diagnostics placed them at the paper's proximate level: a single span plus both the expected and found type names, but no chain of expressions or causes. Two concrete gaps stood out, both confirmed by writing and compiling Wado programs:

Unlike the paper's proximate-vs-all framing, Wado anchors types at function signatures (parameters require annotations), so ordinary type mismatches are already local. The places where Wado's cause is genuinely far from the blame are trait resolution and generic inference — the same territory as interactive Rust trait-error debuggers.

Decision

Make diagnostics richer by default. We explicitly reject a separate "AI mode": a single detailed default serves both audiences and avoids a configuration surface that agents would have to discover. The MVP ships two changes that move trait/operator diagnostics from proximate toward all.

Operator errors

Add TypeError::OperatorNotApplicable { op, operands, note } and route every operator type error through it. The requires-trait check inspects both operands, so a pair that cannot combine under the operator produces one symmetric message naming both types regardless of operand order, and the misleading "invalid pattern:" prefix is gone (see Examples).

Trait-bound reason chains

Add trait_unimpl_reason_chain(type_id, trait_name), which walks the Eq/Ord auto-derive structure (struct fields, generic-struct fields with type-argument substitution, and variant payloads), finds the first member that breaks the trait, and recurses into it. The result is a step-by-step chain, depth-bounded and cycle-guarded, carried on TraitBoundNotSatisfied and rendered by default as indented note: lines beneath the headline at every bound-check site (see Examples).

Rendering mechanism

The MVP folds the chain into the existing Diagnostic.message string (the same pattern as OperatorNotApplicable's note). Diagnostic is constructed in ~85 places and carries a single optional span; adding structured secondary-span notes would touch all of them, so it is deferred until the value is proven. Substring-matched compile_error fixtures keep working because the headline is unchanged and notes are appended.

Examples

All examples are the actual compiler output on the MVP. The location prefix (file:line:col:) and the leading timestamp are elided for brevity.

Operator error — symmetry

The same defect under both operand orders previously read as two unrelated errors. Source:

fn f(a: i32, lst: List<i32>) -> i32 { return a - lst; }   // and the mirror: lst - a

Before:

// a - lst
error: type mismatch: expected 'i32', found 'List<i32>'
// lst - a
error: invalid pattern: operator `-` cannot be applied to type `List<i32>`: type does not implement `Sub`

After:

// a - lst
error: operator `-` cannot be applied to types `i32` and `List<i32>`
// lst - a
error: operator `-` cannot be applied to types `List<i32>` and `i32`

Operator error — same type, missing trait impl

Source:

struct V2 { x: i32 }
let _ = a - b;   // a, b: V2, no `impl Sub for V2`

Before:

error: invalid pattern: operator `-` cannot be applied to type `V2`: type does not implement `Sub`

After:

error: operator `-` cannot be applied to type `V2`: type does not implement `Sub`

Trait bound — single field

Source:

struct Handler { cb: fn(i32) -> i32 }
fn smallest<T: Ord>(a: T, b: T) -> T { if a < b { return a; } return b; }
let _ = smallest(Handler { cb: |x: i32| x }, Handler { cb: |x: i32| x });

Before:

error: type 'Handler' does not implement trait 'Ord' required by bound on 'T'

After:

error: type 'Handler' does not implement trait 'Ord' required by bound on 'T'
  note: `Handler` does not implement `Ord` because field `cb` of type `fn(i32) -> i32` does not implement `Ord`

Trait bound — recursive chain

Source:

struct Inner { f: fn() -> i32 }
struct Outer { inner: Inner }
fn smallest<T: Ord>(a: T, b: T) -> T { if a < b { return a; } return b; }
let _ = smallest(Outer { inner: Inner { f: || 1 } }, Outer { inner: Inner { f: || 2 } });

Before:

error: type 'Outer' does not implement trait 'Ord' required by bound on 'T'

After:

error: type 'Outer' does not implement trait 'Ord' required by bound on 'T'
  note: `Outer` does not implement `Ord` because field `inner` of type `Inner` does not implement `Ord`
  note: `Inner` does not implement `Ord` because field `f` of type `fn() -> i32` does not implement `Ord`

Consequences

Shipped and validated:

Trade-offs and known limits:

Next steps, in priority order toward the paper's all level: