Rill v0.2

A functional language
that ships 33 KB.

Rill has ML's types and Go's concurrency, 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 — and the compiler decides where every allocation is freed, so nothing pauses.

See the language 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.

Rill 33 KB
OCaml ~1 MB
Go 2.1 MB

The language

Nine constructs, and no others. Everything below is the whole of the syntax.

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

There is no for and no while. A call in tail position compiles to a jump, so this runs in constant stack at any size.

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 56 MB and start in microseconds.

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. Set one variable and the runtime tells you what was still alive at exit:

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

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.

One canonical spelling

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

Numbers

Apple M4, outputs verified identical across all three languages. Reproduce with python3 benchmarks/run.py.

BenchmarkRillGoOCamlRill RSSGo RSSOCaml RSS
fib(35)17.2 ms22.0 ms23.7 ms1.3 M3.6 M2.1 M
binary trees33.0 ms55.9 ms60.0 ms13.3 M16.6 M18.3 M
100k-thread ring13.5 ms96.2 ms61.7 ms53.4 M269.4 M103.0 M
100M-iteration loop127.6 ms271.6 ms295.3 ms1.3 M3.5 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 seven times, and the second, where a compiler inserting frees beats two mature garbage collectors on time and memory.

What it does not have

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

Rill is version 0.2. It is a good language to learn these ideas in and a bad one to run a business on, and this page would rather say so than have you find out.

The book

Functional Programming with Rill — 78 pages, sixteen 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.

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

Getting it

git clone …/funclang && cd funclang
cargo build --release          # builds ./target/release/rill

rill run   program.rill        # compile to a temporary binary and run
rill build program.rill -o p   # keep the binary
rill fmt   program.rill        # canonical formatting
rill repl                      # definitions persist for the session

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