Skip to content

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.

A typed literal’s prefix is a type that implements the Literal interface — one function, from_fragments(List<Fragment<'I>>) -> 'R, with no self parameter. The type’s own name prefixes the literal at use sites; I is the interface every ${…} slot must satisfy, and R is what the literal produces. A Sql"…" site desugars to Sql.from_fragments([...]) — the ordinary interface dispatch you’ve seen for Display/Debug, so coherence and the orphan rule apply unchanged:

import {
  std/io: IO
  std/literal: Fragment, Literal
}

type SafeQuery String

// A zero-sized type whose only job is to name the literal.
type Sql

impl Literal for Sql {
  fn from_fragments(fragments: List<Fragment<String>>): SafeQuery {
    body = Iter.reduce(fragments, |acc = "", frag|
      case frag {
        .Static(s) -> acc + s
        .Dynamic(v) -> acc + "'" + v + "'"
      }
    )
    SafeQuery(body)
  }
}

fn main() {
  user = "alice"
  q = Sql"SELECT * FROM users WHERE name = ${user}"

  SafeQuery(text) = q
  IO.print(text)
}

The body of Sql"…" is split into Static(text) runs and Dynamic(value) 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.

Because the prefix is a type, you have two natural ways to spell one:

  • A zero-sized type Sql, as above — its only purpose is to name the literal, giving a DSL a dedicated stateless value type.
  • An existing domain type, so the use site reads as a constructor. Box"…" builds an actual Box — same spelling whether you write Box{contents: "x"} or Box"x":
import {
  std/io: IO
  std/literal: Fragment, Literal
}

struct Box {
  field contents: String
}

impl Display for Box {
  fn to_string(b: Box): String {
    "Box(${b.contents})"
  }
}

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() {
  IO.print(Box"hello")
  IO.print(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 — covers how Nomi programs call out to the current Go host through extern fn declarations, and how the Dynamic type handles untyped values at that boundary.