Skip to content

Literals

Boolean, numeric, character, string, byte, list, map, set, tuple, record, struct, and variant literals.

Updated View as Markdown

A literal builds a value directly from source text. Its type comes from the literal’s own form, and — where the form is incomplete — from the expected type at its position.

fn f(): void {
    enabled := true              // bool
    attempts := 3                // int
    ratio := 0.5                 // float
    initial := 'A'               // char
    label := "ready"             // string
    magic := b"\x89PNG"          // []byte
    scores := [90, 85, 77]       // []int
    pair := ("port", 8080)       // (string, int)
    origin := { x: 0, y: 0 }     // { x: int, y: int }
    println("${enabled} ${attempts} ${ratio} ${initial} ${label} ${magic.len()} ${scores.len()} ${pair.0} ${origin.x}")
}

Booleans

true and false are the only bool literals.

fn f(): void {
    enabled := true
    visible: bool = false
    println("${enabled && !visible}")
}

Atoll has no truthiness. A condition must be a bool; an int, string, List, or Option is not a condition on its own.

fn f(): int {
    count := 3
    if count {
        return 1
    }
    return 0
}

Write the comparison you mean — if count > 0 { ... }.

Integers

Integer literals come in four bases, and _ may separate digits anywhere.

fn f(): void {
    decimal := 1_000_000
    hex := 0xFF
    binary := 0b1010_0011
    octal := 0o755
    println("${decimal} ${hex} ${binary} ${octal}")
}

An unsuffixed integer adopts the expected numeric type when one exists, and otherwise defaults to int.

fn f(): void {
    ordinary := 200          // int
    small: u8 = 200          // u8, from the annotation
    wide: i64 = 200          // i64, from the annotation
    println("${ordinary} ${small} ${wide}")
}

A suffix fixes the type at the literal itself, which is what you want inside a larger expression where no annotation is reachable. Every integer width has a suffix: i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 isize usize.

fn f(): void {
    port := 8080u16
    limit := 255u8
    offset := 32isize
    index := 9usize
    huge := 340282366920938463463374607431768211455u128
    masked := 0b1111_0000u8
    println("${port} ${limit} ${offset} ${index} ${huge} ${masked}")
}

Suffixes never truncate. A literal that does not fit its suffix or its contextual type is a compile-time error:

fn f(): void {
    overflow: u8 = 999
    println("${overflow}")
}

The leading minus sign is a unary operator, not part of the token. That keeps overflow checking on the positive magnitude explicit, and it is why an extreme negative bound is written with a parenthesis or an annotation when the intended width is not obvious.

fn f(): void {
    below_zero := -5
    delta := 3 - -2
    println("${below_zero} ${delta}")
}

Floating point

A float literal has a decimal point, an exponent, or both, and defaults to float. f32 and f64 select a fixed width.

fn f(): void {
    ratio := 0.625
    scientific := 6.022e23
    tiny := 1.0e-3
    grouped := 1_234.567_8
    compact := 1.5f32
    precise := 1.25f64
    println("${ratio} ${scientific} ${tiny} ${grouped} ${compact} ${precise}")
}

Integer and float literals do not silently cross categories, even when the mathematical values agree. Use a cast when a conversion is intended.

fn f(): float {
    return 1
}
fn f(): float {
    n := 1
    return n as float
}

Characters

Single quotes build a char — one Unicode scalar value, not one byte.

fn f(): void {
    letter := 'A'
    newline := '\n'
    tab := '\t'
    quote := '\''
    backslash := '\\'
    heart := '\u{2764}'
    println("${letter}${newline}${tab}${quote}${backslash}${heart}")
}

Characters are comparable, which makes classification checks direct:

fn is_lower_ascii(c: char): bool {
    return c >= 'a' && c <= 'z'
}

fn is_digit(c: char): bool {
    return c >= '0' && c <= '9'
}

Strings

Double quotes build a UTF-8 string. Escapes are processed and $name or ${expression} interpolates a value through its Display implementation.

fn f(): void {
    name := "Ada"
    sum := 2 + 3
    greeting := "Hello, $name; 2 + 3 = ${sum}"
    println(greeting)
}

Escapes

The recognized escapes are \n, \t, \r, \0, \\, \", \', \$, and \u{...} for a Unicode scalar.

fn f(): string {
    return "tab:\t quote:\" backslash:\\ dollar:\$ return:\r nul:\0 emoji:\u{1F600}"
}

\xHH is not a string escape — it belongs to byte strings only. Anything else after a backslash is ATOLL1027: Invalid escape sequence:

fn f(): string {
    return "reset:\e[0m"
}

Interpolation

$name interpolates a bare identifier and stops there. It does not pick up a following .field or call — that text stays literal. Use ${...} for anything beyond one name.

struct Point { x: int, y: int }

fn f(): void {
    p := Point { x: 3, y: 4 }
    items := [1, 2, 3]
    println("x=${p.x} count=${items.len()} first=${items[0] ?? 0}")
    println("sum=${p.x + p.y}")
}

A braced interpolation may hold a full expression, including calls, member access, indexing, and operators. The interpolated type must implement Display:

struct Point { x: int, y: int }

impl Display for Point {
    fn to_string(self): string => "(${self.x}, ${self.y})"
}

fn f(): void {
    println("origin = ${Point { x: 0, y: 0 }}")
}

Without that implementation the compiler reports ATOLL3100: type ... does not implement Display.

Multi-line and triple-quoted

An ordinary string may span lines, but the leading whitespace on each continuation line is part of the value. Triple quotes solve that: they strip the indentation shared with the closing delimiter, and they still process escapes and interpolation.

fn f(): void {
    count := 3

    flat := "line one
line two"

    block := """
        Report
          indented detail
        count: ${count}
        """

    println(flat)
    println(block)
}

Raw strings

r"..." suppresses escape processing and interpolation. It is the right form for regular expressions, Windows paths, and templates that contain $.

fn f(): void {
    pattern := r"\d+\.\d+"
    shell := r"$HOME/bin"
    println("${pattern} ${shell}")
}

“Raw” changes content processing, not delimiting: a raw string still ends at its closing ", so it cannot contain an unescaped double quote.

The r prefix applies to the single-quoted form only. A triple-quoted string always processes escapes and interpolation, whether or not you write r""":

fn f(): string {
    return r"""
    reset: \e[0m
    """
}

To keep a backslash-heavy block literal, build it from raw single-quoted pieces:

fn f(): void {
    lines := [
        r"path: C:\bin\tools",
        r"regex: \d+\.\d+",
    ]
    println(lines.join("\n"))
}

Slicing, iteration, and Unicode boundary rules are in Strings.

Byte strings

b"..." builds a []byte. Source bytes must be ASCII, and \xHH writes an arbitrary byte.

fn f(): void {
    png := b"\x89PNG\r\n\x1a\n"
    tag: []byte = b"ATOLL"
    println("${png.len()} ${tag.len()}")
}

Non-ASCII source characters are rejected — one visible character need not be one byte, so the encoding must be explicit:

fn f(): void {
    greeting := b"héllo"
    println("${greeting.len()}")
}

Encode Unicode text through the string API instead:

fn f(): void {
    text := "héllo"
    encoded := text.bytes()
    println("${text.len()} chars, ${encoded.len()} bytes")
}

Lists

Square brackets build a List. The element type comes from the elements, or from the expected type when the literal is empty.

fn f(): void {
    numbers := [1, 2, 3]
    names: []string = []
    grid := [[1, 2], [3, 4]]
    println("${numbers.len()} ${names.len()} ${grid.len()}")
}

An empty literal with nothing to infer from is an error, so annotate it:

fn f(): void {
    values := []
    println("${values.len()}")
}

List literals nest and hold aggregates, which is how fixture data is usually written:

struct User { id: int, name: string, active: bool }

fn active_names(users: []User): []string {
    return users.filter(u => u.active).map(u => u.name)
}

fn f(): void {
    users := [
        User { id: 1, name: "ada", active: true },
        User { id: 2, name: "grace", active: false },
        User { id: 3, name: "alan", active: true },
    ]
    println("${active_names(users).len()} active")
}

Fixed arrays

[N]T is a fixed-length array. It uses the same bracket syntax as a list and is selected by the expected type.

fn f(): void {
    magic: [4]byte = [0x7F, 0x45, 0x4C, 0x46]
    weights: [3]int = [1, 2, 3]
    mut total := 0
    for w in weights {
        total += w
    }
    println("${magic[0]} ${total}")
}

A fixed array is indexed and iterated but does not carry the List method surface — magic.len() is not available, because the length is in the type.

Maps and sets

Map and Set have no brace literal. Build them with the prelude constructors and fill them in.

fn f(): void {
    mut ports: Map[string, int] = Map.new()
    ports.put("http", 80)
    ports.put("https", 443)

    mut tags: Set[string] = Set.new()
    tags.add("web")
    tags.add("web")           // already present; the set stays at one entry

    println("${ports.get("http") ?? 0} ${ports.len()} ${tags.len()}")
}

Map.new and Set.new take an optional capacity, and the type arguments can be supplied at the call instead of on the binding:

fn f(): void {
    mut counts := Map.new[string, int](16)
    for word in ["a", "b", "a"] {
        counts.put(word, (counts.get(word) ?? 0) + 1)
    }
    for word, n in counts {
        println("$word: $n")
    }
}

Map.get returns an Option, which is why ?? 0 appears above. Set membership is contains; map membership is contains_key.

Tuples

A comma inside parentheses makes a tuple. () is the unit value.

fn f(): void {
    pair := ("port", 8080)
    triple := (1, true, "ready")
    empty := ()
    println("${pair.0}=${pair.1} ${triple.0} ${triple.1} ${triple.2}")
}

Members are read positionally with .0, .1, and so on. Without a comma, parentheses only group; a one-element tuple needs a trailing comma.

fn f(): void {
    value := 3
    grouped := (value)            // int — just grouping
    single: (int,) = (value,)     // (int,) — a one-element tuple
    println("${grouped}")
}

Tuples are the usual way to return two values without declaring a type:

fn divide(n: int, d: int): (int, int) {
    return (n / d, n % d)
}

fn f(): void {
    (quotient, remainder) := divide(17, 5)
    println("${quotient} rem ${remainder}")
}

Records

Braces holding field: value pairs build an anonymous record. Its type is structural — spelled out in full — and it needs no declaration.

fn f(): void {
    origin := { x: 0, y: 0 }
    annotated: { x: int, y: int } = { x: 1, y: 2 }
    nested := { host: "localhost", port: 8080, tls: { enabled: true } }
    println("${origin.x} ${annotated.y} ${nested.tls.enabled} ${nested.host}:${nested.port}")
}

Records compose with lists and with inferred return types, which makes them convenient for ad-hoc result shapes:

fn summarize(values: []int) {
    mut total := 0
    mut worst := 0
    for v in values {
        total += v
        if v > worst { worst = v }
    }
    return { total: total, worst: worst, count: values.len() }
}

fn f(): void {
    s := summarize([3, 9, 4])
    println("total=${s.total} worst=${s.worst} count=${s.count}")
}

See Records for structural typing rules.

Structs and variants

A nominal type name followed by braces constructs a struct. Commas are required between fields in a literal, even when the declaration separated them with newlines.

struct Point {
    x: int
    y: int
}

fn f(): void {
    p := Point { x: 10, y: 20 }
    q := Point {
        x: 1,
        y: 2,
    }
    println("${p.x} ${q.y}")
}

Newline-only separation inside a literal is a parse error:

struct Point { x: int, y: int }

fn f(): Point {
    return Point {
        x: 1
        y: 2
    }
}

Field shorthand uses a visible binding with the same name:

struct Point { x: int, y: int }

fn f(): Point {
    x := 10
    y := 20
    return Point { x, y }
}

Enum variants use the enum name for the unit and tuple forms. A struct-shaped variant is written unqualified, because Enum.Variant { ... } parses the braces as a separate record:

enum Color { Red, Green, Blue }

enum Shape {
    Circle { radius: float }
    Square { side: float }
}

fn f(): void {
    c := Color.Red
    round := Circle { radius: 1.5 }      // unqualified struct-shaped variant
    boxed := Shape.Circle(2.0)           // positional form of the same variant
    match round {
        Circle { radius } => println("circle ${radius}")
        Square { side } => println("square ${side}")
    }
    println("${c == Color.Red} ${boxed == Shape.Circle(2.0)}")
}

Error variants follow the same rule after error:

error ParseError {
    InvalidDigit { position: int }
    Empty
}

fn parse_digit(text: string): int ! ParseError {
    if text.is_empty() { error Empty }
    error InvalidDigit { position: 4 }
}

Option and Result

Some, None, Ok, and Err are prelude constructors, not literal syntax, but they behave like literals at a typed destination.

error LoadError { NotFound }

fn f(): void {
    found: int? = Some(3)
    missing: int? = None
    good: Result[int, LoadError] = Ok(7)
    bad: Result[int, LoadError] = Err(LoadError.NotFound)
    println("${found ?? 0} ${missing ?? -1} ${good.unwrap_or(0)} ${bad.unwrap_or(0)}")
}

A bare None with no surrounding Option[T] has nothing to infer T from and must be annotated.

When a literal needs context

Literal Needs context when
Unsuffixed number a non-default numeric width is intended
[] no element supplies T
None no surrounding Option[T] fixes T
List syntax for [N]T a fixed array rather than a List is intended
Closure literal parameter types are not inferable from the destination

Context comes from a binding annotation, a parameter, a return type, a field type, a collection element, a branch join, or an explicit generic argument. It never comes from a value observed later at runtime.

Literal construction evaluates its embedded expressions left to right. String interpolations, list elements, tuple members, and field initializers may all have effects — being inside a literal does not make an expression constant. For what is folded at compile time, see Constants.

Composed example

struct Sample {
    label: string
    value: float
    tags: []string
}

const WARN_THRESHOLD = 0.75

fn classify(sample: Sample): string {
    return if sample.value >= WARN_THRESHOLD { "warn" } else { "ok" }
}

fn report(samples: []Sample): string {
    mut out := ""
    for sample in samples {
        if out.len() > 0 { out = out + "\n" }
        count := sample.tags.len()
        out = out + "${sample.label}\t${sample.value}\t${classify(sample)}\t${count} tags"
    }
    return out
}

fn main(): void {
    samples := [
        Sample { label: "cpu", value: 0.91, tags: ["host", "core0"] },
        Sample { label: "mem", value: 0.42, tags: [] },
        Sample { label: "disk", value: 0.75, tags: ["ssd"] },
    ]
    println(report(samples))
}
Navigation

Type to search…

↑↓ navigate↵ selectEsc close