Skip to content

Fmt[V, S @ {*, *(Int | Sym): Value}]

Preserves literal text, bound interpolations, and unbound parameters from a t"..." string.

V is the union of the interpolated values' types, and S is the schema of the arguments that format needs to fill the sequence's parameters, by position or by name. Since a sequence is immutable, one whose values have narrower types can stand in for one whose values are wider.

Every segment is a Str of literal text, a FmtValue interpolation, or a FmtParam still waiting to be filled. This type behaves like an immutable array: indexable, iterable, spreadable, and destructurable.

Trust

A sequence written as t"..." carries a guarantee: its Str segments are program text, and everything interpolated into it is a FmtValue or FmtParam. That is what lets a consumer treat the literal segments as the trusted skeleton of a command and each interpolation as data to be bound or quoted. sqlite is a prime example, as it relies on this guarantee to exclude SQL injection vulnerabilities.

Toward that end, no conversion implicitly expands a sequence, so it can't be expanded by accident before it reaches its consumer. str and plain interpolation ("$seq", "${seq}", "${seq:s}") raise a TypeError. dbg and verbatim, and so the ! conversion and command arguments, give the source form, reconstructed from the literal text and each interpolation's source. An interpolation built at runtime has no source, so its bound value's debug form stands in. format() is the only expansion.

let count = 3
assert_eq (dbg t"n=${count:03d}") r#"t"n=${count:03d}""#
assert_eq (verbatim t"n=${count:03d}") (dbg t"n=${count:03d}")

Building a sequence yourself must uphold this guarantee. The constructor accepts whatever segments it is given, so a Str built from untrusted input would subsequently be treated as trusted. Programmatic construction of Fmt must be done with care.

# Fine: the query text is literal, the value is bound.
let query = t"select * from users where name = $name"

# Fine: assembled at runtime, but the untrusted value is still bound.
let assembled = Fmt ["select * from users where name = ", (FmtValue name)]

# Danger: untrusted input is treated as a trusted string literal
let injected = Fmt ["select * from users where name = ", name]

Example

let user = "root"
let seq = t"user: ${user:>8}"

# Expanding gives what the equivalent `"..."` would have.
assert_eq $seq.format() "user:     root"

# The interpolated value is still there to be inspected.
let std.FmtValue(segment) = seq[1]
assert_eq $segment.value $user
assert_eq $segment.width 8
Open in playground

Implements: Index[FmtSegment[V]], Spread[FmtSegment[V]], Unpack[FmtSegment[V], Rest: BaseSeq[FmtSegment[V]]]

Constructor

Fmt segments

Builds a sequence from an iterable of segments.

Writing a t"..." is the usual way to get one; the constructor is for assembling a sequence at runtime. The type checker doesn't know which parameters the result has.

Parameters

NameTypeDescription
segments Iterable[FmtSegment[V]]

Errors

Exception Condition
TypeError A segment is not a Str, FmtValue, or FmtParam

Example

let built = Fmt ["a=", (FmtValue 42), ";"]
assert_eq $built.len 3
assert_eq $built.format() "a=42;"

Fields

len @ Int [get]

The number of segments.

let greeting = t"hello $name!"
assert_eq $greeting.len 3

Methods

(eq) other -> Bool

Two sequences are equal when their segments are. How a sequence was assembled does not show. The text each interpolation records does: a segment carries its source, so t"$name" and t"${name}" are not equal.

Parameters

NameTypeDescription
other Value

(fmt) spec -> Str

Formats the sequence's source form for dbg and verbatim. Display formatting raises TypeError; see Trust.

Parameters

NameTypeDescription
spec FmtSpec

(index)[K] key -> IndexItem[{*FmtSegment[V]}, K] overload 1 of 2

Returns a segment: a Str of literal text, a FmtValue, or a FmtParam.

let greeting = t"hello $name!"
assert_eq $greeting[0] "hello "
assert_eq $greeting[1].source r"$name"

Assigning to a segment raises an ImmutableError.

Parameters

NameTypeDescription
key K

(iter)() -> UnpackIter[FmtSegment[V]]

Returns an iterator over the segments in order, so a consumer can decide what to do with each.

for segment = greeting
  echo $ type $segment

The console is the worked example of such a consumer: term walks a sequence segment by segment rather than converting it, which is what lets styling interpolated into one survive to the terminal.

(spread)() -> Spread[FmtSegment[V]]

Spreads the segments.

(unpack)() -> Unpack[FmtSegment[V], Rest: BaseSeq[FmtSegment[V]]]

Destructures the segments positionally.

format[B @ {*Value, **Value}] ...bindings -> Str

Expands the sequence and returns the result: literal text as it stands, and each interpolation through its own specification. This is the only way to get the expansion — see Trust.

Given arguments, it fills the parameters first. Positional argument i fills ${#i}, and a keyword argument fills ${#name}:

  • Holes inside a bound Fmt are filled too, depth first, by the same arguments: a nested ${#0} is filled by argument 0 as well. The type checker sees only the outer holes, so it may report an argument for a nested hole as unused.
  • A filled hole keeps the parameter's specification and source.
  • A hole left unfilled, or an argument no hole consumed, is an error.

Parameters

NameTypeDescription
self Fmt[V, B] Receiver.
...bindings B

Errors

Exception Condition
MissingPosError A numbered parameter unfilled
MissingKeyError A named parameter unfilled
UnexpectedPosError A positional argument unused
UnexpectedKeyError A keyword argument unused

Example

let count = 3
assert_eq $(t"n=${count:03d}").format() "n=003"

let stmt = t"select * from t where a = ${#0} and c = ${#name}"
assert_eq $stmt.format(1, name: "n") "select * from t where a = 1 and c = n"