Invisible link to canonical for Microformats

ADR-024 Structured String Formatting via fmt() in slug.std


Status

Accepted

Context

Slug needs a single, ergonomic, and Slug-native way to format strings for:

  • logging and diagnostics
  • user-facing text
  • building protocol strings (HTTP, etc.)
  • tooling output (REPL, test runners, CLIs)

Classic names such as printf / sprintf imply C/Go-style formatting: percent verbs, positional coupling, implicit coercions, and historical edge cases. That model conflicts with Slug’s design goals:

  • explicitness and predictability
  • DEC64-friendly numeric semantics
  • composability (especially with pipelines)
  • strict separation of value transformation and IO

Slug requires a modern, minimal formatting facility that is easy to reason about, easy to implement in multiple runtimes, and easy to teach.

This ADR follows the standard ADR template. :contentReference[oaicite:0]{index=0}

Decision

Introduce fmt() in slug.std as Slug’s primary string formatting function.


1. Naming and placement

  • The function name is fmt.
  • It lives in slug.std.
  • fmt() returns a string and performs no IO.

Printing is composed explicitly:

println(fmt("Hello {}", name))

or via pipelines:

fmt("Hello {}", name) /> println

2. Signature and varargs

fmt(@str format, ...args) -> string

Rationale:

  • avoids requiring callers to allocate a list
  • mirrors common Slug APIs that prefer direct values
  • keeps call sites compact and readable

3. Placeholder syntax (positional only)

Formatting uses brace placeholders. Named placeholders are intentionally not supported.

Supported forms:

  • {} → next positional argument (auto-incrementing cursor)
  • {N} → positional argument at index N (0-based)

All placeholders may include a format specifier:

  • {:.2f} (next positional)
  • {0:.2f} (indexed positional)
  • {:d} (next positional, integer-style)

Examples:

fmt("Hello {}, you have {} msgs", "Slug", 3)
fmt("{1} then {0}", "A", "B")
fmt("x={:.2f} y={:.2f}", 1.2, 3.4)

Cursor rules

  • {} consumes the next unused positional argument.
  • {N} reads argument N without advancing the {} cursor.
  • {} and {N} may be mixed freely.

4. Escaping rules

To include literal braces, use backslash escaping:

  • \{{
  • \}}

Examples:

fmt("\{")            // "{"
fmt("value=\{x\}")   // "value={x}"

Rules:

  • Backslash escaping applies only to { and } inside fmt format strings.
  • An unescaped { that does not begin a valid placeholder is a runtime error.
  • A dangling backslash at the end of the format string is a runtime error.

Rationale:

  • avoids collision with Slug’s `` string interpolation
  • keeps escaping explicit and minimal
  • preserves a single mental model for braces

5. Width and alignment

Format specs support optional alignment and width.

Alignment characters:

  • < left
  • > right
  • ^ center

Width is a positive integer.

Examples:

fmt("|{:>8}|", 12.3)      // right-aligned
fmt("|{:<10s}|", "Slug")  // left-aligned
fmt("|{:^9s}|", "hi")     // centered

Rules:

  • If width is smaller than the rendered value, no truncation occurs.
  • Padding uses spaces.
  • Custom fill characters are intentionally out of scope for v1.

6. Numeric formatting (DEC64-aware)

Verbs

  • f → fixed decimal
  • d → fixed decimal with 0 fractional digits
  • % → percent formatting

Precision

  • .Nf renders exactly N fractional digits using DEC64 rounding.

Examples:

fmt("{:.2f}", 12.345) // "12.35"
fmt("{:f}",   12.3)   // default precision (implementation-defined)

:d semantics

d is a presentation choice, not a type assertion:

  • {:d} behaves like {:.0f}
  • rounding uses DEC64 half-even semantics

Examples:

fmt("{:d}", 12.3) // "12"
fmt("{:d}", 12.5) // "12"
fmt("{:d}", 13.5) // "14"

7. Grouping (thousands separators)

Grouping is enabled via , in the format spec:

fmt("{:,}",    1234567)      // "1,234,567"
fmt("{:,.2f}", 1234567.89)   // "1,234,567.89"

Rules:

  • Grouping applies to the integer portion only.
  • Separator is , (locale-independent in v1).

8. Percent formatting

Percent formatting multiplies the value by 100 and appends %.

Examples:

fmt("{:%}",   0.123)   // "12.3%"
fmt("{:.1%}", 0.123)   // "12.3%"
fmt("{:.0%}", 0.126)   // "13%"

Rules:

  • Precision applies to digits after the decimal point.
  • Rounding uses DEC64 rules.

9. Errors and strictness

fmt is strict and fails fast:

  • missing positional argument
  • out-of-range index
  • invalid format spec
  • unmatched or unescaped { / }
  • unsupported verb for the resolved value

Error messages should include:

  • the offending placeholder
  • a short excerpt of the format string
  • the reason for failure

Non-normative: placeholder grammar (for implementers)

The following grammar is illustrative and non-normative.

format_string  := { text | escape | placeholder }

escape         := "\" ("{" | "}")

placeholder    := "{" index? ":"? spec? "}"

index          := DIGIT { DIGIT }

spec           := align? width? precision? verb? grouping? percent?

align          := "<" | ">" | "^"
width          := DIGIT { DIGIT }
precision      := "." DIGIT { DIGIT }
verb           := "f" | "d"
grouping       := ","
percent        := "%"

Notes:

  • {} is represented by an empty index and empty spec.
  • Validation order is implementation-defined but must be deterministic.
  • Parsing should be single-pass where possible.

Consequences

Positive

  • Minimal, modern formatting model aligned with Slug philosophy.
  • No printf / sprintf legacy semantics.
  • Easy to teach and easy to read at call sites.
  • DEC64 semantics are explicit and predictable.
  • Covers the majority of real-world formatting needs.

Negative

  • Positional-only placeholders require callers to manage argument order.
  • Introduces a small formatting mini-language.

Neutral

  • Date/time formatting is intentionally excluded and will be addressed in a future ADR.
  • Additional features (custom fill chars, locale-aware grouping, truncation) may be added later without changing the core model.