Wado

WEP: Format Traits

Context

WEP: Template Format Specifiers defines what ${x:spec} accepts. This WEP defines what it dispatches to: the trait per specifier, the Formatter they write through, and which types have which impl.

Decision

Formatter

A format trait writes into a Formatter, which carries the parsed spec and a reference to the caller's buffer. It does not own the buffer, and the spec fields are embedded rather than boxed, so formatting one value allocates no GC struct of its own.

enum Alignment {
    Left,
    Center,
    Right,
}

struct Formatter {
    fill: char,
    align: Alignment,
    sign_plus: bool,   // `+`
    alternate: bool,   // `#`
    zero_pad: bool,    // `0`
    width: i32,        // NO_WIDTH (-1) when the spec set none
    precision: i32,    // PRECISION_DEFAULT (-2) when the spec set none
    indent: i32,       // nesting depth of the alternate `Inspect`
    buf: &mut String,
}

The fields are public and read directly; there are no accessors.

impl Formatter {
    fn new(buf: &mut String) -> Formatter;

    // Writing
    fn write_str(&mut self, s: &String);
    fn write_char(&mut self, c: char);
    fn write_char_n(&mut self, c: char, n: i32);
    fn write_from_memory(&mut self, ptr: i32, len: i32);

    // Padding
    fn pad(&mut self, content: String);
    fn mark(&self) -> i32;
    fn apply_padding(&mut self, start_pos: i32);
    fn prepare_int_write(&mut self, is_negative: bool, alt_prefix: String, digit_count: i32) -> i32;

    // Alternate `Inspect`
    fn write_indent(&mut self);
    fn write_newline_indent(&mut self);
    fn open_brace(&mut self, open: String);
    fn close_brace(&mut self, close: String);

    fn resolved_seq_limit(&self) -> i32;
}

Three padding paths exist because content reaches the buffer three ways:

Method Input Used by
pad a rendered String String, char, bool
mark / apply_padding content already written at an offset floats, composite renderers
prepare_int_write a digit count, to fill backwards integers

prepare_int_write places the sign, the radix prefix and the padding itself, then returns the offset of the reserved digit area. It writes alt_prefix only when alternate is set, so a caller passes "0x" unconditionally rather than choosing between it and an empty string — an empty String would leave a value copy behind that no pass removes.

Width counts characters, so pad and apply_padding count them rather than subtracting byte offsets.

synthesis::template writes Formatter literals directly instead of calling new, so it repeats the sentinel constants; the e2e fixture template_format_spec.wado fails if the two copies drift.

String::push_display (the PushDisplay trait) writes one Display value into a String in place, skipping the temporary a `${value}` template would allocate. It is the hot path in the serde numeric encoders.

The traits

Each method is named after its trait, so a type implementing several declares no overloads.

pub trait Display   { fn fmt(&self, f: &mut Formatter); }
pub trait Binary    { fn fmt_binary(&self, f: &mut Formatter); }
pub trait Octal     { fn fmt_octal(&self, f: &mut Formatter); }
pub trait LowerHex  { fn fmt_lower_hex(&self, f: &mut Formatter); }
pub trait UpperHex  { fn fmt_upper_hex(&self, f: &mut Formatter); }
pub trait LowerExp  { fn fmt_lower_exp(&self, f: &mut Formatter); }
pub trait UpperExp  { fn fmt_upper_exp(&self, f: &mut Formatter); }

internal trait Inspect { fn inspect(&self, f: &mut Formatter); }

Inspect is internal — it is the compiler's dispatch target for ?, and the stdlib's own impls are what a program normally reaches. A T: Inspect bound is accepted and always holds, since the trait is total over every type, and a type may write an impl Inspect to override the derived one. See WEP: Trait Derivation Policy.

Resolution

Specifier Resolves to
(none) Display::fmt — compile error if T has no Display
f Display::fmt — Display already honours precision
? Inspect::inspect
b Binary::fmt_binary
o Octal::fmt_octal
x LowerHex::fmt_lower_hex
X UpperHex::fmt_upper_hex
e LowerExp::fmt_lower_exp
E UpperExp::fmt_upper_exp

# changes no row: it sets Formatter.alternate, and the implementation the row already names reads it.

The alternate flag

Trait Syntax Effect of alternate
Display ${:#} Implementation-defined; most ignore it
Inspect ${:#?} Pretty-print with indentation
Binary ${:#b} 0b prefix
Octal ${:#o} 0o prefix
LowerHex ${:#x} 0x prefix
UpperHex ${:#X} 0x prefix, A-F digits
LowerExp / UpperExp ${:#e} None

A flag rather than a second trait per specifier costs one branch on a field the template synthesiser writes as a literal, so optimize::param_spec clones the callee on it and constant folding drops the arm not taken. Below -O2 no such clone happens and both arms remain.

Where the two forms differ by more than a constant, the alternate body is outlined into its own function that the branch calls — as write_seq_inspect calls write_seq_inspect_alt — so the plain path stays the size it was and DCE drops the alternate one when nothing asks for #.

Debug formatting

:? and :#? both resolve to Inspect, which derives from the type's shape through one blanket impl per reflection kind in core:prelude/traits:

Bound Renders
ReflectStruct Name { field: value, … }, secret fields as ..
ReflectVariant Type::Case(payload), a unit case bare
ReflectEnum Type::Case
ReflectFlags set bits joined by \|, else Type::none()
ReflectNewtype the base's rendering, then as Name

Each branches on alternate where the two forms differ, calling out to the outlined alternate body, so a struct, variant, enum, flags or newtype type needs no synthesised impl of its own. A generic newtype needs none either: the blanket covers its declaration and instantiates per use. The compiler still emits an impl per type for what carries no kind — tuples, generic resource instances, resources, and the fn(..) dispatch stubs. See WEP: Trait Derivation Policy and WEP: Inspect.

Display derivation

Display comes from the type's own impl Display. The compiler writes one for the two kinds whose string form is unambiguous:

Every other type — struct, variant, generic container — needs a hand-written impl; ${x} without one is a compile error naming ${x:?} as the debug form. So T: Display certifies a real string representation, which is what String::push_display takes.

Primitive implementations

Type Traits
Integer types Display, Binary, Octal, LowerHex, UpperHex, LowerExp, UpperExp
Float types Display, LowerExp, UpperExp
bool, char Display
String Display

Inspect is implemented for every primitive. None of them needs a second impl for #: the numeric traits read the flag inside the shared fmt_base, and the rest ignore it.

Consequences

The Formatter reaches every implementation, so precision, width and the alternate flag are available wherever a value is rendered — including inside a container, which shares the caller's Formatter with its elements.

Alignment and Formatter are types the stdlib must carry, and each primitive needs one impl per format trait it supports; there is no blanket that covers them.

Reading alternate at run time rather than dispatching on it costs a branch wherever it cannot be folded — at -O0 and -O1 both arms stay in the one function, where a separate trait per form would have let DCE drop the half a program does not use.

Known gaps

References