Skip to content

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

Zynx
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 static str; 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.

Zynx
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:

ValueRenders asExample output
signed/unsigned integer, including custom widthsdecimal digits42, -5
i128 / u128 and wider custom integersfull decimal (never truncated)340282366920938463463374607431768211455
float (f16/bf16/f32/f64)shortest round-trip decimal, auto fixed/scientific1.5, 1500, 1e300
f128shortest round-trip decimal at full binary128 precision (up to 36 significant digits)3.1415926535897932384626433832795028
booltrue or falsetrue
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 enumthe variant nameFast
strthe text itselfborrowed
nullable T?the payload, or the literal null when absent7, null
reference, pointer, unique pointer, array/slice viewaddress-like hex0x16d8f3a40

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.

Zynx
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:

Zynx
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]}
FieldFormsApplies toNotes
fill + alignany char + one of < > ^ =allfill defaults to space; = pads between sign and digits
align< left, > right, ^ center, = numericallwidth must be set for padding to show
sign+ always, (space) on positivenumeric
#alternate formintegersadds 0x / 0X / 0b / 0o base prefix
0zero paddingnumericshorthand for 0=
widthdigits, max 4096allminimum field width
grouping, or _numericdigit separators
.precision.Nfloats, stringsfloat decimals/sig-digits; string truncation length
typed b o x X int · f e E g G % float · s stringper typeselects representation

Unsupported or out-of-order specifiers are compile-time errors.

Alignment, fill, width, and sign

SpecValueOutput
{x:<6}1212
{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

SpecValueOutput
{n:x} / {n:X}255ff / FF
{n:b} / {n:o}25511111111 / 377
{n:#x}2550xff
{n:#06x}2550x00ff
{big:,d}12345671,234,567
{big:_d}12345671_234_567

Floats and strings

SpecValueOutput
{pi:.2}3.141593.14
{f:.2e} / {f:.2E}12.34561.23e+01 / 1.23E+01
{f:.4g}12.345612.35
{ratio:.1%}0.12512.5%
{money:,.1f}12345.512,345.5
{name:.2} / {name:.2s}"Ada"Ad
Zynx
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 and s is 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 try or catch before 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.

Released under the MIT License.