What if references can’t be stored?
Make references temporary — never in a struct, never returned from a function — and lifetime annotations stop being necessary. There is nothing left to track.
Somewhere between Rust and Go. Closer to Rust on safety, closer to Go on ceremony. Whether the trade actually works out is what I’m trying to find out.
or build from source with cargo build --release — needs a Rust toolchain
func grep(path: string, pat: string) -> void or IoError {
mut file = try fs.open(path)
ensure file.close()
let text = try file.read_text()
for line in text.lines() {
if line.contains(pat) { println(line) }
}
}
Three ideas do most of the work
Everything else in the language is downstream of these.
References are temporary
You can borrow a value for a call or an expression. You cannot put the borrow in a struct field, and you cannot return it — there is no syntax that would let you, which is the point.
So there is no lifetime to annotate, no variance to reason about, and nothing in a signature except the types.
// `cart` is borrowed. The caller still owns it
// afterwards, and no signature says so.
func total(cart: Vec<Item>) -> i64 {
mut sum: i64 = 0
for item in cart { sum += item.price }
return sum
}
Everything is a value
No reference types, and no Box/Rc/Arc distinction to learn. Values up to 16 bytes copy, anything bigger moves, and you write .clone() when you want a second one.
That is more clones than Rust asks for. Every one of them is written down.
let a = Point { x: 1.0, y: 2.0 }
let b = a // 16 bytes: copied
mut names = Vec.new()
names.push("ada")
let taken = names // bigger: moved
let copy = taken.clone() // say so to get two
Graphs get links, not lifetimes
A Rack<T> owns nodes at stable addresses. A Link<T> points at one, and is the single kind of reference you can keep in a field.
Deleting a node sets every edge pointing at it to none before the delete returns. A dangling link therefore never exists, so following a live one is just a pointer hop — no generation counter, no liveness check.
struct Node {
id: i32
peer: Link<Node>?
}
let a = world.insert(Node { id: 1, peer: none })
let b = world.insert(Node { id: 2, peer: none })
a.peer = b
world.delete(b)
// a.peer is `none` now — nothing to check for
Files and sockets get consumed
Resources are linear: the compiler makes you consume them exactly once. ensure file.close() defers that consumption to the end of the scope, which is what lets try bail out early without leaking the handle.
Linearity, deferred consumption and error propagation are three separate rules, and the idiom at the top of this page falls out of them composing. It’s the part of the design I’m happiest with.
func copy(from: string, to: string) -> void or IoError {
let src = try fs.open(from)
ensure src.close()
mut dst = try fs.create(to)
ensure dst.close()
// either `try` can leave. both files still close.
try dst.write(try src.read_text())
}
What it costs
If you want pointer-level control over layout and traversal, use Rust or C++. Rask doesn’t offer it.
You give up
- References in struct fields
- Returning a reference from a function
- Implicit copies of anything large
- Pointer-chasing a graph you built by hand
You get
- No lifetime annotations, anywhere
- Signatures you read in one pass
- Allocations visible at the call site
- Deterministic cleanup, no GC pauses
Strings are the deliberate exception. string is immutable and refcounted, so it is Copy at 16 bytes and passes around like an integer — no .clone(). The visible cost is reserved for the things that actually own heap memory, like Vec and Map.
Where it actually stands
Measured 2026-09-12 by running it. This section gets re-measured, not copy-pasted.
Runs today
- Native codegen via Cranelift, plus an interpreter that acts as the reference for what the answer should be
- Ownership, moves, borrows, linear resources
RackandLinkfor graphs, on both backends- Structs, enums, generics, traits,
comptime spawn/join, channels, thread pools- Packages, workspaces, watch mode, LSP
Not there yet
- No fibers.
spawnis an OS thread; the M:N scheduler is designed, not built - Panics abort instead of unwinding
- x86-64 only, and SIMD is a stub
Caveats
- Around 80 open issues, most of them codegen getting memory release wrong
- It’s a solo project, so fixes land in waves
- Pre-0.1: breaking changes whenever a design turns out wrong
There are five test programs: a sensor processor, a grep clone, a game loop with entities, a text editor with undo, and an HTTP JSON API server. All five run natively. The HTTP one started working last week, once a bug that corrupted the first eight bytes of every response was fixed. They live in examples/ and CI runs them.
Ideas and complaints both welcome on GitHub issues.
rask