Rill v0.13

A functional language
that ships 51 KB.

Rill has ML's types, Go's concurrency and SDL's state machines, compiled straight to native code. No virtual machine, no garbage collector, no runtime to install.

Type inference, algebraic data types, exhaustive pattern matching, lightweight threads over typed channels, processes with mailboxes drawn as the telephone exchanges drew them — and the compiler decides where every allocation is freed, so nothing pauses.

See the language The reference Try the model editor Read the book
Engraving of a common kingfisher perched on a reed

Alcedo atthis · H. von Kittlitz, 1832

Hello world, drawn to scale

The same program, compiled. Bars are the true ratio — not adjusted to fit. The 51 KB includes the crash reporter: a fault names the function and the line.

Rill 51 KB
OCaml ~1 MB
Go 2.1 MB

The language

A small language, read in an afternoon. What follows is most of the syntax; traits, records, machines and a few conveniences are the rest, and the reference has all of it, chapter by chapter, with the programs that show each construct.

Data, and taking it apart

A type is a list of the shapes a value can have. match tests the shape and binds the pieces in one step, and the compiler checks that you covered every case.

type Shape
  Circle(r: Float)
  Rect(w: Float, h: Float)

fn area(s) = match s
  Circle(r) -> 3.14159 * r * r
  Rect(w, h) -> w * h

fn main() =
  shapes = Cons(Circle(1.0), Cons(Rect(2.0, 3.0), Nil))
  println(sum(map(shapes, area)))
$ rill run shapes.rill
9.14159

Leave out a case and the program does not compile — and the compiler names the one you forgot:

colour.rill: type error at line 6: match is not exhaustive; no arm matches `Blue`

Loops are recursion

A call in tail position compiles to a jump, so this runs in constant stack at any size. for x in xs and while c with (acc) = init exist for the reader, and are this underneath.

fn sum_to(n) = go(n, 0)

fn go(i, acc) =
  if i == 0 then acc
  else go(i - 1, acc + i)
go(20000000, 0) → 1.3 MB resident

Pipelines

x |> f(a) means f(x, a), so a calculation reads in the order it happens instead of inside-out.

range(1, 10)
  |> filter(\x -> x % 2 == 0)
  |> map(\x -> x * x)
  |> sum
220

Concurrency: strands

Many lightweight threads over typed channels, taken from Go. One of them is a strand — a strand of a rope, of which a program is many. A hundred thousand of them cost about 65 MB and start in microseconds; with --parallel they spread across cores, and the compiler refuses a program in which two of them could write the same storage.

fn worker(jobs, results) =
  n = recv(jobs)
  send(results, n * n)
  worker(jobs, results)          # a loop that never ends

fn main() =
  jobs = channel()
  results = channel()
  spawn worker(jobs, results)
  send(jobs, 7)
  println(recv(results))
49

No collector

The compiler works out where each value's last use is and inserts the release there, following ownership rules. Nothing runs periodically; a value is freed at the instruction after it dies, and a box that dies where a new one is made is reused in place. A reference ring — the one thing counting cannot free — is refused at compile time, where it is written. Set one variable and the runtime tells you what was still alive at exit:

$ RILL_DEBUG_ALLOC=1 ./program
100000
live allocations: 0

Models

A machine is a process in the sense the telephone exchanges gave the word — a state machine with a mailbox, one strand, one signal at a time — and a system is the block diagram between the kinds of machine and the environment, which the compiler checks against the code.

system exchange
  env -> line: OffHook, Digit, OnHook, Answer, Report
  line -> env: Status
  line <-> line: Ring, Answer, OnHook

machine line(me: Chan(Mail(Signal)), log: Chan(Str))
  state Idle
    OffHook -> Dialling(Nil)
    Ring -> Ringing(sender)
  state Dialling(digits)
    Digit(n) if n >= 0 -> Dialling(Cons(n, digits))
    after 50 ->
      send(log, "gave up dialling")
      Idle
  state Ringing(caller)
    Answer ->
      output(caller, Answer)
      Talking(caller)

Timers, save, priority, procedures with states of their own, and a signal no arm names is consumed and the state kept — the rules SDL had. A run traced with RILL_TRACE=1 is a sequence chart (rill msc); on the virtual clock a night of retransmissions takes no time; rill diagram draws the system and each machine in SDL's own symbols; and a signal a machine sends that no route carries is a compile error naming both.

The editor at rillsdl.baltavista.com draws all of this in the browser: click a state or an input to build it in a form, run a scenario and watch the chart grow while the machines light the state they are in. The code stays the model — every edit is written back through the formatter — and the programs run in the compiler's sandbox, so try anything.

What you get

Inference everywhere

Annotations are optional. Generics, traits and requirements like “must be ordered” are inferred from the body and checked.

Sum types + exhaustiveness

Nested patterns, and a checker that names the case you forgot rather than a runtime default branch.

No null, no exceptions

Absence is Option, failure is Result. Nothing unwinds the stack from under you.

Monomorphized

One specialized copy per concrete type. Nothing is boxed and trait dispatch is resolved at compile time.

Talks to C directly

extern "m" fn sqrt(x: Float) -> Float and it links. Opaque pointers, explicit string bridging, exact C widths, inline asm.

One canonical spelling

rill fmt gives every program exactly one formatting, which is what makes it predictable to read and diff.

A standard library

Forty-three modules in Rill: JSON, YAML, CSV, deflate, HTTP and TLS, WebSockets, a regex, a B+ tree store, SHA-2, big integers, decimals, UUIDs, a grid that keeps up with NumPy — every function with an example the test suite runs.

Tests, docs, a language server

rill test runs every test_ function in a process of its own; rill doc reads the comments off the file; rill lsp answers any editor, with a VS Code extension.

Cores, wasm, a sandbox

--parallel spreads strands across cores and refuses shared writes; --target wasm32-wasip1 runs in a browser; --sandbox refuses a program that would reach past the process.

Numbers

Rill 0.13.0 on an Apple M4, 16 September 2026; Go 1.23, OCaml 5.5; outputs verified identical across the languages, time the median of 25 runs. Reproduce with python3 benchmarks/run.py.

BenchmarkRillGoOCamlRill RSSGo RSSOCaml RSS
fib(35)15.0 ms20.9 ms22.0 ms1.3 M3.6 M2.1 M
binary trees16.3 ms55.1 ms57.3 ms13.3 M14.6 M18.3 M
100k-thread ring12.7 ms94.4 ms64.8 ms64.7 M269.3 M103.0 M
100M-iteration loop121.0 ms264.6 ms284.2 ms1.3 M3.6 M2.1 M
Four microbenchmarks are not a workload. The first row is really “both of these are LLVM-quality code generation”; the rows that carry information are the third, where the concurrency implementation differs by five times, and the second, where a compiler inserting frees beats two mature garbage collectors on time and memory. Against C (measured 13 September, before this release) — twenty programs, each written once in Rill and once in C to the same algorithm and layout — Rill wins eleven, is within run-to-run noise on nine, and is behind on none; on disk and sockets it is ahead of Go on the four stream programs and within a few percent on the round trips, which are the kernel's. Eight independent computations on eight strands scale 1.0× · 1.9× · 3.1× on one, two and four workers. The reasons, step by step, are in benchmarks/.

What it does not have

Some of these are choices and some are a version number. Both are listed.

  • MutationChosen. No assignment, no mutable fields — a record is updated by making a new one, { p | x = 1 }. The exceptions are storage: Buf, Map and a string builder, for C and for hot tables.
  • A garbage collectorChosen. Reference counting decided at compile time; a ring of references is refused where it is written, since counting could not free it.
  • Exceptions and nullChosen. Result and Option instead, with ? to pass a failure up.
  • LazinessChosen. Strict evaluation, so allocation appears where you wrote it.
  • Higher-kinded typesVersion number. A trait is over a type, not over a container of types; map is a function on lists, not a method of a functor.
  • Strings indexed by characterChosen. A string is bytes and #s counts them; the char_ functions and the text library walk it by code point when that is the question.
  • A second implementationVersion number. The compiler compiles itself, but there is one of it, no package manager, and nobody else has hit your bug.
Rill is version 0.13. It is a good language to learn these ideas in and a good one to model a protocol in; whether it is one to run a business on is a question this page would rather leave open than answer for you.

What's changed

0.13.0 — the model editor. The release where the models got an editor in the browser, with its server written in Rill on the standard library. The page shows a file's system as the block diagram, channels routed as SDL drew them, and each machine as a process in SDL's symbols, beside its coloured code, every symbol a click from its line.

Since then:

Every release, every change

Made with Rill

Programs that are not benchmarks, written in Rill and nothing else.

Rill Valley

A walkable low-poly valley at dusk, computed by Rill and rendered through WebGPU: the terrain, every pine, crystal and grass tuft, the walking camera and the shadow matrices are worked out in world.rill compiled to WebAssembly — the whole valley rebuilt in about four milliseconds, denser or sparser until the display's frame budget is spent. Runs in the browser; ?join shares the valley with whoever else is there, over a server that is Rill too.

Art Vandal

A first-person maze lit by torches, native on Apple silicon, with no C written anywhere — no shim library, no generated bindings, no build script: Metal, the window and the sound reached through extern declarations alone. A 405 KB binary and its assets, nothing to install.

The model editor

The SDL editor's server is a Rill program on the standard library — HTTP, JSON, processes, paths — that runs the compiler for the page and sandboxes what it runs. So is the server the valley's visitors share a world through, and the packer that puts the valley's assets into one file.

The book

Functional Programming with Rill — 86 pages, seventeen chapters.

It starts with what functional programming is, at length and before any Rill appears: expressions instead of commands, why assignment hides information, purity, higher-order functions, recursion and tail calls, and the types that make illegal states unrepresentable. Then the language, then algorithms — sorting, trees, a recursive-descent parser, graphs and flood fill, dynamic programming — each one put beside the same algorithm in Python, Java or Go. It ends underneath: memory without a collector, strands and channels, and machines — a protocol modelled, traced and drawn.

Every program in it was compiled and run by the compiler in the repository, and the output printed in the book is the output it produced. Where a listing shows a compiler error, that error came from running the broken program.

Download the PDF Back to the language

Download

Rill 0.13.0, packaged: the compiler, the runtimes, the standard library, the examples and the reference, in one directory. Needs a C linker on the PATH — the Xcode command-line tools on a Mac, gcc or clang on Linux — and nothing else.

macOS, Apple silicon

rill-0.13.0-macos-arm64.tar.gz · macOS 13 or later on M-series. Unsigned: a browser download is quarantined, so either curl -O it or xattr -dr com.apple.quarantine rill after unpacking.

Linux, x86-64

rill-0.13.0-linux-x86_64.tar.gz · glibc 2.36 or later (Debian 12, Ubuntu 22.04 and newer). Needs gcc or clang for the link.

Not yet

Intel Macs, Linux on ARM and Windows have no build: the compiler is built with a static LLVM, and only these two machines have been built on. Windows and wasm are targets — --target — from either.

curl -O https://rill.baltavista.com/dl/rill-0.13.0-macos-arm64.tar.gz
tar xzf rill-0.13.0-macos-arm64.tar.gz
rill/bin/rill run rill/examples/hello.rill        # hello, world

Checksums in SHA256SUMS. The wasm target needs wasm-ld, which comes with LLVM.

The commands

rill run   program.rill        # compile to a temporary binary and run
rill build program.rill -o p   # keep the binary; --parallel, --sandbox, --target wasm32-wasip1
rill test  program.rill        # every test_ function, each in its own process
rill check program.rill        # type-check only, saying everything it finds
rill fmt   program.rill        # canonical formatting
rill doc   lib -o docs         # Markdown from the comments the modules already have
rill diagram exchange.rill     # the system and the machines, drawn
rill repl                      # definitions persist for the session

The runtime is no_std and links as a static library, which is most of why the binaries are the size they are.