fmt
fmt turns values into text. Display is human-readable output for end users; Debug is developer-readable output for diagnostics. Both write through a Formatter<W> sitting on any FmtWrite sink, so formatting to stdout, into a String, or out to a file is the same code path.
The print family lives directly in this module — import std::fmt; brings it in. Note that it is not in the prelude.
| Item | Import |
|---|---|
print, println, eprint, eprintln, printf, eprintf | import std::fmt; |
Display, Debug, Formatter<W>, format_to_string | import std::fmt::display; |
FmtWrite | import std::fmt::write; |
FmtError | import std::fmt::error; |
f64::to_buf, parse_f64 | import std::fmt::float; |
| f-string runtime | (prelude) |
Printing
Two flavours, and the distinction matters.
print / println / eprint / eprintln are Display-based. They take an already-formatted owned String, write it, and drop it. Because the f-string desugar has already run each hole through Display or Debug, there is no format string left to interpret — a % in the text is data, not a directive.
import std::fmt;
println(f"connected to {host}:{port}");
println("plain text works too"); // string literals convert implicitly
eprintln(f"failed: {err:?}");
printf / eprintf are the C-style variadic forms with %s and %d specifiers, returning the byte count (negative on error, per the libc convention). Reach for these when the output is genuinely variadic; prefer an f-string otherwise.
Both flavours go through the real stdout, so their output stays interleaved rather than landing on a second FILE*.
f-strings
An f-string builds an owned String. Each {expr} hole is rendered through Display and each {expr:?} hole through Debug:
const line: String = f"user {name} has {count} items";
const dump: String = f"state = {state:?}";
The parser lowers the literal into a chain of calls threading one String builder left to right, each taking the partial string by value and returning it — so the whole expression evaluates to a single owned String with no intermediate temporaries to drop. Those helpers live in fmt::interp, which is in the prelude so f"..." works with no import.
Format specs
A hole may carry a spec after a colon. The grammar is a subset of Rust's:
spec := [[fill]align][#][0][width][.precision][type]
align := '<' | '>' | '^' (left, right, centre)
type := 'x' | 'X' | 'o' | 'b' (hex lower/upper, octal, binary)
f"{value:>8}" // right-align in a field of 8
f"{byte:#04x}" // 0x0f - `#` adds the radix prefix, `0` zero-pads
f"{ratio:.3}" // three fractional digits
f"{label:*^12}" // centred, padded with '*'
width is a minimum field width measured in Unicode scalars. .precision truncates a Display value to that many scalars, or sets the fractional digits of a float. Alignment defaults to left for text and numbers alike — chosen for predictability over a type-dependent default — and an explicit > or ^, or the 0 flag, overrides it.
For a signed integer, a decimal spec keeps the sign, while a radix formats the two's-complement bit pattern at the value's own width with no sign.
Display and Debug
type trait Display {
fmt<W>(&this, f: mut &Formatter<W>) -> Result<(), FmtError> where W: FmtWrite;
}
Debug has the identical shape. Implement one for your own type and it becomes usable in every hole, every print, and every format_to_string call:
implement trait Display for struct Point {
fmt<W>(&this, f: mut &Formatter<W>) -> Result<(), FmtError> where W: FmtWrite {
f.write_str(Str::new("("))?;
this.x.fmt(f)?;
f.write_str(Str::new(", "))?;
this.y.fmt(f)?;
return f.write_str(Str::new(")"));
}
}
Both traits ship for every primitive width, boolean, char, string, Str, String, Option, Result, Array, HashMap, HashSet, Pair, and every standard library error type.
Debug is the unambiguous form. For Str and String it wraps the content in double quotes and escapes ", \, control bytes, and non-ASCII low codepoints, so the output is valid UTF-8 whatever the source and round-trips through a parser that understands \n, \r, \t, \\, \", and \xNN. For char it uses single quotes and emits non-ASCII scalars as \u{XXXX} to keep terminal output safe.
Display for char encodes the scalar as UTF-8 rather than emitting the low byte, which would truncate non-ASCII to garbage. Surrogates are not legal Unicode and produce U+FFFD.
Formatter<W>
Formatter<W> borrows its writer through a raw pointer — the caller owns the sink and is responsible for dropping it, which is what lets a Formatter<String> hand the string straight back without into_inner gymnastics.
static new(writer: W*), write_str(mut &this, source: Str), write_char(mut &this, c: char), and get_writer(&this) -> W* for dispatching a second formatter against the same sink or flushing mid-stream.
Formatting to a String
match (format_to_string(&value)) {
Result::Ok(s) => { /* you own `s` */ }
Result::Err(e) => { /* FmtError */ }
}
format_debug_to_string is the Debug counterpart. You own the result and are responsible for dropping it.
FmtWrite
The sink trait every formatter writes through. It parallels io::Write but with a different error type and a stricter contract:
type trait FmtWrite {
write_str(mut &this, source: Str) -> Result<(), FmtError>;
write_char(mut &this, c: char) -> Result<(), FmtError>;
}
write_str is the one required method; everything else routes through it. Its argument is a Str, so a sink only ever sees valid UTF-8 — that invariant is what lets a text container like String be a format target. A byte sink that isn't UTF-8-constrained is an io::Write, not an FmtWrite.
Implementations ship for Stdout, Stderr, and String.
FmtError
A formatted write can fail two ways, and collapsing them would throw away information a caller might want to branch on — a format that failed because String::push couldn't reallocate is not the same shape of problem as a closed socket.
type enum FmtError {
Io(IoError);
Alloc(AllocError);
}
is_io(), is_alloc(), and describe() -> Str, which forwards to the wrapped error's own describe. From<IoError> and From<AllocError> are the lifting conversions used by sink implementations.
Floats
Float formatting lives here rather than in core::primitives because it needs libm, and keeping libc out of core matters more. The API is extension methods on the primitives.
| Method | Notes |
|---|---|
to_buf(&this, buffer: u8*) -> u64 | Fixed-point with six fractional digits. Buffer must hold at least 32 bytes. |
to_buf_with(&this, buffer: u8*, decimals: u64) | Caller-chosen precision. |
to_buf_roundtrip(&this, buffer: u8*) | The shortest decimal string that re-parses to the identical bit value. |
is_negative_zero(&this) -> boolean |
Values beyond |v| >= 1e16 or below 0 < |v| < 1e-4 switch to scientific notation, so no precision disappears into a giant zero prefix or a run of trailing zeros. NaN, both infinities, and both zeros are handled explicitly — negative zero prints as -0 so the sign is not lost.
to_bufis a first-cut formatter: correct for human-readable output but not round-trippable, since the fixed six decimals silently truncate (3.141592653589793becomes3.141593). Anything that must preserve the value — JSON serialization, notably — should useto_buf_roundtrip, which tries 15, then 16, then 17 significant digits and returns the first that re-parses exactly. Typical values stay compact:0.1, not0.10000000000000001.
parse_f64(s: string) -> Option<f64> is the reverse, with C strtod semantics: skip leading whitespace, read the longest valid float prefix, decimal or scientific, no hex floats. It returns None when there is no numeric prefix. Strip digit separators (_) before calling — strtod stops at them.
Integer formatting is on the primitives themselves in core::primitives, and integer parsing goes through TryFrom<Str> in collections::str.