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 indexN(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 argumentNwithout 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}insidefmtformat 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 decimald→ fixed decimal with 0 fractional digits%→ percent formatting
Precision
.Nfrenders exactlyNfractional 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 emptyindexand emptyspec.- 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/sprintflegacy 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.