String Interpolation And Formatting
Owning escaped string literals embed expression values directly in text using {expr}, with optional {expr:spec} format specifiers. Every interpolation is written with the owning-literal prefix ^, has success type ^str, and reports AllocError; it therefore appears under try or catch. Interpolation is the normal path for building formatted, human-facing text; there is no separate formatting module or printf-style API. This page is the normative reference for the syntax, allocation boundary, default rendering of each type, and supported format specifiers.
Quick Example
import std.io;
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let name = "Ada";
let count = 42;
let ratio = 0.125;
// {expr} interpolates; {expr:spec} formats.
io.println(try ^"{name} ran {count} jobs ({ratio:.1%} done)");
// Prints: Ada ran 42 jobs (12.5% done)
}Literal Forms
Only owning escaped string forms interpolate. See Source And Grammar for the full literal grammar.
"text"— hole-free escaped staticstr; it never allocates by context.^"text {expr}"— owning escaped text.{expr}substitutes a value; write{{or}}to emit a literal brace.`text`— raw static text: braces and backslashes are ordinary, and a doubled backtick emits one backtick.^""" ... """— owning multiline escaped/interpolated text.^``` ... ```— owning multiline raw text.
The ^ prefix may also request an owned copy of hole-free escaped, raw, or multiline text. A hole-free owning form whose decoded content is statically empty is infallible, including empty raw and trimmed multiline forms. Every statically nonempty owning form is fallible, and every interpolation is fallible even when its rendered result happens to be empty.
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let name = "Ada";
// Only the explicit owning form interpolates.
assert try ^"hi {name}" == "hi Ada";
// Doubled braces are literal; the raw RHS never interpolates.
assert try ^"{{name}} = {name}" == `{name} = Ada`;
// Backticks are raw static text.
assert `hi {name}` == `hi {name}`;
}Default Rendering
Without a format specifier, each value renders as follows:
| Value | Renders as | Example output |
|---|---|---|
| signed/unsigned integer, including custom widths | decimal digits | 42, -5 |
i128 / u128 and wider custom integers | full decimal (never truncated) | 340282366920938463463374607431768211455 |
float (f16/bf16/f32/f64) | shortest round-trip decimal, auto fixed/scientific | 1.5, 1500, 1e300 |
f128 | shortest round-trip decimal at full binary128 precision (up to 36 significant digits) | 3.1415926535897932384626433832795028 |
bool | true or false | true |
Vector<T, N> | bracketed lanes separated by comma-space | [1, 2, 3] |
Matrix<T, R, C> | bracketed row-major rows with nested brackets | [[1, 2], [3, 4]] |
| fieldless enum | the variant name | Fast |
str | the text itself | borrowed |
nullable T? | the payload, or the literal null when absent | 7, null |
| reference, pointer, unique pointer, array/slice view | address-like hex | 0x16d8f3a40 |
Floats use shortest round-trip rendering: the result parses back to the same value with no trailing .000000. Integer-valued floats drop the fractional part (1500.0 prints 1500), very large or very small magnitudes switch to scientific form, and inf/nan render as text. Note the default scientific form omits the + and exponent padding that the explicit e mode adds (1e300, not 1e+300). f16 and bf16 widen to f32 before rendering — every f16/bf16 value is exactly representable there, so the output is exact. f128 renders with shortest round-trip decimal at full binary128 precision in the same style; explicit format specifiers (f, e, ...) on an f128 value still format through f64 and are limited to its precision.
import std.mem.{ AllocError };
enum Mode {
Slow,
Fast,
}
fn main() throws(AllocError) {
let some: i32? = 7;
let none: i32? = null;
assert try ^"{42}|{-5}" == "42|-5";
assert try ^"{true}|{false}" == "true|false";
assert try ^"{Mode.Fast}" == "Fast";
assert try ^"{some}|{none}" == "7|null";
// Floats: shortest round-trip, auto fixed/scientific, inf/nan as text.
let big: u128 = 340282366920938463463374607431768211455;
assert try ^"{big}" == "340282366920938463463374607431768211455";
let inf: f64 = 1.0e308 * 10.0;
assert try ^"{inf}|{inf - inf}" == "inf|nan";
}To render a referenced payload instead of an address, dereference explicitly; to print an enum's numeric tag, cast it to an integer type first.
Vector and Matrix formatting
A Vector or Matrix interpolation hole evaluates the aggregate expression once, then a runtime formatter iterates its elements in Vector lane order or Matrix row-major order. A format spec belongs to the scalar element type and is broadcast unchanged to every lane:
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let v: Vector<i32, 3> = [1, 2, 30];
let m: Matrix<f64, 2, 2> = [[1.0, 2.0], [3.0, 4.0]];
let mask: Vector<bool, 3> = [true, false, true];
assert try ^"{v}" == "[1, 2, 30]";
assert try ^"{v:04d}" == "[0001, 0002, 0030]";
assert try ^"{m:.1f}" == "[[1.0, 2.0], [3.0, 4.0]]";
assert try ^"{mask}" == "[true, false, true]";
}Every otherwise legal Vector or Matrix shape is formattable. There is no separate 4,096-element limit for one hole and no combined lane budget across several holes. Brackets, separators, and row nesting are structural and are not padded or otherwise changed by the element format spec.
Formatting uses a compact runtime iterator/helper. It does not create one AST, typed node, CoreFn operation, generated call, or unrolled code fragment per lane, including after generic instantiation. Runtime work, output size, allocation, and formatting errors scale normally with the text produced. The existing compile-time legality and size limits for Vector/Matrix shapes remain separate and unchanged.
Format Specifiers
A format specifier follows the value after a colon: {value:spec}. Fields are parsed in this fixed order, and every field is optional:
{value:[fill][align][sign][#][0][width][grouping][.precision][type]}| Field | Forms | Applies to | Notes |
|---|---|---|---|
| fill + align | any char + one of < > ^ = | all | fill defaults to space; = pads between sign and digits |
| align | < left, > right, ^ center, = numeric | all | width must be set for padding to show |
| sign | + always, (space) on positive | numeric | |
# | alternate form | integers | adds 0x / 0X / 0b / 0o base prefix |
0 | zero padding | numeric | shorthand for 0= |
| width | digits, max 4096 | all | minimum field width |
| grouping | , or _ | numeric | digit separators |
.precision | .N | floats, strings | float decimals/sig-digits; string truncation length |
| type | d b o x X int · f e E g G % float · s string | per type | selects representation |
Unsupported or out-of-order specifiers are compile-time errors.
Alignment, fill, width, and sign
| Spec | Value | Output |
|---|---|---|
{x:<6} | 12 | 12 |
{x:>6} | 12 | 12 |
{x:*^6} | 7 | **7*** |
{n:8d} | 255 | 255 |
{n:+d} | 255 | +255 |
{n: d} | 255 | 255 |
{neg:06d} | -42 | -00042 |
{n:0=+8d} | 255 | +0000255 |
Bases, alternate form, and grouping
| Spec | Value | Output |
|---|---|---|
{n:x} / {n:X} | 255 | ff / FF |
{n:b} / {n:o} | 255 | 11111111 / 377 |
{n:#x} | 255 | 0xff |
{n:#06x} | 255 | 0x00ff |
{big:,d} | 1234567 | 1,234,567 |
{big:_d} | 1234567 | 1_234_567 |
Floats and strings
| Spec | Value | Output |
|---|---|---|
{pi:.2} | 3.14159 | 3.14 |
{f:.2e} / {f:.2E} | 12.3456 | 1.23e+01 / 1.23E+01 |
{f:.4g} | 12.3456 | 12.35 |
{ratio:.1%} | 0.125 | 12.5% |
{money:,.1f} | 12345.5 | 12,345.5 |
{name:.2} / {name:.2s} | "Ada" | Ad |
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let n = 255;
let neg = -42;
let big = 1234567;
let f = 12.3456;
let ratio = 0.125;
let money = 12345.5;
let name = "Ada";
// Alignment, fill, sign, zero padding.
assert try ^"[{n:<6}][{n:>6}][{n:*^6}]" == "[255 ][ 255][*255**]";
assert try ^"[{n:+d}][{n: d}][{neg:06d}]" == "[+255][ 255][-00042]";
// Bases, alternate form, grouping.
assert try ^"{n:x}|{n:X}|{n:b}|{n:o}" == "ff|FF|11111111|377";
assert try ^"{n:#x}|{n:#06x}" == "0xff|0x00ff";
assert try ^"{big:,d}|{big:_d}" == "1,234,567|1_234_567";
// Floats and strings.
assert try ^"{f:.2e}|{f:.4g}|{ratio:.1%}|{money:,.1f}"
== "1.23e+01|12.35|12.5%|12,345.5";
assert try ^"{name:.2}|{name:.2s}" == "Ad|Ad";
}Restrictions
- Dynamic specifiers such as
{value:{width}}and conversion flags such as{value!r}are compile-time errors. - Format specifiers are not supported for enums, references, pointers, arrays,
i128,u128, or custom-width integers. Use default interpolation, dereference or select a payload, or cast to a supported representation explicitly. - Numeric type specifiers (
d,x,%, …) apply only to the matching kind:%is float-only andsis rejected on numbers. - A format specifier on a nullable value applies only to the present payload; the absent case still renders
null. - Error-result values must be unwrapped with
tryorcatchbefore interpolation.
Ownership, Allocation, And Evaluation
An interpolation expression has success type ^str and checked effect AllocError. It owns one final counted UTF-8 buffer; logical capacity counts UTF-8 bytes and the result may contain U+0000. Hole expressions are evaluated exactly once from left to right. Allocation failure follows the ordinary checked-error path and cleans every initialized temporary and buffer. The compiler does not select static, stack, or heap ownership from destination, escape shape, async lowering, or optimization.
Borrowing any complete or partial str view from the result preserves pointer and size. Neither the owner nor a view carries a ptr[size] == 0 promise.
Interpolation emits ASCII for numeric and boolean values; see types and values for the owned-text type and std.io for writing formatted text to standard handles.
Status
current public API.