WEP: Template Format Specifiers
Context
Template strings in Wado support interpolation with format specifiers: `value: {x:spec}`. Different languages provide different sets of format specifiers:
Language Comparison
| Feature | Go | Python | Rust | Wado |
|---|---|---|---|---|
| Fixed-point float | %f, %F |
:f, :F |
{} (default) |
{} |
| Exponential (lower) | %e |
:e |
{:e} |
{:e} |
| Exponential (upper) | %E |
:E |
{:E} |
{:E} |
| General format (adaptive) | %g, %G |
:g, :G |
❌ None | ❌ None |
| Binary | %b |
:b |
{:b} |
{:b} |
| Octal | %o |
:o |
{:o} |
{:o} |
| Hex (lower) | %x |
:x |
{:x} |
{:x} |
| Hex (upper) | %X |
:X |
{:X} |
{:X} |
| Pointer | %p |
N/A | {:p} |
❌ None |
| Debug | %#v |
!r |
{:?} |
{:?} |
The Problem with General Format (g/G)
C's %g format specifier automatically switches between fixed-point and exponential notation based on magnitude. While convenient, this has been identified as a "negative legacy" from C:
- Unpredictable output: Values unexpectedly switch to scientific notation
- Production issues: Common source of bugs when output format matters
- User confusion: Developers often encounter this multiple times
Rust's design decision was to exclude g/G format specifiers entirely. Instead:
- RFC #844 (2015): Community requested
%gequivalent for prettier float output - RFC #2729: Proposed
{:g?}debug formatter - Final solution (Rust 1.58): Modified
{:?}(Debug) to automatically use exponential notation for very large/small floats, without adding a separate format specifier
This avoided the unpredictability of g while still providing readable output for debugging.
Decision
Wado will use Rust-compatible format specifiers, following Rust's philosophy of explicit, predictable formatting.
Format Specifier Syntax
`{expr}` // Default (Display trait)
`{expr:spec}` // With format specifier
Where spec follows Rust's format specification mini-language.
Supported Format Types
| Specifier | Trait | Description | Example |
|---|---|---|---|
| (none) | Display | Default display | {x} → "42" |
? |
Inspect | Debug representation | {x:?} → "42" |
#? |
InspectAlt | Pretty-print debug | {x:#?} (indented) |
b |
Binary | Binary integers | {x:b} → "101010" |
o |
Octal | Octal integers | {x:o} → "52" |
x |
LowerHex | Lowercase hex | {x:x} → "2a" |
X |
UpperHex | Uppercase hex | {x:X} → "2A" |
e |
LowerExp | Lowercase exponential | {x:e} → "4.2e1" |
E |
UpperExp | Uppercase exponential | {x:E} → "4.2E1" |
Notes:
- No
g/Gformat specifiers. Users must explicitly choose between default{}and exponential{:e}/{:E}. - No
p(Pointer) format specifier. Wado does not have pointer types. - Negative signed integers format as the two's complement bit pattern of the value's own width under
b/o/x/X(e.g.{-1 as i32:x}→"ffffffff"), matching Rust.
Format Parameters
Format specifiers can include additional parameters in this order:
{expr:[[fill]align][sign][#][0][width][.precision]type}
Alignment and Width
| Syntax | Description | Example | Output |
|---|---|---|---|
{x:5} |
Width 5, default align | {42:5} |
" 42" |
{x:<5} |
Left align | {42:<5} |
"42 " |
{x:^5} |
Center align | {42:^5} |
" 42 " |
{x:>5} |
Right align | {42:>5} |
" 42" |
{x:05} |
Zero-padding | {42:05} |
"00042" |
{x:_>5} |
Fill with _, right align |
{42:_>5} |
"___42" |
Precision
For floating-point numbers, precision specifies decimal places:
println(`{3.14159:.2}`); // => "3.14"
println(`{100.0:.4}`); // => "100.0000"
println(`{1.23e5:.1e}`); // => "1.2e5"
For integers in exponential notation, precision specifies significant digits:
println(`{12345:.2e}`); // => "1.23e4"
For strings, precision specifies the maximum length in characters, matching
Rust's {:.N}. Display truncates silently; Inspect (:?) truncates to the
same characters and appends a ... marker (outside the quotes) when truncation
actually happened. The marker does not count toward the precision:
let s = "hello world";
println(`{s:.5}`); // => "hello"
println(`{s:.5?}`); // => "hello"... (note: with surrounding quotes)
println(`{s:.20}`); // => "hello world" (precision >= length: unchanged)
For arrays, precision specifies the maximum number of elements. Display
truncates silently; Inspect appends , ... when elements are dropped:
let a: List<i32> = [1, 2, 3, 4, 5];
println(`{a:.3?}`); // => "[1, 2, 3, ...]"
println(`{a:.3}`); // => "[1, 2, 3]"
The truncation point is uniform across collection-like types: precision caps the
number of rendered items (characters for String, elements for List). A
shared Formatter flows into nested element rendering, so an explicit precision
also bounds elements inside collections (e.g. long String elements of an
List). Tuples and structs are fixed-size, so their own arity is never capped,
but their String / List fields still honor the active precision.
Default Inspect cap and the precision sentinels
Inspect / InspectAlt (:? / :#?) of a String or List apply a default
length cap of DEFAULT_SEQ_LIMIT (256 items) even when no precision is given, so
debug output — including power-assert operand dumps — stays readable. Display
never applies the default cap: a plain {s} renders the full value.
To make this work, Formatter.precision uses two negative sentinels instead of a
single "unspecified" value:
PRECISION_DEFAULT(-2): no precision in the spec. SequenceInspectfalls back toDEFAULT_SEQ_LIMIT; every other type (floats, ints,Display) treats it the same as unset (itsprecision >= 0check stays false).PRECISION_INFINITE(-1): render with no cap; sequenceInspectskips truncation. There is no surface syntax for it yet (the.Ngrammar cannot produce a negative value), so it is currently reachable only by constructing aFormatterdirectly — see the TODO informat.wado.
This keeps precision's meaning intact per type (decimal places for floats, max
length for sequences) while letting :? cap sequences by default without
affecting non-sequence operands.
Sign
| Syntax | Description | Example | Output |
|---|---|---|---|
{x:+} |
Always show sign | {42:+} |
"+42" |
{x:+} |
Always show sign (negative) | {-42:+} |
"-42" |
| (default) | Only negative sign | {42} |
"42" |
Alternate Form
The # flag provides alternate representations:
| Type | Effect | Example | Output |
|---|---|---|---|
x, X |
Add 0x prefix |
{42:#x} |
"0x2a" |
b |
Add 0b prefix |
{42:#b} |
"0b101010" |
o |
Add 0o prefix |
{42:#o} |
"0o52" |
Complete Examples
// Basic formatting
let x = 42;
println(`{x}`); // => "42"
println(`{x:?}`); // => "42"
println(`{x:b}`); // => "101010"
println(`{x:o}`); // => "52"
println(`{x:x}`); // => "2a"
println(`{x:X}`); // => "2A"
println(`{x:#x}`); // => "0x2a"
// Width and alignment
println(`{x:5}`); // => " 42"
println(`{x:<5}`); // => "42 "
println(`{x:^5}`); // => " 42 "
println(`{x:>5}`); // => " 42"
println(`{x:05}`); // => "00042"
// Floating-point
let pi = 3.14159;
println(`{pi}`); // => "3.14159"
println(`{pi:.2}`); // => "3.14"
println(`{pi:+.2}`); // => "+3.14"
println(`{pi:8.2}`); // => " 3.14" (width 8, precision 2)
println(`{pi:e}`); // => "3.14159e0"
println(`{pi:.2e}`); // => "3.14e0"
println(`{pi:E}`); // => "3.14159E0"
// Structs
let p = Point { x: 10, y: 20 };
println(`{p}`); // => "(10, 20)" (if Display implemented)
println(`{p:?}`); // => "Point { x: 10, y: 20 }" (inspect)
// Arbitrary expressions (Wado extension)
println(`{x + 1}`); // => "43"
println(`{x * 2:x}`); // => "54" (84 in hex)
println(`{p.x + p.y}`); // => "30"
println(`{arr.len()}`); // => "5"
Interaction with Type Stringification
This WEP extends WEP: Type Stringification:
{expr}: Uses Display trait, falls back to inspect{expr:?}: Always uses inspect (compiler intrinsic){expr:spec}: Uses appropriate trait forspec(e.g., Binary, LowerHex, LowerExp)
Resolution order for {expr:spec}:
- If type does not support the format trait → compile error
- Apply format parameters (width, precision, alignment)
- Return formatted String
Key differences from Rust:
- Arbitrary expressions: Wado allows any expression in
{expr}, while Rust only allows simple value expressions (no method calls, operators, etc. without parentheses) - Inspect fallback: Wado provides inspect as a fallback for
{}and{:?}, while Rust requires explicit trait implementations - Other format specifiers: Format specifiers like
{:x}still require the type to support that trait (or be a primitive) - No
:pspecifier: Wado excludes:psince it has no pointer types
Syntax disambiguation: The parser distinguishes :: (scope resolution/turbofish) from : (format specifier) by lookahead:
{foo::bar}-::is part of the expression{foo::<T>}-::<is turbofish syntax{foo:x}-:starts format specifier
Consequences
Positive
- Rust familiarity: Developers familiar with Rust can immediately use Wado's format specifiers (except
:p) - Predictable output: No surprise switches to exponential notation
- Explicit intent: Developers explicitly choose the format they want
- Type safety: Format specifiers that don't apply to a type cause compile errors
- Extensible: Supports
{:#?}for pretty-print and other Rust-compatible specifiers - Language coherence: Excluding
:paligns with Wado's lack of pointer types
Negative
- No convenience format: Developers must choose between
{}and{:e}for floats- Mitigation: Default
{}works well for most cases;{:e}is explicit for scientific notation
- Mitigation: Default
- Learning curve: Developers from Python/Go may need to learn Rust's format syntax
- Mitigation: Rust's format syntax is well-documented and widely used
- Implementation complexity: Requires implementing all Rust format traits
- Mitigation: Can implement incrementally, starting with most common specifiers
Implemented Extensions
- Pretty-print:
{:#?}for indented multi-line debug output viaInspectAlttrait
Future Extensions
Following Rust's ecosystem, we can add:
- Dynamic width and precision: Allow runtime values using nested
{}syntax- Syntax:
{value:{width}.{precision}}(Python-style) - Rationale: Python's nested
{}approach is more consistent with Wado's "arbitrary expressions" philosophy than Rust's$syntax, which requires argument lists. Variables in the current scope can be referenced directly. - Example:
let w = 8; let p = 2; println(\{pi:{w}.{p}}`);→" 3.14"`
- Syntax:
Migration from Other Languages
For developers coming from other languages:
| From | To Wado | Notes |
|---|---|---|
Python f"{x:.2f}" |
`{x:.2}` |
No f suffix needed |
Go fmt.Printf("%d", x) |
`{x}` |
Type inferred |
Go fmt.Printf("%x", x) |
`{x:x}` |
Same specifier |
Python f"{x:g}" |
`{x}` or `{x:e}` |
Choose explicitly |
Rust format!("{:?}", x) |
`{x:?}` |
Identical |
