Typed Literals
You’ve already met one of these: Date"2026-06-15" in Dates & Times
isn’t a separate language feature, it’s a regular type-qualified function call dressed up as
a literal. The grammar is <Type>"…" for any type in scope, and the body
becomes a list of fragments — alternating literal text and ${…}
interpolations — that the type’s handler reduces into a value.
The shape
Section titled “The shape”A typed literal starts with a type name: Sql"...", Date"...",
Box"...". The prefix type decides how to turn the literal body into a
value.
To opt in, the prefix type implements Literal by defining
from_fragments. A Sql"..." use site becomes Sql.from_fragments([...]),
where the list contains literal text plus the values from ${...} slots.
That is ordinary interface dispatch, so the same coherence and orphan-rule
checks apply:
import {
std/io: IO
std/literal: Fragment, Literal
}
type Sql String
impl Literal for Sql {
fn from_fragments(fragments: List<Fragment<String>>): Sql {
body = Iter.reduce(fragments, |acc = "", frag|
case frag {
.Static(s) -> acc + s
.Dynamic(v) -> acc + "'" + v + "'"
}
)
Sql(body)
}
}
fn main() {
user = "alice"
Sql(text) = Sql"SELECT * FROM users WHERE name = ${user}"
IO.print(text)
}
The body of Sql"…" is split into Static(text) and Dynamic(value) fragments
slots wherever ${…} appears. The handler walks the list and builds the
result however it likes — here we add SQL-style quotes around dynamic
values, but a real implementation might validate, parameterize, or escape.
The prefix is just a type
Section titled “The prefix is just a type”Because the prefix is a type, you have two natural ways to spell one:
- A dedicated distinct type, as above —
type Sql Stringboth names the literal and carries the value it produces. - An existing domain type, so the use site reads as a constructor.
Box"…"builds an actualBox— same spelling whether you writeBox{contents: "x"}orBox"x":
import std/literal: Fragment, Literal
struct Box {
field contents: String
}
impl Literal for Box {
fn from_fragments(fragments: List<Fragment<String>>): Box {
body = Iter.reduce(fragments, |acc = "", frag|
case frag {
.Static(s) -> acc + s
.Dynamic(v) -> acc + v
}
)
Box{contents: body}
}
}
fn main(): Box {
dbg Box"hello"
dbg Box{contents: "world"}
}
Box"hello" and Box{contents: "hello"} build the same value (an actual
Box), through different syntactic doors.
The next chapter — FFI & Dynamic — steps back to the
host boundary: embedding Nomi in host programs, wrapping host libraries in
Nomi packages, and using Dynamic when a boundary value’s shape is not known
yet.