Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

A tour of Brim

Every example on this page is a complete program; the block under it is its output. The language specification has the details.

Values and variables

fn main() {
    let name: string = "Brim"
    let year: i64 = 2026
    let ready: bool = true
    print(name + " " + to_string(year))
    print(ready)

    let mut count: i64 = 0
    count += 5
    count = count * 2
    print(count)
}
Brim 2026
true
10

Variables are immutable unless declared with let mut, and always have a type. The basic types are i64, f64, bool and string. Integer arithmetic is checked: an overflow stops the program with a run-time error instead of wrapping around.

Numbers

fn main() {
    let price: f64 = 19.95
    let count: i64 = 3
    let total: f64 = price * to_float(count)
    print(total)
    print(0.1 + 0.2)
    print(to_int(total))
    match "2.5e3".parse_float() {
        Some(x) => print(x / 1000.0)
        None => print("not a number")
    }
}
59.849999999999994
0.30000000000000004
59
2.5

i64 is for counting and f64 for measuring. They never mix by accident: price * count is an error until you convert one with to_float or to_int. f64 follows IEEE 754, so 0.1 + 0.2 is not exactly 0.3, and 1.0 / 0.0 is inf rather than an error.

For bytes and other fixed sizes there are u8, i32 and u64. A literal takes the type it needs, and a result that does not fit stops the program instead of wrapping around:

fn main() {
    let data: List[u8] = "Hé".bytes()
    print(data)
    let mut level: u8 = 250
    level += 5
    print(level)
    let big: u64 = 18446744073709551615
    print(big / 3)
    print(to_i32(level) - 300)
    print(string_from_bytes(data))
}
[72, 195, 169]
255
6148914691236517205
-45
Some("Hé")

Functions

fn area(width: i64, height: i64) -> i64 {
    return width * height
}

fn greet(name: string) {
    print("Hello, " + name)
}

fn main() {
    greet("Ada")
    print(area(3, 4))
}
Hello, Ada
12

Functions without -> return nothing. A function can be used before its declaration. Statements end at the end of the line.

Functions as values

fn apply(f: fn(i64) -> i64, x: i64) -> i64 {
    return f(x)
}

fn double(x: i64) -> i64 {
    return x * 2
}

fn main() {
    print(apply(double, 21))
    let offset: i64 = 100
    let shift: fn(i64) -> i64 = fn(x: i64) -> i64 {
        return x + offset
    }
    print(apply(shift, 1))
}
42
101

fn(i64) -> i64 is a function type. A fn expression copies the variables it uses when it is made, so it never changes them.

Control flow

fn main() {
    let n: i64 = 7
    if n % 2 == 0 {
        print("even")
    } else if n > 5 {
        print("odd and large")
    } else {
        print("odd")
    }

    let mut i: i64 = 0
    while i < 3 {
        i += 1
    }
    print(i)

    for k in 1..4 {
        print(k * k)
    }
}
odd and large
3
1
4
9

for runs over a range a..b (without b) or over a list. break and continue work in both kinds of loop.

Structs

struct Point {
    x: i64,
    y: i64,
}

fn main() {
    let a: Point = Point { x: 1, y: 2 }
    let mut b: Point = a
    b.x = 10
    print(a.x)
    print(b.x + b.y)
}
1
12

Structs are values: let mut b = a copies a, so changing b leaves a alone.

== compares structs, enums, lists and maps by their contents, and print shows them:

struct Point {
    x: i64,
    y: i64,
}

fn main() {
    let a: Point = Point { x: 1, y: 2 }
    print(a == Point { x: 1, y: 2 })
    print([a, Point { x: 3, y: 4 }])
    print(Some("ada"))
}
true
[Point { x: 1, y: 2 }, Point { x: 3, y: 4 }]
Some("ada")

Enums and match

enum Shape {
    Circle(i64),
    Rect(i64, i64),
    Empty,
}

fn describe(s: Shape) -> string {
    match s {
        Shape.Circle(r) => return "circle with radius " + to_string(r)
        Shape.Rect(w, h) => return to_string(w) + " by " + to_string(h)
        Shape.Empty => return "nothing"
    }
}

fn main() {
    print(describe(Shape.Circle(2)))
    print(describe(Shape.Rect(3, 4)))
    print(describe(Shape.Empty))
}
circle with radius 2
3 by 4
nothing

match must cover every variant; _ matches the rest. It also works on integers, strings and booleans.

Patterns nest, | joins alternatives, and if adds a condition to an arm:

fn classify(o: Option[i64]) -> string {
    match o {
        Some(0) => return "zero"
        Some(1) | Some(2) | Some(3) => return "small"
        Some(n) if n < 0 => return "negative"
        Some(n) => return "large: " + to_string(n)
        None => return "nothing"
    }
}

fn main() {
    print(classify(Some(2)))
    print(classify(Some(-5)))
    print(classify(Some(40)))
    print(classify(None))
}
small
negative
large: 40
nothing

A lowercase name in a pattern binds the value; a guard that is false moves on to the next arm.

An enum can contain itself through a Box, which keeps the value on the heap:

enum Expr {
    Num(i64),
    Add(Box[Expr], Box[Expr]),
}

fn eval(e: Expr) -> i64 {
    match e {
        Num(n) => return n
        Add(a, b) => return eval(a.value()) + eval(b.value())
    }
}

fn main() {
    let e: Expr = Expr.Add(Box(Expr.Num(2)), Box(Expr.Num(3)))
    print(e)
    print(eval(e))
}
Add(Num(2), Num(3))
5

Lists and maps

fn main() {
    let mut scores: List[i64] = [7, 3, 9]
    scores.push(5)
    let mut total: i64 = 0
    for s in scores {
        total += s
    }
    print(to_string(scores.len()) + " scores, total " + to_string(total))
    print(scores[2])

    let mut ages: Map[string, i64] = ["Ada": 36]
    ages["Grace"] = 85
    for name in ages.keys() {
        print(name + ": " + to_string(ages[name]))
    }
    print(ages.contains_key("Linus"))
}
4 scores, total 24
9
Ada: 36
Grace: 85
false

Lists and maps are values too. Indexing outside a list, or with a key a map does not have, stops the program; get returns an Option instead.

Methods

struct Counter {
    count: i64,
}

impl Counter {
    fn new() -> Counter {
        return Counter { count: 0 }
    }

    fn add(mut self, n: i64) {
        self.count += n
    }

    fn twice(self) -> i64 {
        return self.count * 2
    }
}

fn main() {
    let mut c: Counter = Counter.new()
    c.add(3)
    c.add(4)
    print(c.twice())
}
14

A method with mut self changes the value it is called on; one with self gets a copy. Methods without self are called on the type.

Generics

struct Pair[A, B] {
    first: A,
    second: B,
}

fn swap[A, B](p: Pair[A, B]) -> Pair[B, A] {
    return Pair { first: p.second, second: p.first }
}

fn main() {
    let p: Pair[string, i64] = Pair { first: "answer", second: 42 }
    let q: Pair[i64, string] = swap(p)
    print(q.first)
    print(q.second)
}
42
answer

Type arguments are inferred from the arguments and the expected type.

Traits

trait Shape {
    fn area(self) -> f64
}

struct Square {
    side: f64,
}

impl Shape for Square {
    fn area(self) -> f64 {
        return self.side * self.side
    }
}

fn largest[T: Shape](shapes: List[T]) -> f64 {
    let mut best: f64 = 0.0
    for s in shapes {
        if s.area() > best {
            best = s.area()
        }
    }
    return best
}

fn count[T: Eq](xs: List[T], x: T) -> i64 {
    let mut n: i64 = 0
    for y in xs {
        if y == x {
            n += 1
        }
    }
    return n
}

fn main() {
    print(largest([Square { side: 2.0 }, Square { side: 3.0 }]))
    print(count(["a", "b", "a"], "a"))
}
9.0
2

A bound such as T: Shape lets generic code call the trait's methods. The built-in traits Eq and Show allow == and print on a T.

Errors: Option, Result and ?

fn parse_age(text: string) -> Result[i64, string] {
    match text.parse_int() {
        Some(n) => {
            if n < 0 {
                return Err("an age cannot be negative")
            }
            return Ok(n)
        }
        None => return Err("not a number: " + text)
    }
}

fn total_age(a: string, b: string) -> Result[i64, string] {
    return Ok(parse_age(a)? + parse_age(b)?)
}

fn main() {
    match total_age("36", "85") {
        Ok(n) => print(n)
        Err(e) => print("error: " + e)
    }
    match total_age("36", "old") {
        Ok(n) => print(n)
        Err(e) => print("error: " + e)
    }
}
121
error: not a number: old

? takes the value out of an Ok or Some, or returns the Err or None from the function at once.

Strings and the standard library

An f-string puts values in text:

fn main() {
    let name = "Ada"
    let langs = ["en", "fr"]
    print(f"{name} speaks {langs.len()} languages: {langs}")
}
Ada speaks 2 languages: ["en", "fr"]
fn main() {
    let line: string = "  Brim is small, Brim is fast  "
    let text: string = line.trim()
    print(text.to_upper())
    print(text.replace("Brim", "it"))
    for word in text.split(", ") {
        print(word.len())
    }
    print(text.starts_with("Brim"))
}
BRIM IS SMALL, BRIM IS FAST
it is small, it is fast
13
12
true

The standard library also reads input (read_line), files (read_file, write_file), arguments (args) and environment variables (env), and sets the exit code (exit).

Modules

A program can be split into files. Every file is a module; pub makes an item visible to other modules:

src/main.brim
src/geometry.brim
// src/geometry.brim
pub fn square(side: i64) -> i64 {
    return side * side
}
// src/main.brim
import geometry

fn main() {
    print(geometry.square(4))
}

Tasks

std.task runs functions at the same time. A task gets copies of what its fn uses, so tasks never change each other's values; they hand results back with join and talk through channels:

import std.task

fn main() {
    let total = task.spawn(fn() -> i64 {
        let mut sum = 0
        for i in 0..1000 {
            sum += i
        }
        return sum
    })
    let words: task.Channel[string] = task.channel()
    task.detach(fn() {
        words.send("hello")
        words.close()
    })
    print(total.join())
    print(words.receive())
}
499500
Some("hello")

The next chapter covers projects, tests and dependencies.