A statically typed, compiled language for backend services, by Ordnary.
Why Brim · Status · Getting started · Language spec · Roadmap
Brim is a programming language for the kind of software Ordnary builds every day: APIs, backend services, CLI tools and developer tooling. It compiles ahead of time, keeps Go's simplicity, and fixes the problems we keep running into in Go.
fn fizzbuzz(n: i64) -> string {
if n % 15 == 0 {
return "FizzBuzz"
} else if n % 3 == 0 {
return "Fizz"
} else if n % 5 == 0 {
return "Buzz"
}
return to_string(n)
}
fn main() {
let mut i: i64 = 1
while i <= 15 {
print(fizzbuzz(i))
i += 1
}
}
Brim is heading for 1.0. Programs run on a reference interpreter or compile to native executables with LLVM, on Linux, macOS and Windows. Go is still Ordnary's production backend. See Status for what 1.0 covers.
Ordnary's backend is written in Go, and our frontend in Angular. Go served us well: it compiles fast, deploys as a single binary, and is easy to read. But as our services grew, the same classes of bugs and friction kept coming back, and most of them are not fixable in Go itself because they come from its core design decisions.
Brim is our answer: keep what makes Go productive, and design out the parts that cost us time.
1. The backend and the frontend do not share a type system.
Every API type exists twice: as a Go struct with json:"..." tags, and as a hand-written TypeScript interface in Angular. Rename a field in Go and the Angular app still compiles; it only breaks at run time, in a browser. In Brim, API types are declared once: brim gen ts --angular generates the TypeScript types and an Angular client from them, so a breaking change becomes a compile error on both sides.
2. Errors are easy to ignore.
if err != nil { return err } appears everywhere, and result, _ := doSomething() silently throws an error away. Nothing forces the caller to look at it. In Brim, errors are Result values the compiler makes you handle, and ? passes them up.
3. nil is everywhere.
Nil pointer dereferences, writes to nil maps, and the "typed nil inside an interface is not equal to nil" trap are all run-time panics in Go. Brim has no null value at all: absence is an Option, which the compiler checks.
4. No real enums.
Go has iota constants, not enums. A switch over them is never checked for missing cases, and any integer can sneak in as a "status". Brim has enums (sum types) with exhaustive match: add a new case and the compiler lists every place that needs to handle it.
5. Integer overflow is silent.
In Go, math.MaxInt64 + 1 quietly wraps around to a large negative number. Brim checks every integer operation and stops with an error that points at the exact expression:
error[R0001]: integer overflow in `+`
--> billing.brim:3:12
|
3 | return total + fee
| ^^^^^^^^^^^
|
= note: call stack: main -> charge
6. Everything is mutable, and zero values hide mistakes.
In Go any variable can be reassigned from anywhere in its scope, and forgetting to set a field or a variable silently gives you 0, "" or nil. In Brim, variables are immutable unless declared let mut, and every variable must be initialised explicitly; there are no implicit zero values.
7. The compiler blocks you for the wrong things and helps too little with the right ones.
Go refuses to build while you have an unused variable or import, which interrupts every debugging session, but gives terse messages for real type errors. Brim reports harmless leftovers (an unneeded mut, unreachable code) as warnings, and real errors with codes, source excerpts and suggestions:
error[E0218]: cannot assign to immutable variable `count`
--> handler.brim:8:5
|
7 | let count: i64 = 0
| ----- declared here
8 | count = count + 1
| ^^^^^ cannot be reassigned
|
= help: make it mutable: `let mut count`
| Go | Brim | Brim status | |
|---|---|---|---|
| Integer overflow | wraps silently | stops with a located error | available |
| Variables | mutable by default | immutable by default, let mut to opt in |
available |
| Uninitialised values | implicit zero values | every variable is initialised explicitly | available |
| Null | nil pointers, maps, interfaces |
no null value; Option[T] |
available |
| Unused code | build error | warning | available |
| Compiler errors | short messages | error codes, excerpts, suggestions | available |
| Enums | iota constants |
sum types with exhaustive match |
available |
| Error handling | if err != nil, easy to ignore |
Result values and ?, enforced by the compiler |
available |
| API types shared with Angular | written twice, by hand | declared once, TypeScript and an Angular client generated | available |
| Concurrency | goroutines with shared memory, data races possible | tasks and channels, no shared changeable memory | available |
| Native single-binary deploys | yes | yes | available (LLVM; Linux, macOS, Windows) |
Fast builds, a small language you can learn in a day, readable code without clever tricks, a single binary to deploy, and tooling in the box. Brim is not trying to be Rust: there is no borrow checker, and memory will be managed automatically (see Memory management).
Brim is heading for 1.0: the language and its standard library are complete for backend services and command-line tools, and from 1.0 they are stable (see Releasing).
- Language: structs, enums with exhaustive
match, generics, traits, closures, tuples,OptionandResultwith?, lists, maps and sets with copy-on-write value semantics,i64,i32,u8,u64,f64, checked arithmetic, f-strings, modules withpub. The specification describes all of it. - Concurrency: tasks on threads with channels (
std.task), without shared changeable memory. - Standard library: files, processes, time, logging, random numbers, TCP, an HTTP/1.1 server and client (HTTPS through the system), JSON with typed
encode/decode, PostgreSQL, hashes and base64. - Tools: one
brimcommand to run, check, build, test, format, document (brim doc), generate TypeScript and Angular clients (brim gen ts), and manage packages (brim publish, workspaces,.brimlock); a language server and packages for 13 editors (seeeditors/). - Backends: a reference interpreter, and native executables through LLVM 22 on Linux, macOS and Windows, with debug information for lldb and gdb. Both behave the same; the tests compare them on every program.
- Packages: a registry at registry.brimlang.com, and dependencies from directories and git.
Not in 1.0: the items in section 12 of the specification, TLS for std.postgres and HTTP servers (put a proxy in front), and cross-compilation. The roadmap lists what comes next.
Install the latest release on macOS or Linux:
curl -fsSL https://brimlang.com/install.sh | shOn Windows (PowerShell): irm https://brimlang.com/install.ps1 | iex.
To build from source, you need Rust 1.88 or newer, installed with rustup. LLVM is only needed for native builds (see below).
git clone https://github.com/ordnary-com/brim.git
cd brim
cargo build --releaseThe compiler is target/release/brim, or install it with cargo install --path crates/brim-cli.
brim run examples/hello.brim # check and run a program
brim run examples/control_flow.brim # FizzBuzz and primes
brim check examples/variables.brim # report errors without running
brim check --emit ir examples/functions.brim # show the intermediate representation
brim run examples/project # run a project described by .brimproj
brim new hello && cd hello && brim test # create a project and run its tests
brim fmt # format the project's source files
brim --helpTo compile native executables, build brim with the llvm feature. It needs LLVM 22 and a C compiler; the development guide has the setup.
export LLVM_SYS_221_PREFIX="$(brew --prefix llvm@22)" # macOS; see the guide for Linux
cargo build --release --features brim-cli/llvm
brim build examples/control_flow.brim && ./examples/control_flow
brim build --release examples/project # writes examples/project/target/helloA project is a directory with a .brimproj:
[package]
name = "hello"
version = "0.1.0"
edition = "2026"
[build]
entry = "src/main.brim"cargo test --workspace
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warningsRead CONTRIBUTING.md and the development guide first. Language changes start with an update to the language specification.
Repository layout
crates/
brim-diagnostics source files, spans, diagnostics and their rendering
brim-lexer source text to tokens
brim-ast syntax tree
brim-parser tokens to syntax tree
brim-semantic name resolution and type checking
brim-ir intermediate representation and validator
brim-lower checked program to IR
brim-interp reference interpreter (temporary execution path)
brim-codegen LLVM backend and native runtime (`llvm` feature)
brim-runtime runtime ABI and runtime functions
brim-stdlib builtin function catalogue
brim-manifest .brimproj parsing and validation
brim-fmt the formatter
brim-package dependencies and .brimlock
brim-tsgen TypeScript types for API contracts
brim-driver loads a program's modules (shared by the command and the language server)
brim-lsp the language server
brim-cli the `brim` command
assets/ logo and other project artwork
docs/ language spec, architecture, error handling, roadmap, development
editors/ support for VS Code, Neovim, Helix, Zed, Emacs, JetBrains, Sublime and more
site/ documentation site (mdBook)
examples/ example programs and an example project
tests/ lexer, parser, diagnostics and integration fixtures
- Documentation site: a tour of the language, guides and the reference (source in
site/) - Language specification
- Architecture
- Error handling and diagnostic codes
- Roadmap
- Development guide
- Releasing and stability
- Security policy · Changelog
Licensed under the Apache License, Version 2.0. See LICENSE.
