A memory-safe systems language with second-class references: no lifetimes, no GC, one owner per value.
from "std/http" import { Context, Response, Router, serveRouter }
fn main(): i32 {
var r = Router.new()
r.get("/", (c: &mut Context) => Response.Html("hello from milo"))
r.get("/users/:id", (c: &mut Context) => Response.Text($"user {c.param("id")!}"))
serveRouter(8080, r)!
return 0
}
Compiles through LLVM to a static binary.
git clone https://github.com/milo-language/milo && cd milo
./milo run examples/hello.milo # needs bun and clangMilo moves fast, so build from source (install has the details and prebuilt binaries).
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 are parameters only. You cannot return one, store one in a struct, or keep one past the call.
That one restriction is what keeps the rest of the language simple. A reference that can outlive the call that made it is the reason C has dangling pointers, Rust has lifetime annotations, and Java and Go have a garbage collector. A reference that only travels down the call stack cannot outlive what it points to, because the owner is still alive in the caller until the call returns. So safety needs no annotations and no runtime. What you give up is pointing into memory you do not own: you own the buffer and carry an index, a Span, or an arena handle, or you clone().
It also buys local reasoning: the function you are reading is the whole story of the values it touches. No pointer into its locals can exist anywhere else, so every mutation is visible at the call site.
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) // the only line that can change v
print(v) // [3, 0, 4, 0, 9]
}
The ownership checker settles everything inside one function. It never consults a lifetime on a signature three modules away, and neither do you.
| What you want | C | Rust | Milo |
|---|---|---|---|
| Return a pointer into a buffer you still hold | char *, you promise it stays valid |
fn longest(...) -> &'a str |
Not expressible. Return an index, a Span, or an owned string. |
| A parser that keeps the input | struct Parser { char *src; } |
struct Parser<'a> { src: &'a str } |
Own the input; store a cursor (pos: i64). |
| Iterator over a collection | pointer into the array | Iterator<Item = &T> |
A cursor; each step takes the store: scanNext(&store, &mut cursor). |
| Graph, parent pointer, DOM | Node *next |
Rc<RefCell<Node>> or an arena crate |
std/arena: Arena<T> plus Handle<T>, checked at runtime. |
| Temporary read / mutation in a call | pointer argument | &T / &mut T |
&T auto-borrowed; &mut T written at the call: f(&mut x). |
| Two owners of one buffer | two pointers, good luck | lifetimes, 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. Also in the language: requires / ensures contracts checked by milo prove, and concurrency without Send / Sync (a value that cannot hold a borrow moves to another task as is).
- Returning or storing a view. Use an index,
Span, owned copy, or arena handle. - Some zero-copy. Where Rust hands out a borrow, Milo sometimes asks for a
clone(). - One check moves to runtime. "This offset still belongs to that buffer" is a named runtime failure, not a compile error and not a segfault.
Good fit: CLIs, services, compilers, emulators, anything you would write in careful C as a buffer plus integer ids. Awkward fit: code that wants to keep an object graph as is (widget trees with parent pointers, intrusive lists, parsers that store slices of their input). Those become arenas and handles, and won't look like the original (what that looks like).
Young, but dogfooded: 250k+ lines of Milo across a port of the compiler, a JS engine, three emulator cores, a debugger, and a dozen packages. Nearly every unsafe block is the C boundary; none exist because the ownership model rejected the program.
More: why there are no lifetimes · memory safety vs Rust · std/arena