Skip to content

MiloA memory-safe systems language with second-class references.

No lifetimes, no garbage collector, and every mutation visible at the call site.

Milo
sh
git clone https://github.com/milo-language/milo && cd milo && ./milo run examples/hello.milo
milo
fn main() {
    let name = "world"
    print($"hello, {name}")
}
milo
fn main() {
    let names: Vec<string> = ["milo"]
    let team = names      // `names` moves here. It is no longer yours

    print(team)           // ["milo"]
    print(names)          // error: use of moved variable 'names'
}
milo
fn total(xs: &Vec<i64>): i64 {
    var sum = 0
    for x in xs { sum += x }
    return sum
}

fn double(xs: &mut Vec<i64>): void {
    for i in 0..xs.len { xs[i] *= 2 }
}

fn main() {
    var v: Vec<i64> = [1, 2, 3]
    print(total(v))    // 6: borrowed for this call, no &v needed
    double(&mut v)     // the only kind of line that can change v
    print(v)           // [2, 4, 6]
}
milo
struct Parser {
    src: &string,   // error: references cannot be stored in structs
}

fn longest(a: &string, b: &string): &string {   // error: cannot return a reference
    if a.len > b.len { return a }
    return b
}
milo
from "std/fetch" import { fetch }
from "std/runtime" import { Promise }

fn main() {
    let a = Promise<i32>.run(() => fetch("https://example.com")!.status)
    let b = Promise<i32>.run(() => fetch("https://httpbin.org/get")!.status)

    print($"{a.await()!} {b.await()!}")   // 200 200
}
milo
fn clamp(x: i64, lo: i64, hi: i64): i64
    requires lo <= hi                       // the caller's obligation
    ensures result >= lo && result <= hi    // proven, for every input that meets it
{
    if x < lo { return lo }
    if x > hi { return hi }
    return x
}

Second-class references ​

A value is first-class if you can store it, return it and pass it around, and second-class if you can only pass it down into a call. In Milo, references are second-class: &T and &mut T exist only as function parameters. You can't return one, store one in a struct, or keep one past the call.

A reference that can outlive its call is the reason C has dangling pointers, Rust has lifetime annotations, and Java and Go have a garbage collector. One that only travels down the call stack can't outlive its owner, so Milo needs none of the three.

It also buys local reasoning: &mut x at a call site is the full blast radius of a mutation, and the checker settles every ownership question inside one function, never by a signature three modules away.

milo
fn zeroNegatives(values: &mut Vec<i64>): void {
    for i in 0..values.len {
        if values[i] < 0 {
            values[i] = 0       // in-place, no copy, no allocation
        }
    }
}

fn main(): void {
    var v: Vec<i64> = [3, -1, 4, -5, 9]
    zeroNegatives(&mut v)      // v is mutated here and nowhere else
    print(v)                   // [3, 0, 4, 0, 9]
}

What you get: no lifetime annotations, no GC or RC, concurrency without Send/Sync, requires/ensures contracts checked by milo prove.

What you give up: returning or storing a view. Use an index, a Span, an owned copy, or an arena handle. The tie between a stored offset and its buffer becomes a named runtime check where Rust's lifetime is a compile error (why that trade).

In C, in Rust, in Milo ​

What you wantCRustMilo
Return a pointer into a buffer you still holdchar *, you promise it stays validfn longest(...) -> &'a strNot expressible. Return an index, a Span, or an owned string.
A parser that keeps the inputstruct Parser { char *src; }struct Parser<'a> { src: &'a str }Own the input; store a cursor (pos: i64).
Iterator over a collectionpointer into the arrayIterator<Item = &T>A cursor; each step takes the store: scanNext(&store, &mut cursor).
Graph, parent pointer, DOMNode *nextRc<RefCell<Node>> or an arena cratestd/arena: Arena<T> plus Handle<T>, checked at runtime.
Temporary read / mutation in a callpointer argument&T / &mut T&T auto-borrowed; &mut T written at the call: f(&mut x).
Two owners of one buffertwo pointers, good lucklifetimes, or clone / Arc.clone(), or seal it and share a read-only copy.

Same memory-safety guarantees as Rust wherever both languages can express the program. 250k+ lines of Milo so far; nearly every unsafe block is the C boundary, and none exist because the ownership model said no (the full matrix).

Built with Milo

Milo is young, still a puppy 🐶, but we dogfood it hard: every program below is real, and building them is how we find what needs fixing. It's ready for you to try today.