Inference everywhere
Annotations are optional. Generics, traits and requirements like “must be ordered” are inferred from the body and checked.
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 bookAlcedo atthis · H. von Kittlitz, 1832
The same program, compiled. Bars are the true ratio — not adjusted to fit.
Nine constructs, and no others. Everything below is the whole of the syntax.
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`
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
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
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
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
Annotations are optional. Generics, traits and requirements like “must be ordered” are inferred from the body and checked.
Nested patterns, and a checker that names the case you forgot rather than a runtime default branch.
Absence is Option, failure is Result. Nothing unwinds the stack from under you.
One specialized copy per concrete type. Nothing is boxed and trait dispatch is resolved at compile time.
extern "m" fn sqrt(x: Float) -> Float and it links. Opaque pointers, explicit string bridging, exact C widths.
rill fmt gives every program exactly one formatting, which is what makes it predictable to read and diff.
Apple M4, outputs verified identical across all three
languages. Reproduce with python3 benchmarks/run.py.
| Benchmark | Rill | Go | OCaml | Rill RSS | Go RSS | OCaml RSS |
|---|---|---|---|---|---|---|
| fib(35) | 17.2 ms | 22.0 ms | 23.7 ms | 1.3 M | 3.6 M | 2.1 M |
| binary trees | 33.0 ms | 55.9 ms | 60.0 ms | 13.3 M | 16.6 M | 18.3 M |
| 100k-thread ring | 13.5 ms | 96.2 ms | 61.7 ms | 53.4 M | 269.4 M | 103.0 M |
| 100M-iteration loop | 127.6 ms | 271.6 ms | 295.3 ms | 1.3 M | 3.5 M | 2.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.
Some of these are choices and some are a version number. Both are listed.
Buf, a counted array, for C and for hot tables.Result and Option instead.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.
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 languagegit 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.