Wado

WEP: Library-Defined Derivation: Reflect Extensions and the #[validate] Attribute

Context

Wado derives a growing set of type-directed traits — Eq, Ord, Inspect, Serialize, Deserialize, Default — by bespoke compiler synthesis (each hand-written into synthesis/, e.g. serde_synth.rs). Every new type-directed capability has so far meant another hardcoded synthesizer. That model has two limits that a concrete new requirement now makes acute:

  1. It does not scale: each capability is compiler work, and the compiler grows a special case per trait.
  2. It is closed to libraries. A third-party package cannot get a synthesizer, and Wado has no macros and no dynamic reflection, so it cannot introspect a type itself.

The forcing function is wep-2026-06-13-jade.md (JSON Schema for Wado). Jade's capability B — "given a Wado type, produce its JSON Schema" — is exactly a type-directed derivation, but Jade is an ordinary package (wado:jade), not the compiler. It cannot be a synthesizer. Either type→schema is impossible as library code, or the language grows a general facility that lets libraries derive over a type's structure.

The escape was already chosen, in wep-2026-03-14-variadic-type-parameters.md §10: Reflect, a compiler-synthesized trait that exposes a struct's fields as a typed tuple at compile time (type Fields = [..F], field_names()), in the same static, monomorphized, no-dynamic-reflection lineage as tuple for-of and [..T::default()]. That WEP's stated goal is to "remove compiler-magic struct Inspect; replace with the Reflect-based impl" — i.e. move derivation out of the compiler into library code. The variadic substrate (type packs, tuple for-of, [..T::method()] expansion, variadic trait impls) is implemented; the two pieces derivation most needs are designed but unbuilt (WEP 2026-03-14 checklist): Reflect per-struct synthesis, and where-clause pack binding T: Trait<Assoc = [..F]>.

Bringing Jade's needs to that design surfaced gaps. Current Reflect exposes only field name + type + value. Schema derivation (and, once migrated, serde itself) additionally needs each field's wire name (serde rename / rename_all applied), whether it has a default (→ JSON Schema required), its doc string (→ description), and the validation constraints (minLength, minimum, pattern, format, …) that no type metadata carries today — and that a library cannot add via an attribute of its own.

This WEP evolves WEP 2026-03-14 §10 to close those gaps generically, and adds a single generic built-in #[validate(…)] attribute. It is the language-side dependency of the Jade WEP; Jade ships no compiler change and consumes only what is decided here.

Scope

In scope:

Out of scope (recorded as future directions, not built here):

Decision

Principle: derivation is library code over Reflect

The compiler's only job is to expose a type's structure at compile time. Every derivation — the built-in Inspect / serde / Default, Jade's JsonSchema, and future user-written ones — is a generic library impl over Reflect, resolved at monomorphization. No per-capability synthesizer, no macros, no dynamic reflection. The canonical shape, using the where-clause pack binding established by WEP 2026-03-14 §11:

impl<T, ..F: SomeTrait> SomeTrait for T
where T: Reflect<Fields = [..F]>
{
    fn method(&self) -> R {
        let meta = Reflect::<T>::field_meta();   // value-level per-field metadata
        let parts = [..F::method_of()];          // type-level, value-free, per field type
        // combine meta + parts …
    }
}

1. Finish Reflect (the two unbuilt items from WEP 2026-03-14)

1a. API surface: a sealed trait, called as Reflect::<T>::…

Reflect is a sealed trait — the compiler synthesizes impl Reflect for S for every eligible struct, and a user impl Reflect for T is a compile error. Sealing is what lets a derivation trust the projection: a program cannot forge a type's reflection. Reflect stays a trait (not a struct or a builtin type) because the derivation mechanism binds it as where T: Reflect<Fields = [..F]> (§1) — only a trait can carry that bound and its associated Fields pack.

Its members are reached only through the trait-qualified form, with the subject type as a turbofish on the trait:

Reflect::<Point>::type_name()     // "Point"
Reflect::<Point>::field_names()   // ["x", "y"]
Reflect::<Point>::fields(&p)      // [p.x, p.y]

The trait-qualified form is the only spelling: the metadata is introspection about a type, not part of the type's own API, so it lives in the Reflect namespace and never appears among a struct's own methods (a struct's method namespace is entirely the author's). In generic code the same form applies to a type parameter — Reflect::<T>::field_names().

2. Extend Reflect with derivation metadata

The extension is purely additive over the §10 signature — type Fields, fields(&self), field_names(), and type_name() carry over unchanged, and two members are added. Reflect is a sealed, compiler-only trait, so it is internal and anchored — trait and each callable member — through the compiler-item registry:

pub struct FieldMeta {
    name: String,            // source name, e.g. "user_name" (== field_names()[i])
    wire_name: String,       // serde rename / rename_all applied, e.g. "userName"
    has_default: bool,       // field has a default value `f: T = expr`
    doc: String,             // /// doc comment ("" if none)
    validate: List<ValidateEntry>,   // parsed #[validate(...)] (see §4)
    secret: bool,            // field is #[secret]; its Fields slot is Secret<F_k>
}

#[compiler_item("reflect")]
internal trait Reflect {
    type Fields;                          // [F_0, F_1, …]  — type-level pack  (§10)
    #[compiler_item("reflect_fields")]
    fn fields(&self) -> Self::Fields;     // value tuple (§10)
    #[compiler_item("reflect_field_names")]
    fn field_names() -> List<String>;     // source names  (§10)
    #[compiler_item("reflect_type_name")]
    fn type_name() -> String;             // (§10)
    fn field_meta() -> List<FieldMeta>;   // added: wire name, default, doc, validate
    fn type_doc() -> String;              // added: /// on the type itself
}

field_meta() keeps index correspondence with field_names() and the type-level Fields pack. A #[secret] field occupies its slot in Fields as the value-opaque Secret<F_k> projection — see Struct Walkability. Two channels are unavoidable and intentional: field types must stay a type-level pack (Fields = [..F]) so [..F::method()] can expand; everything else is value-level metadata (field_meta()). A derivation zips the two by index. Like the original, Reflect is compiler-synthesized, cannot be user-implemented, and is callable only in monomorphized contexts.

3. Variant / enum / flags reflection

Reflect as designed is struct-only. Sum and bitmask types get analogous compile-time introspection so a derivation can lower a variant to JSON Schema oneOf, an enum to a string enum, and flags to its bit set. These follow §1a: each is a sealed internal trait reached only as ReflectVariant::<T>::… (never a bare T::case_meta()):

#[compiler_item("reflect_variant")]
internal trait ReflectVariant {
    type Cases;                          // payload types as a pack ([P_0, P_1, …])
    fn case_meta() -> List<CaseMeta>;    // name, wire_name, discriminant, is_unit, doc
    fn type_name() -> String;
}
// ReflectEnum (discriminants + names), ReflectFlags (bit names) likewise.

The exact unification (one Reflect that reports a type kind, vs. three traits) is an API question (see "Open questions"); the capability set is fixed here.

4. The #[validate(…)] attribute

A single generic, built-in attribute with a closed declarative vocabulary — not owned by any library:

struct CreateUser {
    #[validate(min_length = 1, max_length = 64)]
    user_name: String,
    #[validate(format = "email")]
    email: String,
    #[validate(minimum = 0, maximum = 150)]
    age: i32 = 0,
}

Recognized keys (initial set): min_length / max_length, minimum / maximum / exclusive_minimum / exclusive_maximum, multiple_of, pattern, format, min_items / max_items, unique_items. The compiler parses these into ValidateEntry values and does two things with them — and together they are what stop the attribute from being silently inert, the failure mode of Rust's validator crate (annotate, but nothing runs unless you remember to call .validate()):

description is not a #[validate] concern: it comes from /// doc comments, surfaced as FieldMeta::doc / type_doc().

5. Migrate bespoke synthesizers to library code (staged)

Once §1–§3 land, the hand-written synthesizers become generic library impls over Reflect, shrinking the compiler's special-case surface:

  1. Inspect / InspectAlt first — already the stated goal of WEP 2026-03-14; the lowest-risk proof.
  2. serde Serialize / Deserialize — the struct/variant walks move to library code. Deserialize's #[validate] enforcement moves with it, now reading FieldMeta::validate; the compiler is left exposing the entries via Reflect, nothing more.
  3. Default[..F::default()] over the field pack.

Each migration is its own PR with golden-output parity against the current synthesizer. The end state: the compiler synthesizes Reflect (and exposes #[validate] through it); everything else, enforcement included, is library code.

Implementation checklist

Ordered so each step is independently testable; Layer-B-of-Jade is unblocked after the first three.

Future directions

Consequences

Benefits

Costs and trade-offs

Open questions