Wado

WEP: Trait Derivation Policy — Bound-Driven Synthesis

Status: Implemented. Eq / Ord / Default / Serialize / Deserialize are demand on_bound: a body is synthesized only where a reference needs it, discovered through the shared bound_driven_synth_requests channel a bound check or explicit marker records into. They are not total — a fn-typed field blocks Eq / Ord / serde, a field without a default blocks Default — so a T: Trait obligation there is real, and gating avoids code-size waste on types that never use the trait.

The debug format traits (Inspect / InspectAlt) are total on_bound: every type is structurally debug-formattable, so a T: Inspect obligation always holds (for a type parameter or any concrete type), and impl Inspect for T; is a conformance check that always validates. Because they are total and universal debug output is a feature rather than waste, their generation stays eager (a body for every type kind) — gating it would demand a discovery mechanism for {v:?} over an unbounded type param (whose concrete reference only materializes at monomorphize) with no offsetting code-size benefit.

The display traits (Display / DisplayAlt) are not total (revised 2026-07-15). Display is never auto-derived for a struct, variant, or generic container — a type has a human-facing string representation only if someone wrote impl Display, so {x} on such a type is a compile error (use {x:?} for debug output), and T: Display is a real obligation. The two exceptions are types with an unambiguous canonical string form, which auto-derive it: a plain enum displays its bare case name (Red, distinct from Inspect's type-qualified Color::Red), and a newtype inherits its base type's Display transparently (a Meters = f64 renders 3.14, no as Name tag). DisplayAlt ({x:#}) tracks Display: it auto-derives a fallback delegating to Display only where a Display impl exists. A policy declaration for user-defined traits is open. See Open Questions.

Context

Wado derives type-directed traits under two inconsistent policies — the rule for when a derived impl exists for a type T:

The split is ad hoc: serde's marker is boilerplate the compiler could discharge structurally (as it already does for Inspect), and it makes anonymous-struct serialization impossible (no name to write a marker against). Eq / Ord, meanwhile, synthesize for every declared type regardless of use — compile-time and code-size waste for types the program never compares.

Orthogonal to Library-Defined Derivation (Reflect), which decides how an impl is written. This WEP decides when one is instantiated for a given T.

Forcing functions

Decision

A per-trait derivation policy

Policy A T: Trait obligation is satisfied by … Generation Examples
on_bound (demand) structural synthesis, on demand when a reference requires it on demand Eq, Ord, Default, Serialize, Deserialize
on_bound (total) always — the trait is total over all types eager Inspect, InspectAlt
display a real impl Display, a plain enum (bare case name), or a Display base enum: eager body; newtype: inherited at call site Display, DisplayAlt
explicit only a written impl Trait for T; (or full manual impl) on demand default for user traits

There is no longer an automatic policy: its members split by totality. Default joined the demand on_bound traits (Eq / Ord / serde) — it is not total (a field without a default blocks it), so gating its generation saves code on structs never defaulted. The debug format traits stayed eager but gained on_bound obligation semantics (see below). DisplayAlt synthesizes a fallback that delegates to Display.

The debug format traits Inspect / InspectAlt are total: every type is structurally debug-formattable (an Inspect body is generated for every type kind — struct, enum, variant, flags, newtype, tuple, closure, opaque resource). Totality is a type-system fact — a T: Inspect obligation always holds, for a type parameter or any concrete type — so those bounds compile where the old policy rejected them, and an impl Inspect for T; marker always validates. Generation stays eager: a total trait wastes nothing meaningful by existing for every type (universal debug output is the point), and gating it would need a discovery mechanism for {v:?} over an unbounded type param — whose concrete T^Inspect reference only materializes at monomorphize, after synthesis — for no code-size gain.

Display / DisplayAlt are the display policy — deliberately not total (revised 2026-07-15). A human-facing string representation is not something the compiler can invent structurally the way it can a debug dump: for a struct there is no canonical field layout, delimiter, or ordering to pick, so Display is left to the author. {x} on a type with no Display is a compile error (use {x:?}), and T: Display is a real obligation. Two type kinds are the exception because their string form is canonical and unambiguous, so the compiler derives it eagerly:

DisplayAlt derives a fallback delegating to Display only where a Display impl exists (manual, enum-synthesized, or newtype-inherited), so the alternate display of a Display-less type is likewise a compile error. The demand on_bound traits are not total (a fn-typed field, a field without a default, blocks them), so a T: Trait there is a real obligation and gating its generation removes genuine waste.

Bound-driven synthesis semantics

An on_bound obligation T: Trait is satisfied structurally: no manual impl exists, and every field/case of T satisfies Trait recursively (Default instead requires every field to carry a default expression). On failure, the error reason-chains from the bound site to the offending field/case (Diagnostic Reason Chains).

A marker for any of the structurally-checkable traits (Eq / Ord / Default / Serialize / Deserialize) validates T at its own span and is a hard compile error if ineligible, then records the request exactly as a bare bound does.

Whole-program and monomorphized, so there's no orphan rule to violate. Generic types record nominally against the base declaration — the many instantiations collapse onto one request, and synthesis emits a generic template that monomorphize instantiates per concrete type. Serialize / Deserialize templates are additionally generic over the serializer type S / D (the Deserialize FieldSchema keying keeps the next_field selector on the base type — see Serde).

Discovery mechanism

Discovery applies to the demand on_bound traits (Eq / Ord / Default / serde) only; the total debug traits and the display auto-derivations generate unconditionally and need none. A demand obligation is satisfiable only if the reference that needs it can be found, and discovery funnels into one shared set, TypeTable::bound_driven_synth_requests: the pre-monomorphize synthesis pass reads it and emits a body (concrete or generic template) for each recorded (type, trait) pair, gated so nothing is generated for a pair no reference recorded.

Each demand reference records at its own resolution site: Eq / Ord at operator dispatch and == / < method lowering (type_implements_trait records while it recurses through fields, so a struct and every field it reaches are recorded together); Default at a T: Default bound check or a P::default() static-call resolution; serde at a T: Serialize / T: Deserialize bound. None of these has an unbounded reference path — a value is compared, defaulted, or serialized only through a bound or a concrete call the resolver sees — so per-site recording is complete.

A total debug trait has no such gate: type_implements_trait short-circuits true for it (totality), and generation is eager, so {v:?} over an unbounded type param — whose concrete T^Inspect reference only appears after monomorphize substitutes T — always finds its body already emitted. This is exactly the case that made gating the debug traits unattractive, and eager generation sidesteps it. Display does not face this: it is not total, so {v} over an unbounded T is a compile error unless T: Display is written, and a bounded generic only instantiates with Display types — no post-mono discovery gap.

An explicit marker feeds the demand request set: it validates structurally at its own span (hard error if ineligible) and records the request like a bound. An Inspect / InspectAlt / DisplayAlt marker validates but records nothing meaningful (generation is already eager). This is scoped to compiler-synthesized bodies; a hand-written impl Trait for T { … } is ordinary source, type-checked because it exists and left to ordinary dead-code elimination.

Policy assignment

The trust boundary

Serialize / Deserialize cross a wire/storage boundary, so on_bound means a type becomes serializable the moment some code asks, and a later field addition silently extends the wire shape — why Rust serde and Swift Codable are opt-in. Wado accepts the trade-off: its whole-program model has no downstream consumers to surprise. A manual impl or field-level #[secret] remain the levers for tighter control; no dedicated opt-out is introduced.

Eq / Ord cross no data boundary — on_bound only changes when their impl is generated, never what any == / < call site returns. Their motivation is pure compile-time / code size, with no opt-out to weigh.

Consequences

Benefits

Trade-offs

Relationship and prerequisites

Ships directly against the existing bespoke synthesizers (synthesis::serde_synth, synthesis::traits), not against Reflect, which remains unbuilt. The original plan was to land this after migrating serde onto a Reflect-based impl, but the two turned out independent — this WEP only changes when a request is created, not how the body is written. A future Reflect-based rewrite can land later against the same plumbing.

Alternatives Considered

Open Questions