Strings and Interpolation

String literals, UTF-8 text, escapes, interpolation, and format specs.

String literals have type str. A str is a borrowed UTF-8 text view; it does not own storage and cannot contain embedded NUL bytes.

Double-quoted literals process escapes and interpolation. Single-quoted literals are raw text literals: they do not process escapes or interpolation.

import std.os;

fn main() {
    let value = 42;
    let label = "ready";

    _ = os.write("{label}: {value}\n");
}

Use std.string.String when text needs owned, mutable storage.

Escapes

Escapes use a backslash.

EscapeMeaning
\nnewline
\ttab
\rcarriage return
\\backslash
\"double quote
\'single quote
\eASCII escape byte, 0x1b
\xNNbyte from exactly two hex digits
\u{...}Unicode scalar value encoded as UTF-8
import std.os;

fn main() {
    _ = os.write("line one\nline two\n");
}

Escape decoding happens before literal-only size checks and call-argument borrowed byte views are computed. Escapes that decode to invalid UTF-8 or to a NUL byte are not valid text literals. Use byte slices and std.bytes.Bytes for arbitrary bytes.

import std.bytes;
import std.mem;

fn check(data: bytes.Bytes) {
    assert data.length == 2;
    assert data[0] == 0 as u8;
    assert data[1] == 65 as u8;
}

fn main() {
    let raw_data = [65 as u8, 255 as u8];
    let raw = bytes.Bytes(mem.slice_of<u8>(raw_data.ptr, raw_data.length));
    let heart = "\u{2665}";
    let esc = bytes.Bytes("\e");
    let nul_data = [0 as u8, 65 as u8];
    let nul_view = bytes.Bytes(mem.slice_of<u8>(nul_data.ptr, nul_data.length));

    assert raw[0] == 65 as u8;
    assert raw[1] == 255 as u8;
    assert heart.length == 1;
    assert heart.size == 3;
    assert esc[0] == 27 as u8;
    check(nul_view);
}

Unknown escapes are preserved as a backslash followed by the escaped character.

Raw Text Literals

Single-quoted string literals are raw text. They are useful for JSON fixtures, regular-expression-like text, and examples where backslashes or {...} should stay literal.

import std.bytes;

fn main() {
    let json = '{"name":"zynx"}';
    let slash_n = '\n';
    let braces = '{value}';

    assert bytes.Bytes(json).length == 15;
    assert bytes.Bytes(slash_n).length == 2;
    assert bytes.Bytes(braces).length == 7;
}

Expected output: none. The assertions pass silently.

Single-quoted literals cannot contain a single quote. Use hash-delimited raw strings such as r#"don't escape"# when the text itself contains '.

UTF-8

For valid UTF-8 text, str.length counts Unicode scalar values. str.size counts bytes. These are not grapheme-cluster APIs. Use std.unicode when strict validation, fallible decoding, UTF-16LE transcoding, or malformed byte handling is required.

fn main() {
    let text = "Aé世";

    assert text.length == 3;
    assert text.size == 6;
}

Iteration walks UTF-8 codepoints:

import std.os;

fn main() {
    for ch in "Aé世" {
        _ = os.write("{ch}\n");
    }
}

Text indexing and slicing are not 0.0.0-dev operations. Use std.bytes when byte indexing or byte slicing is intended.

Interpolation

Interpolation uses {expression} inside a string. The expression is evaluated and formatted into the resulting str.

import std.os;

fn main() {
    let score = 42;
    let name = "Ada";

    _ = os.write("{name}: {score}\n");
}

Interpolated strings can be passed directly, assigned, or returned as str:

fn status(name: str, score: i32) -> str {
    return "{name}: {score}";
}

They also participate in call-argument borrowed byte conversions, so byte writers can accept formatted text directly:

import std.io;

fn main() {
    let score = 42;

    try io.stdout.write("score: {score}\n");
}

The compiler handles the temporary formatted storage for these cases. The result is still borrowed str, not an owned String.

For simple CLI output, prefer interpolation before calling std.io print helpers:

import std.io;

fn main() {
    let package = "demo";
    let missing = "zynx.lock";

    io.println("package: {package}");
    io.eprintln("missing file: {missing}");
}

Format Specs

Interpolation supports a static Python-inspired format-spec subset after :.

import std.os;

fn main() {
    let x = 12;
    let y = 7;
    let pi = 3.14159;
    let name = "Ada";
    let flag = true;

    _ = os.write("left:[{x:<6}] right:[{x:>6}]\n");
    _ = os.write("zero:[{y:02}] fill:[{y:*^6}]\n");
    _ = os.write("float:[{pi:.2}] str:[{name:.2}] bool:[{flag:.3}]\n");
}

Output:

left:[12    ] right:[    12]
zero:[07] fill:[**7***]
float:[3.14] str:[Ad] bool:[tru]

The supported shape is:

[[fill]align][sign][#][0][width][grouping][.precision][type]

Supported pieces include:

FormMeaning
<, >, ^left, right, or center alignment
*^6, *>8custom one-character fill plus alignment and width
+, space, =numeric sign policy or sign-aware padding
#alternate integer prefixes such as 0x, 0b, or 0o
6minimum width
02zero padding for right-aligned numeric output
,, _numeric grouping separators
.2precision; decimal places for floats, maximum bytes for str/bool
d, b, o, x, Xinteger and enum formatting
f, e, E, g, G, %floating-point formatting
sstring and boolean formatting
import std.os;

fn main() {
    let n = 255;
    let neg = -42;
    let big = 1234567;
    let ratio = 0.125;
    let f = 12.3456;
    let text = "abcdef";

    _ = os.write("bases:{n:d}|{n:x}|{n:X}|{n:b}|{n:o}\n");
    _ = os.write("alt:{n:#x}|{n:#b}|{n:#o}|{n:#06x}\n");
    _ = os.write("zero:[{neg:06d}][{n:0=+8d}]\n");
    _ = os.write("group:{big:,d}|{big:_d}|{n:_b}\n");
    _ = os.write("float:{f:.2e}|{f:.4g}|{ratio:.1%}\n");
    _ = os.write("text:{text:s}|{text:.3s}\n");
}

Output:

bases:255|ff|FF|11111111|377
alt:0xff|0b11111111|0o377|0x00ff
zero:[-00042][+0000255]
group:1,234,567|1_234_567|1111_1111
float:1.23e+01|12.35|12.5%
text:abcdef|abc

When no type suffix is written, the formatter chooses the conversion from the expression type. Integers print as decimal, floats use decimal notation, bool prints true or false, str prints the text, nullable values print the contained value or null, references are formatted through the referenced value, and raw pointers/array-shaped values print as addresses.

Conversion flags such as !r and dynamic nested specs such as {value:{width}} are intentionally unsupported.

Literal braces do not need a special escape in plain text. Doubled braces are the interpolation escape for one literal brace:

import std.os;

fn main() {
    _ = os.write("braces={{name}}\n");
}

Output:

braces={name}

An error-result value cannot be interpolated directly. Handle it with try or catch before interpolation. A separate std.fmt or printf-style formatting API is deferred.