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 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.

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

  • A dedicated distinct type, as above — type Sql String both names the literal and carries the value it produces.
  • 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/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.