Ownership Mental Model
How Loon manages memory without garbage collection or annotations.
Why Ownership
A language has three ways to deal with memory. It can hand the job to you (C, C++), which is fast and terrifying in equal measure. It can hand the job to a garbage collector (Java, Go, Python), which is convenient but pays for that convenience in latency spikes, heavier memory use, and pauses you cannot predict. Or it can use ownership (Rust), which buys deterministic, zero-overhead cleanup with safety proven at compile time.
Loon chooses ownership, then asks a question Rust did not: does the programmer actually need to see the machinery? Rust showed the ownership model works beautifully. Loon wagers that the compiler can make the ownership decisions for you, so you keep the performance and skip the annotation tax.
How It Differs from Rust
In Rust, you are an active participant in ownership. You write &, &mut, 'a, and where T: 'static. You choose between Box, Rc, and Arc. You spend real attention reasoning about lifetimes.
Loon takes a different position entirely. You write code in terms of values, and the compiler decides whether to move, borrow, or copy. It inserts borrows where they are safe, clones where they are necessary, and drops values at exactly the right moment. You never write a lifetime annotation, not because Loon hides them, but because they are not needed.
Note
This is not complexity hidden away. It is decisions removed. The compiler holds strictly more information than you do about how long a value lives, so it makes the better call.
The Compiler's Decision Process
When you pass a value to a function or bind it to a new name, the compiler follows a clear sequence of rules. You do not need to know them to write Loon, but knowing them builds an accurate intuition for what happens beneath the surface.
1. Copy for primitives
Integers, floats, booleans, and characters are always copied. They are small enough that duplicating them costs nothing worth counting, so the compiler tracks no ownership for them at all.
[let x 42]
[let y x]
[println x] ; fine — Int is copied2. Auto-borrow when safe
If the compiler can prove a reference will not outlive its owner, it passes a borrow rather than moving. This is the common case for collections, and it is why most Loon code works without you ever thinking about ownership.
[fn length [xs] [len xs]]
[let items #[1 2 3]]
[println [length items]]
[println [length items]] ; items not consumedThe compiler sees that length only reads xs and returns a primitive. The reference has no way to escape, so a borrow is safe.
3. Move by default
When a value is not a primitive and cannot be borrowed safely, ownership transfers to its new location. This is the fallback, and it is how Loon guarantees memory is freed exactly once, never twice and never not at all.
[fn consume [xs]
[println [str "got " [len xs] " items"]]]
[let items #[1 2 3]]
[consume items]
; items has been moved — using it here is a compile errorValue Lifecycle
Every value in Loon moves through the same predictable arc: creation, use, and drop. No finalizers firing at unpredictable times, no weak references to track. Three stages, each with a known moment.
Creation
A value is created and bound to a name, and that name becomes its owner. Ownership begins here and follows the value wherever it travels.
[let user {:name "Ada" :age 30}]Use
The value is read, passed to functions, woven into expressions. At each use, the compiler decides whether it is a borrow, temporary access that leaves ownership in place, or a move, a permanent transfer of ownership.
[println [get user :name]] ; borrow — user still valid
[save-to-db user] ; move — ownership transfersDrop
When the owner goes out of scope, the value is dropped and its memory reclaimed at once. Not "eventually, whenever the GC gets around to it." Now, at a point you can name in the source.
[fn process []
[let data [load-data]]
[let result [transform data]]
result]
; data is dropped here (if it was moved to transform,
; it was dropped there instead)Common Patterns
Clone when you need two owners
Sometimes you genuinely need the same data in two places. Clone it explicitly when you do. The explicit call keeps the cost on the page where you can see it, so every allocation has a visible cause.
[let a #[1 2 3]]
[let b [clone a]]
; both a and b are independent ownersReturn instead of mutate
Prefer returning a new value to mutating an old one. Ownership makes this efficient rather than wasteful: when the compiler can see that nobody else is looking at the old value, it reuses that memory in place.
[fn add-item [cart item]
[append cart item]]
[let cart #[]]
[let cart [add-item cart "book"]]
[let cart [add-item cart "pen"]]Tip
Rebinding under the same name is idiomatic in Loon. Each let creates a fresh binding that shadows the one before it.
Pipeline-friendly design
The pipe operator and ownership fit together naturally, because each step consumes the result of the last and produces a fresh one. The intermediate values live exactly as long as they are needed, and not a moment longer.
[pipe #[1 2 3 4 5]
[filter [fn [x] [> x 2]]]
[map [fn [x] [* x 10]]]
[fold 0 +]]Each intermediate vector is consumed by the next step, and its memory is free to be reused at once. No garbage collector, no manual frees, only values flowing through the pipeline.