2.10. fmt - Formatting traits
The rok::core::fmt module defines the family of traits that convert values to human-readable strings. Every format spec in an f-string - f"{x}", f"{x:?}", f"{n:08b}", and so on - dispatches to the traits in this chapter.
The lexical decomposition in refs-lang 1.7.4 f-string format spec and the trait dispatch in this chapter together determine the meaning of an f-string.
2.10.1. Dispatch
An f-string hole {expr[:spec]} calls Debug::fmt if the spec's type is ?, and Display::fmt otherwise. The rest of the spec (fill / align / sign / # / 0 / width / precision and b o x X e E f) is carried by the Formatter.
- Numbers (all integers, floating-point types,
int,float) apply the spec directly to their own output (2.10.5, 2.10.6). - The
fmtof any other value does not look at the spec (f.width()isnone). The value writes itself as is; the Formatter then applies fill / align / width and truncates by precision.
d / g / s are other names for the default notation. The meaning of each symbol and flag is defined in refs-lang 1.7.3. An f-string does not build an intermediate string per hole; it writes each hole in turn into the buffer of a single Formatter.
2.10.2. Formatter
struct Formatter { .. } // output buffer + current spec
enum Alignment { Left, Right, Center, Equal }
enum Sign { Plus, Minus, Space }
impl Formatter {
fn new() -> Formatter;
fn with_capacity(n: usize) -> Formatter;
fn with_spec(spec: FormatSpec) -> Formatter;
fn finish(self) -> str; // text written so far
fn write_str(ref self, s: str) throws (FmtError);
fn write_char(ref self, c: char) throws (FmtError);
fn pad(ref self, s: str) throws (FmtError); // applies fill / align / width / precision
fn pad_integral(ref self, is_nonnegative: bool, prefix: str, digits: str) throws (FmtError);
// sign + # prefix + 0 / fill padding
fn fill(self) -> char;
fn align(self) -> Alignment | none; // none: default (right for numbers, left otherwise)
fn sign(self) -> Sign | none;
fn alternate(self) -> bool; // #
fn pad_zero(self) -> bool; // 0
fn width(self) -> usize | none;
fn precision(self) -> usize | none;
}
FormatSpec is a parsed spec value. A static spec is parsed at compile time; a dynamic spec (one containing {..} / ${..}) is parsed once at runtime.
struct FormatSpec {
fill: char; align: Alignment | none; sign: Sign | none; alt: bool; zero: bool;
width: usize | none; precision: usize | none; ty: char;
}
impl FormatSpec {
fn new() -> FormatSpec; // empty spec
fn parse(text: str) -> FormatSpec; // panics on an invalid spec
}
2.10.3. Display
#[lang("display_trait")]
trait Display {
fn fmt(self, ref f: Formatter) throws (FmtError);
}
This is the default dispatch for f"{x}". The fmt of a primitive number applies the spec and writes in a single runtime call.
impl Display for str {
fn fmt(self, ref f: Formatter) throws (FmtError) { try f.write_str(self) }
}
struct Point { x: f64; y: f64; }
impl Display for Point {
fn fmt(self, ref f: Formatter) throws (FmtError) {
try f.write_str("(");
try Display::fmt(self.x, ref f);
try f.write_str(", ");
try Display::fmt(self.y, ref f);
try f.write_str(")");
}
}
f"{Point { x: 1.5, y: 2.5 }}"; //→ "(1.5, 2.5)"
f"{Point { x: 1.5, y: 2.5 }:>12}"; //→ " (1.5, 2.5)" (f.width() inside Point::fmt is none)
- The call site requires
ref(Display::fmt(x, ref f)). The formx.fmt(f)is ambiguous for a type that implements bothDisplayandDebug, so the call is written as a path. Displayis not derived. The type's designer decides its representation.
2.10.4. Debug
#[lang("debug_trait")]
trait Debug {
fn fmt(self, ref f: Formatter) throws (FmtError);
}
impl Debug for str { .. } // a valid rok string literal
impl Debug for char { .. } // a valid rok character literal
This is the dispatch for f"{x:?}"; it aims for a representation that can be read back. The Debug output of str / char is a rok literal. Quotes, \\, \n / \r / \t, and so on are escaped; other control characters and bidi characters (U+202A..U+202E, U+2066..U+2069, etc.) are written as \u{..}.
Display vs Debug
The two traits serve different purposes, summarized in the following table.
| Aspect | Display | Debug |
|---|---|---|
| Audience | end user | developer |
| Accuracy | readability first | no information loss (aims to round-trip) |
| Quotes | string as is (no quotes) | string wrapped in "..." (with escapes) |
| Containers | usually not defined | automatic for every collection |
| derive | not allowed (manual impl) | allowed (#[derive(Debug)]) |
let s = "hello\nworld";
let d1 = f"{s}"; //→ hello
// world (Display - as is)
let d2 = f"{s:?}"; //→ "hello\nworld" (Debug - escaped)
User-defined / derive
#[derive(Debug)]
struct Point { x: f64; y: f64; }
let p = Point { x: 1.5, y: 2.5 };
let s = f"{p:?}"; //→ "Point { x: 1.5, y: 2.5 }"
Debug can be derived automatically by composing the Debug output of every field.
2.10.5. Integer spec
type | Integer |
|---|---|
(none), d, g, s | decimal |
f | fixed point ({5:.2f} → 5.00) |
b o x X | binary / octal / hex (0b / 0o / 0x with #) |
e E | exponent notation |
Giving b o x X to a floating-point value is a compile error (static spec) or a panic (dynamic spec). A negative fixed-width integer prints as the two's complement at that type's width ({-1i8:x} is "ff"). int is written as sign and magnitude (the int value -255 with {n:x} is "-ff").
let n: u32 = 0xCAFE;
f"{n:b}"; //→ "1100101011111110"
f"{n:#x}"; //→ "0xcafe"
f"{n:X}"; //→ "CAFE"
f"{42:08b}"; //→ "00101010"
f"{42:e}"; //→ "4.2e1"
2.10.6. Floating-point output
{}writes the shortest digit string that reads back to the same value, and does not use exponent notation.1.0is1,0.1is0.1, NaN isNaN, infinity isinf/-inf.{:e}writes the same digits in exponent notation (0.15is1.5e-1).{:E}usesE.- With a precision, the value is rounded to that many digits (
{x:.3e},{x:.2}). - A promoted
float(exponent outside f64 range) is written in exponent notation even with{}(1.5e400).
let x: f64 = 12345.678;
f"{x}"; //→ "12345.678"
f"{x:e}"; //→ "1.2345678e4"
f"{x:.3e}"; //→ "1.235e4"
f"{1.0}"; //→ "1"
2.10.7. Full f-string format spec grammar
The authority is refs-lang 1.7.3; this is a summary.
spec = [fill][align][sign][#][0][width][.precision][type]
align = "<" | ">" | "^" | "="
sign = "+" | "-" | " "
type = "" | "s" | "d" | "g" | "f" | "?" | "b" | "o" | "x" | "X" | "e" | "E"
width = unsigned-int | hole
precision = unsigned-int | hole
hole = "{" expr "}" | "${" expr "}" // nested hole, no spec of its own
fill = any single char (default: space)
Dynamic spec
let s1 = f"{x:.{prec}}"; //→ "3.1416" (dynamic precision)
let s2 = f"{x:>${width}.${prec}}"; //→ " 3.1416" (dynamic width + precision)
let s3 = f"{x:{full_spec}}"; //→ ... (entire spec dynamic)
$$ inside a spec is a single $. A $ in the body text is literal (f"cost: $100"). A static spec is checked at compile time; if a dynamic spec expands to an invalid result, it panics at runtime. The lexer's tokenization follows "Lexical processing" in refs-lang 1.7.3.
2.10.8. align / fill / width / precision
Formatter::pad handles the spec's align, fill and width automatically, so a user impl only produces the content.
Without an explicit align, numbers (integers, floating-point) are right-aligned and all other types are left-aligned. When < / > / ^ (and fill) are given, any type follows that alignment. ? (Debug) also accepts width / align.
let n: i32 = 42;
f"{n}"; //→ "42" (default)
f"{n:5}"; //→ " 42" (width 5, numbers right-aligned by default)
f"{n:<5}"; //→ "42 " (left)
f"{n:>5}"; //→ " 42" (right)
f"{n:^5}"; //→ " 42 " (center)
f"{n:0>5}"; //→ "00042" (fill 0)
f"{n:*^7}"; //→ "**42***" (fill *, center, width 7)
f"{n:=+5}"; //→ "+ 42" (= : pad between sign and digits, + : sign on positives)
f"{n:+=5}"; //→ "+++42" (fill '+', align '=')
f"{-42:=6}"; //→ "- 42"
f"{-42:06}"; //→ "-00042" (0 : pad with 0, = alignment)
f"{n:+}"; //→ "+42" (+ : sign on positives too)
f"{"ab":5}"; //→ "ab " (strings left-aligned by default)
precision is applied the same way.
let x: f64 = 3.141592653589793;
f"{x:.2}"; //→ "3.14" (2 digits after the point)
f"{x:.0}"; //→ "3"
f"{x:10.4}"; //→ " 3.1416" (width 10, precision 4, numbers right-aligned by default)
f"{x:<10.4}"; //→ "3.1416 " (left alignment requires explicit <)
Precision on a string is interpreted as truncation to a maximum length; precision on an integer is ignored.
let s = "hello world";
f"{s:.5}"; //→ "hello"
f"{n:.3}"; //→ "42"
2.10.9. Module mapping
The following table lists which traits each type implements.
| Type | Display | Debug |
|---|---|---|
integers, int, floating-point, float | ✓ | ✓ |
bool, char, str | ✓ | ✓ |
List<T>, Set<T> | ✗ | ✓ (T: Debug) |
Dict<K, V> | ✗ | ✓ (K, V: Debug) |
(A, B, ..) | ✗ | ✓ (all Debug) |
T | none | ✗ | ✓ (T: Debug) |
Result<T, E> | ✗ | ✓ (T, E: Debug) |
Display exists only for scalars, strings and user types that implement it by hand; containers do not have it. Debug exists for almost every type and is produced by derive.
2.10.10. #[derive(Debug)]
#[derive(Debug)]
struct User { id: u64; name: str; }
let u = User { id: 1, name: "Alice" };
let s = f"{u:?}"; //→ User { id: 1, name: "Alice" }
The derive works as follows. A struct prints as TypeName { field1: <field1:?>, field2: <field2:?>, ... }, and a tuple struct prints as TypeName(<f1:?>, <f2:?>, ...). An enum prints as VariantName or VariantName(...) / VariantName { ... }. The derive requires every field to implement Debug (recursively).
There is no #[derive(Display)]. Deciding the representation is the user's responsibility.
2.10.11. REPL / print / automatic output
The REPL section of refs-lang 9.7 JIT cell execution flow defines the following.
> let x = 42;
> x
i32: 42
> 1 + 2
i32: 3
> "hello"
str: "hello"
When the REPL automatically prints the last expression, it prints type: value with the value formatted by Debug; for a type without Debug, only the type (refs-lang 9.7.7, 2.12.4).
By contrast, println and print dispatch only to Display.
println(f"x = {x}"); // Display
println(f"x = {x:?}"); // Debug (explicit spec)
2.10.12. FmtError
struct FmtError;
The error for a failed write to a stream. It does not occur when writing to memory (f-strings, Formatter::new, etc.). Every fmt method is throws (FmtError), so calls are marked with try to propagate.
2.10.13. Comparison with other languages
The following table shows where rok's fmt design sits relative to Rust and Python.
| Aspect | Rust Display/Debug | Python __str__/__repr__ | rok |
|---|---|---|---|
| Human / debug split | ✓ separate traits | ✓ separate methods | ✓ separate traits |
| Debug derive | #[derive(Debug)] | automatic default | #[derive(Debug)] |
| Display derive | not allowed | not allowed (manual) | not allowed |
| Radix / exponent | 6 traits | format spec | Formatter carrying the spec |
| Dynamic spec | positional args ({0:.*}) | f-string {:.{prec}} | ${...} expression ($-sigil) |
| Spec syntax | Python-like | PEP 3101 | Python-like + ${...} |
rok's most distinctive choice is to separate dynamic spec insertion visually with ${...}, avoiding the nested-brace ambiguity of Python's {:.{prec}}. The notation is also isomorphic to the $ sigil of JS, Kotlin and Scala.
2.10.14. Derive policy summary
Among the fmt traits, only Debug can be derived. Display is not a derive target: the user-facing surface is decided differently in each domain, so automatic derivation does not fit, and there is no meaningful default form for Display. Types that need a display representation should implement Display by hand.