Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Formatting into a reusable writer

An f-string is the short way to build a str; it compiles to the calls below. Use std.fmt directly when output must go to a chosen byte writer, reuse storage, or stay within a byte budget. Numeric widths count bytes and include the sign. Every call is fallible, and the byte budget includes punctuation and padding.

import std.fmt

def main() -> Result[(), AllocError | IoError | std.fmt.FormatError]:
    mut out = std.fmt.string_formatter(80, 8)?
    out.text("count=")?
    out.integer(12, 4)?
    out.text(", label=")?
    out.debug_text("one\ntwo")?
    print(std.fmt.finish(&out)?)?
    Ok(())
count=  12, label="one\ntwo"

Your type chooses separate display and debug methods. Both can be generic over the output writer; callers need no trait object or format-string parser. print and f"{x}" call display; str.repr, f"{x!r}" and the elements of a printed list or other aggregate call debug. std.fmt.to_string and to_debug call the same methods within a byte budget and depth you choose.

import std.fmt
from std.fmt import Formatter

pub class Label:
    name: str

    pub def display[W](self, out: &mut Formatter[W]) -> Result[(), FormatError]:
        out.text(&self.name)

    pub def debug[W](self, out: &mut Formatter[W]) -> Result[(), FormatError]:
        out.text("Label(")?
        out.debug_text(&self.name)?
        out.text(")")

def main() -> Result[(), AllocError | IoError | FormatError]:
    label = Label("ready")
    print(label)?
    print(str.repr(label)?)?
    print([Label("a"), Label("b")]?)?
    print(std.fmt.to_debug(&label, 80, 8)?)?
    Ok(())
ready
Label("ready")
[Label("a"), Label("b")]
Label("ready")

Because these methods can fail, printing such a value returns Result[(), IoError | FormatError], and str.repr and an f-string with such a field add FormatError to AllocError. A type without display prints with its debug; one without either prints its fields.

Enums use the same methods. Match the shared receiver to format a payload without consuming it:

import std.fmt
from std.fmt import Formatter

pub enum ReadError:
    Missing
    Invalid(i64)

    pub def display[W](self, out: &mut Formatter[W]) -> Result[(), FormatError]:
        match self:
            case ReadError.Missing:
                out.text("no reading")
            case ReadError.Invalid(line):
                out.text("invalid reading on line ")?
                out.integer(*line, 0)

def main() -> Result[(), FormatError | IoError]:
    error = ReadError.Invalid(12)
    print(error)?
    print(std.fmt.to_string(&error, 80, 8)?)?
    Ok(())
invalid reading on line 12
invalid reading on line 12

Returning the enum from main does not call the method yet: the report still uses the structural entrypoint report.

Inside such a method, out.format(f"...") writes an f-string straight to the formatter, with no intermediate str. It returns Result[(), FormatError], and the formatter’s byte budget and depth limit apply:

import std.fmt
from std.fmt import Formatter

pub class Reading:
    line: i64
    value: f64

    pub def display[W](self, out: &mut Formatter[W]) -> Result[(), FormatError]:
        out.format(f"line {self.line}: {self.value:.1f}")

def main() -> Result[(), AllocError | IoError | FormatError]:
    reading = Reading(7, 21.25)
    print(f"[{reading}] [{reading:>14}]"?)?
    mut out = std.fmt.string_formatter(10, 8)?
    match out.format(f"{reading}"):
        case Err(FormatError.LimitExceeded):
            print("kept", std.fmt.finish(&out)?)?
        case _:
            print("fits")?
    Ok(())
[line 7: 21.2] [  line 7: 21.2]
kept line 7: 21

To stream, construct std.fmt.formatter(writer, scratch, max_bytes, max_depth) with a std.io writer and initialized Bytes scratch of at least 32 bytes. Provision scratch and sink capacity before entering an allocation-free section. Formatting itself then reuses that memory; user methods remain responsible for their own allocations. Flush the public out.writer explicitly when needed. On failure, out.completed() tells you how many bytes were accepted; do not blindly restart a partially written record.

The reference describes aggregate callbacks, errors, escaping, depth limits and exact costs.